Eine reiche Anfragesprache verspricht ein populäres NoSQL-System auf seiner Startseite. Reicher als SQL? Ist das auffinden von Informationen einfacher, kann ich besser suchen als mit SQL? Das System gehört zur Gruppe der dokumentenbasierten NoSQL-Stores und wirbt mit Schemafreiheit für sich. Es speichert BSON-Dokumente in Sammlungen. Das Dokument lässt sich grob mit einem Datensatz/Tupel aus einer Tabelle/Relation eines RDBMS vergleichen.
Etwas Relationenalgebra
SQL ist eine konkrete Anfragesprache für eine relationale Datenbank. Eine Anfragalgebra kann benutzt werden um eine Anfragesprache mathematisch exakt zu fassen. Das klingt gruselig für den praktisch orientierten Anwender. Es hilft aber, um sich der Frage zu nähern wie reich die eine oder andere Sprache ist und ist auch gleich vorbei. In der Relationenalgebra finden wir:
- Projektion (Ausblenden von Spalten)
- Selektion (Zeilen suchen)
- Verbund (Tabellen verknüpfen)
- Vereinigung (Tabellen vereinigen)
- Differenz (Tabellen abziehen voneinander)
Spalten umbenennen(das ist nichtAS
, brauchen wir in der Praxis nicht)
Einen Teil können wir gleich streichen. Der fragliche NoSQL-Store kann weder Dokumente noch Sammlungen verknüpfen, vereinigen oder die Differenz bilden. Ein Verbund lässt sich bestenfalls aus mehreren Einzelabfragen nachbauen. Irgendwie logisch – er ist eher als hierarchisch denn als relational zu bezeichnen. SQL-Äpfel und NoSQL-Orangen sind zwei verschiedene Dinge.
- Projektion (Ausblenden von Spalten [Feldern])
- Selektion (Zeilen [Dokumente] suchen)
Verbund (Tabellen verknüpfen)Vereinigung (Tabellen vereinigen)Differenz (Tabellen abziehen voneinander)Spalten [Felder] umbenennen
Die Projektion
Im Rahmen der Projektion werden Spalten ausgewählt . Die Projektion findet sich gleich zu Beginn einer SELECT
-Anfrage:
SELECT [DISTINCT] Attribut | arithmetrischer Ausdruck | Aggregatfunktion
Gegeben sei eine Sammlung "leute" mit folgenden Dokumenten:
[ { "hausnummer" : 64, "_id" : { "$oid" : "50520155cc93742e0d0dac65" }, "vorname" : "Ulf" }, { "hausnummer" : 64, "_id" : { "$oid" : "50520263cc93742e0d0dac7e" }, "vorname" : "Gandalf, der Pflegehamster" } ]
In der Relationenalgebra filtert die Projektion Duplikate während SQL Multimengen von Tupeln zulässt. Ein SELET hausnummer FROM leute
liefert zwei identische Ergebniszeilen (Tupel). Das optionale DISTINCT
filtert die Duplikate: SELECT DISTINCT hausnummer FROM leute
liefert dasselbe wie db.leute.distinct("hausnummer")
.
[
{ "hausnummer" : 64 }
]
Statt alle Attribute/Felder/Spalten auszulesen, gilt es nur "vorname" auszugeben. Der entsprechende SQL-Ausdruck wäre SELECT vorname FROM leute
. Im NoSQL-Store lautet die Syntax db.leute.find({}, {vorname:1, _id:0})
.
[ { "vorname" : "Ulf" }, { "vorname" : "Gandalf, der Pflegehamster" } ]
Als nächstes soll die Projektion einen arithmetrischen Ausdruck enthalten, in SQL: SELECT hausnummer + 1 FROM leute
. Eine Entsprechung in der Anfragesprache des NoSQL-Stores konnte ich erst im Rahmen von MapReduce finden. MapReduce und SQL sind zwei verschiedene Kisten. Mehr dazu unten.
SQL unterstützt folgende Aggregatfunktionen: COUNT(*)
, SUM(spalte)
, MAX(spalte)
, MIN(spalte)
, AVG(spalte)
. Der erste ist einfach: db.leute.count()
. Dann wird es spannend, egal welche Funktion gewählt wird. Eine Möglichkeit ist MapReduce. Die zählt nicht. Die zweite Möglichkeit nennt sich nicht MapReduce obschon der Anwender eine "reduce"-Funktion selbst programmieren muß. Hier der Code für SUM(spalte)
: db.leute.group({key: {}, initial: {sum: 0}, reduce: function (doc, out) { out.sum += doc.hausnummer}})
. Der Möglichkeiten nicht genug, gibt es noch eine dritte Syntax. Die kommt aber erst bei einer Gruppierung ins Spiel und ist damit an dieser Stelle raus.
[ { "sum" : 128 } ]
Zwischenergebnis Projektion
Nochmal: MapReduce zählt nicht. Ich vergleiche die eingebaute Anfragesprache an dieser Stelle.
- Projektion (Ausblenden von Spalten): SELECT [DISTINCT] Attribut | arithmetrischer Ausdruck | Aggregatfunktion
- DISTINCT
- Attribut
arithmetrischer Ausdruck- Aggregatfunktion
- COUNT(*)
- SUM(spalte) (Benutzer liefert Funktion)
- MAX(spalte) (Benutzer liefert Funktion)
- MIN(spalte) (Benutzer liefert Funktion)
- AVG(spalte) (Benutzer liefert Funktion)
- Selektion (Zeilen suchen)
Verbund (Tabellen verknüpfen)Vereinigung (Tabellen vereinigen)Differenz (Tabellen abziehen voneinander)Spalten umbenennen
Die einfache Selektion
Mit der WHERE
-Klausel formuliert SQL die Selektion. Diese Betrachtung unterscheidet drei Fälle. Was gruselig aussieht, hilft sogleich beim Vergleich SQL vs. NoSQL-Anfragesprache.
WHERE Konstanten-Selektion: Attribut = | != | < | <= | > | >=Konstante Attribut-Selektion: Attribut = | != | < | <= | > | >= Attribut Verknüpfungen: (Attribut = | != | < | <= | > | >= Konstante|Attribut) und|oder|nicht (...) Ungewißheitsselektion: Attribut LIKE Konstante (Sonderzeichen: %| _) Nullselektion: Attribut IS NULL
Das erste ist einfach: SELECT * FROM leute WHERE hausnummer = 64
entspricht db.leute.find({"hausnummer": : 64})
. Gleichtes gilt für die Paarung SELECT * FROM leute WHERE hausnummer > 1
und db.leute.find({"hausnummer" : {$gt:1}})
.
Ein Vergleich von zwei Attrributen ist im NoSQL-Fall nur dann möglich, wenn der Anwender einen JavaScript-Ausdruck bereitstellt. Platt gesagt: der Anwender muß eine Funktion schreiben. Die Entsprechung eines SELECT * FROM leute WHERE hausnummer != vorname
ist db.leute.find({$where : "this.hausnummer != this.vorname"})
. Das Handbuch des NOSQL-Store merkt an, daß die Selektion mittels $where
langsamer ist als die Verwendung der eingebauten Vergleiche.
Im SQL gibt es Verknüpfungen von Bedingungen mit AND
, OR
und NOT
. Auf der anderen Seite sind die Namen $and
, $or
und $nor
. Schachtelung funktioniert, Gleichstand.
LIKE
bietet der NoSQL-Store nicht. Dafür einen auf regulären Ausdrücken basierten Textvergleich. LIKE
kann mittels eines regulären Ausdrucks emuliert werden. Selbstverständlich kann auch MySQL reguläre Ausdrücke auswerten: RLIKE.
Ohne Schemata müssen drei Fälle beim Test auf NULL
unterschieden werden: Dokumente bei denen das Attribut fehlt oder NULL
enthält, Dokumente bei denen das Attribut vorhanden ist und den Wert NULL
hat, Dokumente bei denen das Attribut nicht vorhanden ist. Alle drei Fälle sind abgedeckt. Damit lässt sich SQL IS NULL
ausdrücken.
Zwischenfazit nach der einfachen Selektion
Wieder einmal muß der Anwender selbst Funktionen schreiben, das meiste geht jedoch.
- Projektion (Ausblenden von Spalten): SELECT [DISTINCT] Attribut | arithmetrischer Ausdruck | Aggregatfunktion
- DISTINCT
- Attribut
arithmetrischer Ausdruck- Aggregatfunktion
- COUNT(*)
- SUM(spalte) (Benutzer liefert Funktion)
- MAX(spalte) (Benutzer liefert Funktion)
- MIN(spalte) (Benutzer liefert Funktion)
- AVG(spalte) (Benutzer liefert Funktion)
- Selektion (Zeilen suchen): WHERE Konstanten-Selektion: Attribut = | != | < | <= | > | >=Konstante.. (s.o.)
- Konstanten-Selektion: Attribut = | != | < | <= | > | >=Konstante
- Attribut-Selektion: Attribut = | != | < | <= | > | >= Attribut (Benutzer liefert Ausdruck/Funktion)
- Verknüpfungen: (Attribut = | != | < | <= | > | >= Konstante|Attribut) und|oder|nicht (…)
- Ungewißheitsselektion: Attribut LIKE Konstante (Sonderzeichen: %| _) (Emulation über Regular Expression, MySQL-Gegenstück: RLIKE)
- Nullselektion: Attribut IS NULL
Verbund (Tabellen verknüpfen)Vereinigung (Tabellen vereinigen)Differenz (Tabellen abziehen voneinander)Spalten umbenennen
Noch mehr zur Selektion: die geschachtelte Anfrage
Bereits in SQL-89 gibt es Unteranfragen, geschachtelte Anfragen, die unter WHERE
vorkommen können.
WHERE ... Quantifizierte Bedingung: ALL, ANY, IN, EXISTS
"Gibt’s doch!" mögen jetzt alle Leser aus rufen, die den Namen des NoSQL-Stores längt kennen. Falsch! Es gibt die Prädikate, es gibt keine geschachtelten Anfragen für, die Prädikate gedacht sind. db.leute.find({"hausnummer" : { $in : [1, 64] }})
entspricht SELECT * FROM leute WHERE hausnummer IN (1, 64)
.
Hingegen ist folgender SQL-Ausdruck nicht direkt in der Sprache des NoSQL-Store auszudrücken: SELECT * FROM leute WHERE hausnummer IN (SELECT hausnummer FROM pflegeeltern)
. Wie man leicht sieht, kommen wir hier in den Bereich des Verbunds.
Vorläufiges Fazit
Das Fazit ist nicht zu übersehen: SQL ist die reichere Anfragesprache.
- Projektion (Ausblenden von Spalten): SELECT [DISTINCT] Attribut | arithmetrischer Ausdruck | Aggregatfunktion
- DISTINCT
- Attribut
arithmetrischer Ausdruck- Aggregatfunktion
- COUNT(*)
- SUM(spalte) (Benutzer liefert Funktion)
- MAX(spalte) (Benutzer liefert Funktion)
- MIN(spalte) (Benutzer liefert Funktion)
- AVG(spalte) (Benutzer liefert Funktion)
- Selektion (Zeilen suchen): WHERE Konstanten-Selektion: Attribut = | != | < | <= | > | >=Konstante.. (s.o.)
- Konstanten-Selektion: Attribut = | != | < | <= | > | >=Konstante
- Attribut-Selektion: Attribut = | != | < | <= | > | >= Attribut (Benutzer liefert Ausdruck/Funktion)
- Verknüpfungen: (Attribut = | != | < | <= | > | >= Konstante|Attribut) und|oder|nicht (…)
- Ungewißheitsselektion: Attribut LIKE Konstante (Sonderzeichen: %| _) (Emulation über Regular Expression, MySQL-Gegenstück: RLIKE)
- Nullselektion: Attribut IS NULL
- Quantifizierte Bedingung: ALL, ANY, IN, EXISTS (Keine Unteranfragen)
Verbund (Tabellen verknüpfen)Vereinigung (Tabellen vereinigen)Differenz (Tabellen abziehen voneinander)Spalten umbenennen
MapReduce zählt nicht
Das vorläufige Fazit ist schon rot. Beim Fehlen des Selbstverbunds wird manch Programmierer rot. Und bei MapReduce werde ich es. Eine Anfragesprache sollte ad-hoc Formulierungen erlauben ohne ein Programm schreiben zu müssen. MapReduce benutzen bedeutet Funktionen und damit ein Programm zu schreiben. Eine Anfragesprache sollte optimierbar sein. Für MapReduce gibt es praktische keine Optimierungsregeln. Eine Anfragesprache sollte sicher sein indem Sinne, daß sie endlose Schleifen oder Ergebnisse verhindert. Jede Operation soll auf Mengen von Daten gleichzeitig arbeiten und nicht nur über einzelne Sätze/Tupel/Dokumente navigieren.
MapReduce ist raus in dieser Diskussion. Wer MapReduce sagt, der muß sich fragen warum er MySQL nicht ausschließlich über die InnoDB API anspricht. Ob nun JavaScript oder C ist doch egal…
Rant?
Nein. Es gibt einiges zu lernen für MySQL von den NoSQL-Stores. Aber ob diese konkrete Anfragesprache dazu gehört…
Happy hacking!
2012/09/13 at 21:51
Apache Hive erlaubt ad-hoc SQL Subset, das in MapReduce umgeschrieben wird. Hilft das?
2012/09/13 at 22:06
Ich mag den Begriff “NoSQL” nicht. Mein Toaster ist NoSQL…
Wenn Leute “NoSQL” sagen, meinen sie meistens einen Key-Value-Store ohne komplexe Strukturen, sondern mit einer einfachen GET/PUT API, die auf den Key zielt.
Und das beißt sich mit SQL überhaupt nicht. Ein MySQL-Server ist ein SQL-Layer auf einer Ebene von Storage-Engines, zB InnoDB, NDB, MyISAM.
So ist es also möglich, SQL auf einem Key-Value-Store zu packen. Damit gewinnt man an Entwicklungsgeschwindigkeit. Einen Join in SQL schreibe ich nachts nach 10 Bier. Das in Key-Value-API-Calls umzusetzen wird weh tun.
Zu MapReduce: MapReduce ist auch eine sehr einfache API. Aber dank Hive gibt es einen Übersetzer von SQL nach MapReduce, der im Prinzip das gleiche macht wie der MySQL-Server.
Kernel-C-Programmierung macht beim ersten Mal Spaß, danach will man Scriptsprachen benutzen, um sich nicht die Finger zu brechen.
Oder wie es so schön heißt: “He who has never hacked sendmail.cf has no soul; he who has hacked sendmail.cf more than once has no brain.”
2012/09/13 at 22:41
Es gibt überall solche und solche Toaster. Ich schaute mir ein konkretes System an und verglich die Anfragesprache mit SQL. Dabei kam heraus, daß SQL mächtiger ist als die konkrete Anfragesprache des einen NoSQL-Systems.
MapReduce ist nicht mit SQL zu vergleichen. Das sind zwei paar Schuhe. Deswegen ist es raus aus dem Vergleich.
Skriptsprache oder C? Habe ich C, dann habe ich auch die Skriptsprache…
2012/09/13 at 22:45
MapReduce ist in dem Beispiel mit den direkten APIs von InnoDB, NDB oder BDB zu vergleichen. Der MySQL-Server legt eine SQL-Ebene drüber. Und das tut Hive bei MapReduce.
Gib mir Deine SQL-Query und ich lasse die auf meinem Hadoop-Cluster laufen.
2012/09/13 at 23:16
MapReduce ist jenseits meines Vergleichs. Im Rahmen dieses Vergleichs/Blogposts betrachte ich MapReduce als keinen Maßstab. Anders als SQL lasse ich es nicht als Anfragesprache gelten, u.a. weil es nicht auf Mengen sondern auf einzelnen Zeilen/Tupeln/Dokumenten operiert.
Ich wüsste nicht, warum ich Dir irgendeine SQL-Query geben sollte. Es ist völlig egal ob MapReduce das umsetzen kann. MapReduce ist auf einem zu niedrigen Level, um Gegenstand dieses Vergleichs zu sein.
2012/09/14 at 08:58
Deine Basisoperationen einer Relationenalgebra unterscheiden sich leicht von denen, die man in der klassischen Literatur findet. Vergleiche dazu auch http://blog.koehntopp.de/archives/2844-Was-bedeutet-eigentlich-Relationale-Algebra.html bei mir. Normal findet man Selektion, Projektion, Join (LEFT JOIN und INNER JOIN unterscheiden sich im Grunde nur in der Definition des Gleichheitsoperators bei NULL-Werten), Rename und Aggregation.
Im Ergebnis macht das aber bei Deiner Auswertung mit Hinblick auf NoSQL-Sprachen keinen Unterschied, d.h. die Folgerungen aus Deinem Artikel bleiben unberührt.
Die Argumente pro NoSQL sind in der Regel
– Schemafreiheit. Das ist Unsinn, so etwas gibt es nicht (http://blog.koehntopp.de/archives/3056-Schemaless.html, http://blog.koehntopp.de/archives/3057-Zusammenfassung-Schemaless.html, jeweils auch die Kommentare).
Die korrekte Behandlung des Problems wäre a) ein schnelles ALTER TABLE (man hängt sich also an einem Implementationsproblem einer konkreten, populären Implementierung auf) und b) die Einführung von NFNF (Non-First Normal Form) Datentypen wie Arrays oder Hashes als Spaltentyp (Der Hadoop Hive-Compiler bietet so etwas zum Beispiel an, aber im Kontext einer SQLish Abfragesprache).
– horizontale Skalierbarkeit. Also die Möglichkeit des Sharding von Tabellen.
Der Preis, den man dafür bezahlt ist hoch: Man begibt sich in das Gebiet der verteilten Datenbanken, und behielte man die Relationenalgebra als Abfragesprache muß man nun verteiltes Locking/Transaktionen, verteilten Join und verteilte Aggregation als Probleme lösen.
Verteilte Aggregation ist im Grunde Map-Reduce mit einem groß geschriebenen Reduce.
Verteiltes Locking und verteilte Transaktionen sind ein gelöstes Problem, aber die Lösungen sind teuer: 2PC und seine Nachfolger (3PC, …, Paxos) sind Synchronisationsprimitive, die sehr sensitiv auf Netzwerklatenz reagieren. Minimiert man den Einsatz dieser Synchronisationsprimitive, indem man ACID als Default aufgibt, und beschränkt man sich auf spezielle, idempotente (replayable, wiederabspielbare) Statements, wäre im Sinne eines mathematischen Beweises zu zeigen, daß dies immer noch genau so ausdrucksstark ist wie voll ACID SQL. Hat man das getan, gelangt man zu einem lebensfähigen BASE-Modell. Das heißt, man landet dann auf einem Queueing-System mit asynchronen Queues, wie es etwa MySQL Replikation ja darstellt.
Und verteilter Join ist ein hartes Problem, für das es aber Lösungen gibt (Condition Pushdown, Batched Key Access, etc). MySQL Cluster ist bei aller Kritik eine erstaunlich gute Implementierung eines solchen Problems.
NoSQL-Systeme haben in der Regel keine Antworten auf diese Probleme. Sie offerieren Sharding zu einem hohen Preis (keine Transaktionen, kein Join), und wälzen die Lösung dieser in der Informatik durchaus als “schwierig” bewerteten Probleme auf den Anwendungsprogrammierer ab. Das heißt, nicht nur implementiert der Kern der NoSQL-Engine diese Dinge nicht, sondern es existieren in der Nutzercommunity solcher Systeme auch keine Client Side Libraries, die sich generisch oder exemplarisch mit der Behandlung dieser Probleme beschäftigen: Jedem Anwendungsentwickler ist es also wieder selbst überlassen, eine Lösung für diese Dinge zu finden – in der Regel falsch, ineffizient oder halt gar nicht.
Außerdem braucht jedes verteile, shardende NoSQL-System bei aller horizontalen Skalierbarkeit am Ende halt doch eine lokale Storage Engine, die Persistenz macht. Schaut man sich Beispiele an, bekommt man das Grauen: Jedes einzelne dieser Systeme ist lokal schlechter als MyISAM, und das ist nicht leicht.
Hier MongoDB als Beispiel zu nehmen und zu kritisieren ist pointless, man schlägt keine wehrlosen, blutenden Gegner, die am Boden liegen. CouchDB ist da schon besser, stirbt aber an der dümmsten möglichen Implementierung von Vacuum und, wenn jemals von der Platte gelesen wird, auch in Disk Seeks. Redis – single threaded und full dump+delta machend – hat mindestens dieselben Probleme wie NDB-Cluster: Ein full dump von 192GB Speicher auf eine SSD dauert nun mal 15 Minuten und das ist als Abstand für Checkpoints in vielen Fällen zu groß. Und single threaded auf einer 16 Core-Box ist auch ein wenig lachhaft – startet man aber 16 Redis-Instanzen auf so einer Karre, gibt es nix ab Werk, das die Full Dumps dieser Instanzen orchestrieren würde (und schreibt man sich das, stellt man fest, daß es sich tot seekt, selbst auf einer SSD und selbst dann, wenn nur eine Instanz zur Zeit dumped).
In der Zusammenfassung kann man sagen, daß es schon sinnvoll wäre, 40 Jahre Forschung, MVCC und das Buch von Gray nicht ganz zu ignorieren, wenn man mitspielen möchte.
2012/09/14 at 09:40
Schade, daß man Kommentare hier nicht “liken” kann 🙂
Danke für diesen überaus lesenwerten Kommentar, Kris.
2012/09/14 at 09:45
Vielen Dank für den langen Kommentar, der mich hier unmittelbar nach dem Aufwachen im Bett erwarten. Was mich erwartet ist ein Todesstoß dessen Breite ich mir noch aufsparen wollte…
NF^2, PNF ist bekannt und auf dem Radar. Weil ich bislang keine schöne Anfragesprache fand, ging ich absichtlich nicht auf geschachtelte Typen ein. Hier sitze ich seit drei Tagen gedanklich fest. Vielleicht könnte man sogar von funktionalen Shardunits sprechen, wenn man das ganze flott hinkriegt und mit nesten/entnesten verwurstelt.
Zu den verteilten Systemen schnapp Dir mal “Replication: Theory and Practice”, Pedone. Es gibt ja einige “…” zwischen 3PC und Paxos, die interessant sind.
Das von mir ansatzweise verprügelte Kind ist das Lauteste im Vorhof – genau das sollte es sein. CouchDB und Redis saßen schon wieder im Unterricht.
Gute Bücher… der Gray ist schwer zu kriegen im Original.
2012/09/14 at 12:36
Bloß teuer. Elsevier halt.
http://www.amazon.de/Transaction-Processing-Concepts-Techniques-Management/dp/1558601902
Pingback: Die wunderbare Welt von Isotopp