Ulf Wendel

Eine reiche NoSQL-Anfragesprache?

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 nicht AS, 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 = | != | < | <= | > | &gt=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 = | != | < | <= | > | &gt=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!

@Ulf_Wendel Follow me on Twitter