Abfragesprachen für Graphendatenbanken

Seite 3: Abfragesprachen

Inhaltsverzeichnis

Es gibt unterschiedliche Möglichkeiten, Daten in Graphendatenbanken abzufragen und zu manipulieren. Die meisten Datenbanksysteme haben APIs, meistens in der Sprache, in der die Datenbank implementiert ist. Das lässt hochperformante Interaktion zu, vor allem wenn man die Datenbank in den Anwendungsprozess einbettet, da dann der Netzwerk-Overhead keine Rolle spielt. Auch lassen sich auf solchen APIs gut weiterführende aufbauen. Diese sind meist als domänenspezifische Sprache (DSL) realisiert, um sie leserlicher und verständlicher zu gestalten. Hier zwei Beispiele:

Callback-basiert: Hierbei wird die Datenbankstruktur traversiert, und während der Operation geschehen die entsprechenden Callbacks (Beispiel aus der Neo4J-Dokumentation):

for ( Path path : Traversal.description()
.depthFirst()
.relationships( Rels.KNOWS )
.relationships( Rels.LIKES, Direction.INCOMING )
.evaluator( Evaluators.toDepth( 5 ) )
.traverse( node ) ) {
output += path + "\n";
}

Datenfluss-basiert: Ein Beispiel für eine imperative Abfragesprache für Graphendatenbanken
ist die DSL Gremlin, die der Tinkerpop-Stack für ein Datenfluss-Framework nutzt, das wiederum auf miteinander verbundenen Operationseinheiten (Pipes) aufsetzt. Es gibt Implementierungen von Gremlin in Groovy und Java, wobei erstere lesbarer ist.

g.v(39).out('hasRoleInGroup').as('hyperedge').out('hasGroup')
.filter{it.name=='Group2'}.back('hyperedge').out('hasRole').name

Die programmatischen APIs sind imperativ. Das heißt, sie steuern direkt, wie Operationen auf der Datenbank erfolgen. Die darauf aufsetzenden DSLs können dagegen deklarativ oder imperativ sein. Im deklarativen Fall werden erst einmal Informationen gesammelt, die dann später genutzt werden, um die Ausführung zu steuern.

Um den Zugang zur Datenbank zu erleichtern und den an SQL gewöhnten Nutzer zu unterstützen, stellen die meisten Graphendatenbanken auch deklarative Sprachen bereit. Im Folgenden sollen die besprochenen Konzepte an Beispielen aus der Abfragesprache Cypher der Graphendatenbank Neo4j dargestellt werden. Diese entstand ausschließlich als Werkzeug für den Umgang mit Daten eines Graphenmodells. Dabei erfolgten Anleihen und Übernahmen erfolgreicher Ansätze bei anderen Abfragesprachen. Obwohl Neo4j selbst in Java implementiert ist, wurde Cypher in Scala entwickelt. Hauptgründe für diese Entscheidung waren die vorhandenen Parser-Bibliotheken, der funktionale Teil der Programmiersprache für die Implementierung der Abfrage-Engine sowie die gute Integration mit Java und damit Neo4j.

Doch welche Aspekte sollte nun eine deklarative Abfragesprache abdecken, damit sie für die Arbeit mit Graphen geeignet ist? Ein wichtiger Aspekt ist die leichte Lesbarkeit. Zum einen sollte eine deklarative Abfrage für jeden Betrachter mit Kenntnis der Domäne verständlich sein (das sind nicht nur Entwickler). Da Abfragen viel häufiger gelesen als geschrieben werden, ist es des Weiteren sinnvoll, ein Hauptaugenmerk auf Lesbarkeit und Verständlichkeit zu setzen.

Die Lesbarkeit geht einher mit Wartbarkeit. Hier stellt sich die Frage: Wie leicht ist es erkennbar, welche Anpassungen an einer Abfrage vorzunehmen sind, um Änderungen am Anwendungsfall widerzuspiegeln? Die direkte Unterstützung von Konzepten der Datenbank ist ein weiterer wichtiger Punkt. Für Graphendatenbanken sind das:

  • Verbindungen und Pfade
  • transitive Verbindungen auch mit variablen Längen
  • Mustersuche im Graph
  • Graphenalgorithmen
  • Handhabung optionaler Informationen, die wegen Schemafreiheit nicht garantiert sind
  • Erzeugung von Graphenelementen auch unter Berücksichtigung halbexistenter Strukturen

Und, ganz allgemein betrachtet, sollten generische, anwendungsrelevante Operationen wie Filterung, Aggregation, Projektion, Segmentierung und Sortierung unterstützt werden.

Während kleinere Graphen eine natürliche Modellierungsform darstellen (man denke nur an Diagramme auf Tafeln, Whiteboards, Mindmaps usw.), sind komplexere und größere Graphen nicht mehr so leicht verständlich. Daher ist es notwendig, konzeptionell über die Fragen nachzudenken, auf die man von der Graphendatenbank Antworten erwartet. Statt des individuellen Folgens feingranularer Knoten und Verbindungen, wie es in imperativen Ansätzen geschieht, ist es sinnvoll, Muster (Patterns), an denen man interessiert ist, im Graphen zu suchen. Anhand dieser Muster lassen sich Informationen sammeln, aggregieren, filtern und zu Ergebnissen projizieren.

Trotz ihres Fokus auf realen Anwendungen, den Graphendatenbanken mitbringen, sind es Graphenalgorithmen, durch die sich manche Aufgabe eleganter lösen lässt. Beispiele dafür sind das Identifizieren effizienter Routen für den Pakettransport, das Erstellen von Empfehlungssystemen mit kollaborativen Filtern oder das Auflösen von Berechtigungen mit der Pfadanalyse.

Für eine möglichst effiziente Handhabung großer Datenmengen ist es sinnvoll, diese dem Abfragenden erst dann zur Verfügung zu stellen, wenn er sie benötigt – das gilt sowohl beim Ausführen der Abfragen als auch beim Bereitstellen der Ergebnisse. Daher führen eine verzögerte Ausführung ("lazy evaluation") und das Pull-Prinzip bei der Ergebnisbereitstellung zu einer effizienten Speichernutzung und verhindern, dass zu viele Daten zu verarbeiten sind, an denen der Nutzer gar nicht mehr interessiert ist.