Fundierte Artikel über Technologien, die das Kommende formen.

Eingebettete Graphdatenbanken jenseits von SQLite

Warum Graphdatenbanken eingebettet werden, was Rust mitbringt und wann ein Graphmodell relationalen Datenbanken für deinen Anwendungsfall wirklich überlegen ist.

Winziger Würfel aus leuchtenden, verbundenen Knoten, eingebettet in eine verrostete mechanische Hülle

SQLite ist überall. Im Handy, im Browser, im Smart-TV, wahrscheinlich auch im Auto. Es hat ein grundlegendes Problem gelöst, nämlich Anwendungen eine vollwertige SQL-Datenbank zu geben, ohne einen separaten Server laufen zu lassen, und das so gut, dass „eingebettete Datenbank“ und „SQLite“ praktisch zu Synonymen wurden. Aber das relationale Modell von SQLite passt nicht immer. Wenn deine Daten im Kern aus Beziehungen bestehen, etwa soziale Verbindungen, Abhängigkeitsgraphen, Wissensnetze oder Routingprobleme, dann erzeugt das Hineinpressen in Tabellen mit Fremdschlüsseln Abfragen, die entweder furchtbar komplex oder quälend langsam sind.

Eine neue Welle eingebetteter Graphdatenbanken will für Graphdaten erreichen, was SQLite für relationale Daten geleistet hat: eine schnelle, abhängigkeitsfreie In-Process-Datenbank, die die passende Abfragesprache für vernetzte Daten spricht. Mehrere davon sind in Rust geschrieben, und das passt erstaunlich gut zum Problem. Schauen wir uns an, warum Graphdatenbanken eingebettet werden, was das Rust-Ökosystem mitbringt und wann du tatsächlich eine in Betracht ziehen solltest.

Der blinde Fleck des relationalen Modells

Relationale Datenbanken meistern die meisten Datenmuster gut. Eins-zu-viele? Fremdschlüssel. Viele-zu-viele? Zwischentabelle. Einfache Lookups, Aggregationen, Filter, dafür wurde SQL gebaut. Problematisch wird es, sobald deine Abfragen sich um Pfade, Tiefe und Konnektivität drehen.

Nehmen wir einen Dependency-Resolver. Du hast Pakete, die von anderen Paketen abhängen, jeweils mit Versionsvorgaben. Du willst beantworten: „Wenn ich Paket X installiere, wie sieht der vollständige transitive Abhängigkeitsbaum aus? Gibt es zirkuläre Abhängigkeiten? Gibt es widersprüchliche Versionsanforderungen auf irgendeiner Tiefe?“ In SQL brauchst du dafür rekursive CTEs (Common Table Expressions). Die sind ausführlich, schwer zu optimieren und werden mit zunehmender Tiefe des Graphen exponentiell langsamer.

-- Finding all transitive dependencies in SQL
-- This works, but gets ugly fast
WITH RECURSIVE deps AS (
-- Base case: direct dependencies
SELECT dependency_id, 1 as depth
FROM package_dependencies
WHERE package_id = 'my-package'
UNION ALL
-- Recursive case: dependencies of dependencies
SELECT pd.dependency_id, d.depth + 1
FROM package_dependencies pd
JOIN deps d ON pd.package_id = d.dependency_id
WHERE d.depth < 50  -- safety limit to prevent infinite loops
)
SELECT DISTINCT dependency_id, MIN(depth) as min_depth
FROM deps
GROUP BY dependency_id
ORDER BY min_depth;
-- Now try detecting circular dependencies.
-- Or finding the shortest path between two packages.
-- Or querying all packages within 3 hops that match a version constraint.
-- Each query gets progressively more painful.

In einer Graphdatenbank ist das das native Abfragemuster. Du kämpfst nicht gegen das Datenmodell, sondern arbeitest mit ihm. Kanten durchlaufen, Pfade verfolgen, Zyklen erkennen: Das sind erstklassige Operationen und keine nachträglich angeflanschte Rekursion.

Warum Eingebettetsein den Unterschied macht

Neo4j ist seit über einem Jahrzehnt die dominierende Graphdatenbank, und sie ist wirklich hervorragend. Aber sie ist ein Server. Du betreibst sie als eigenen Prozess, verbindest dich über ein Netzwerkprotokoll, verwaltest ihren JVM-Speicher und kümmerst dich um den Betriebsaufwand. Für eine Produktionsanwendung mit eigenem Ops-Team ist das in Ordnung. Für ein CLI-Tool, eine Desktop-App, ein Build-System oder ein eingebettetes Gerät ist es absurd.

Die SQLite-Erkenntnis gilt auch hier: Viele Anwendungsfälle brauchen Graphabfragen, aber keinen Serveraufwand. Ein Code-Analyse-Tool, das einen Call-Graphen aufbaut. Eine Game-Engine, die Beziehungen zwischen Entitäten speichert. Eine lokal-first Notiz-App mit bidirektionalen Links. Ein Analysator für Netzwerktopologien. Alle wollen Graphabfragen, keiner will seinen Nutzern sagen, sie sollen einen Datenbankserver installieren und konfigurieren.

Eingebettete Graphdatenbanken laufen im selben Prozess wie deine Anwendung, speichern Daten in lokalen Dateien und stellen eine Bibliotheks-API statt eines Netzwerkprotokolls bereit. Deine Anwendung linkt sie so, wie sie SQLite linken würde. Kein Server, keine Ports, keine Authentifizierung, keine Deployment-Komplexität.

Was Rust für Graphdatenbanken bringt

Rust ist für eine überproportional große Zahl neuer Datenbankprojekte zur Sprache der Wahl geworden, und die Gründe gehen über das übliche Argument „Speichersicherheit ohne Garbage Collector“ hinaus.

  • Vorhersagbare Latenz. Graphtraversierungen sind latenzempfindlich. Jeder Hop ist ein Speicherzugriff, und tiefe Traversierungen machen davon Millionen. Eine GC-Pause mitten in einer 10-Hop-Traversierung ruiniert deine Tail-Latenz. Das Ownership-Modell von Rust bietet deterministische Speicherverwaltung ohne GC-Pausen, und genau das ist entscheidend für konstante Abfrageperformance.
  • Sichere Nebenläufigkeit. Graphdatenbanken profitieren enorm von paralleler Traversierung. Mehrere Pfade gleichzeitig zu erkunden kann aus einer 100-ms-Abfrage eine mit 10 ms machen. Das Typsystem von Rust verhindert Data Races zur Compilezeit. Du kannst also aggressiv parallelisieren, ohne deine Graphdaten zu korrumpieren.
  • Kleines Binary, keine Laufzeitumgebung. Bei einer eingebetteten Datenbank zählt die Größe des Deployments. Eine Rust-Graphdatenbank kompiliert zu einer einzigen nativen Bibliothek ohne Laufzeitabhängigkeiten. Vergleich das mit einer JVM-Lösung, die eine 200-MB-Laufzeit braucht, oder einer Go-Lösung, die einen Garbage Collector mitliefert, den du nie bestellt hast.
  • C-FFI. Dass Rust eine C-kompatible API bereitstellen kann, heißt: Die Datenbank lässt sich aus praktisch jeder Sprache nutzen. Schreib sie in Rust und ruf sie aus Python, JavaScript, Go, Swift oder allem anderen auf, das C spricht.

Das Property-Graph-Modell

Die meisten eingebetteten Graphdatenbanken nutzen das Property-Graph-Modell. Es lohnt sich, es zu verstehen, falls du noch nicht mit Graphdatenbanken gearbeitet hast. Das Modell kennt drei Grundelemente:

  • Knoten – Entitäten mit einem Label und Schlüssel-Wert-Eigenschaften. Man kann sie sich wie Zeilen in einer Tabelle vorstellen, nur ohne festes Schema.
  • Kanten – gerichtete Verbindungen zwischen Knoten, ebenfalls mit Label und Eigenschaften. Das Label beschreibt den Beziehungstyp (DEPENDS_ON, AUTHORED_BY, LINKS_TO).
  • Traversierungen – Abfragen, die Kanten von Knoten zu Knoten folgen, optional nach Eigenschaften filtern, Ergebnisse aggregieren oder Pfade finden.
// Conceptual API for an embeddable graph database
let db = GraphDB::open("my_graph.db")?;
// Create nodes
let alice = db.create_node("Person", props! {
"name" => "Alice",
"role" => "engineer"
})?;
let project = db.create_node("Project", props! {
"name" => "Auth Service",
"language" => "Rust"
})?;
// Create edges
db.create_edge(alice, project, "WORKS_ON", props! {
"since" => "2025-01"
})?;
// Traverse: find all projects worked on by Alice's teammates
let results = db.traverse()
.from(alice)
.follow("WORKS_ON")        // Alice -> projects
.reverse("WORKS_ON")       // projects <- other people
.follow("WORKS_ON")        // other people -> their projects
.filter(|node| node.label() == "Project")
.collect()?;

Das Property-Graph-Modell ist flexibler als relationale Schemata, wenn sich Daten schnell verändern. Du musst das Schema nicht vorab definieren und keine Migrationen fahren, wenn du einen neuen Beziehungstyp hinzufügst. Erstell einfach Kanten mit neuen Labels. Diese schemalose Flexibilität ist ein zweischneidiges Schwert, denn du verlierst die Sicherheitsgarantien eines klar definierten Schemas. Für Anwendungen, deren Graphstruktur sich entwickelt (Wissensdatenbanken, soziale Netze, Abhängigkeitsverfolgung), ist es aber ein pragmatischer Kompromiss.

Wann du wirklich eine Graphdatenbank einsetzen solltest

Graphdatenbanken werden oft in Situationen empfohlen, in denen sie überflüssig sind, und in Situationen übersehen, in denen sie wirklich helfen würden. Hier meine ehrliche Einschätzung, wo sie glänzen und wo nicht.

Starker Einsatzbereich: Dependency-Auflösung, Zugriffskontrolle (wer kann worauf zugreifen, über welche Gruppenmitgliedschaften), Betrugserkennung (Verbindungen zwischen Entitäten finden), Empfehlungssysteme („Nutzer, die X mochten, mochten auch Y“), Analyse von Netzwerktopologien, Wissensgraphen und Code-Analyse-Tools. Der gemeinsame Nenner: Deine Abfragen drücken natürlicherweise Pfade und Konnektivität aus.

Schwacher Einsatzbereich: einfache CRUD-Anwendungen, Zeitreihendaten, Analytik- und Aggregations-Workloads und alles, bei dem deine Abfragen hauptsächlich lauten „hol Datensätze, die Bedingung X erfüllen“. Wenn du SELECT * FROM users WHERE country = 'US' ORDER BY created_at schreibst, ist eine relationale Datenbank das richtige Werkzeug. Nimm keine Graphdatenbank, weil sie spannend klingt, sondern weil deine Abfragen wirklich graphförmig sind.

Mein Litmus-Test: Zeichne dein Datenmodell auf ein Whiteboard. Besteht es hauptsächlich aus Kästchen in Reihen (Entitäten mit Attributen), nimm eine relationale Datenbank. Sind es Kästchen, die durch Pfeile verbunden sind, und sind die Pfeile genauso wichtig wie die Kästchen, dann zieh eine Graphdatenbank in Betracht.

Speicher-Engines und Abwägungen

Unter der Haube stehen eingebettete Graphdatenbanken vor interessanten Entscheidungen bei der Storage-Engine. Die häufigsten Ansätze:

  • Adjazenzlisten-Speicherung. Jeder Knoten speichert eine Liste seiner ausgehenden Kanten. Schnell für lokale Traversierungen (die Nachbarn eines Knotens finden), aber langsam für globale Abfragen (alle Kanten mit Label X finden). Die meisten eingebetteten Graphdatenbanken setzen darauf, weil lokale Traversierungen die häufigste Operation sind.
  • Kantenlisten-Speicherung. Kanten liegen in einer separaten, sortierten Struktur, indiziert nach Quelle, Ziel oder Label. Besser für globale Abfragen und Bulk-Operationen, fügt aber lokalen Traversierungen eine Indirektion hinzu.
  • Hybride Ansätze. Manche Datenbanken nutzen Adjazenzlisten für Traversierungen und pflegen zusätzlich Sekundärindizes auf Kantenlabels oder Knoteneigenschaften für gefilterte Abfragen. Das ist am flexibelsten, benötigt aber mehr Speicher und macht Schreibvorgänge teurer.

Viele Rust-basierte Graphdatenbanken setzen auf bestehenden eingebetteten Key-Value-Stores wie RocksDB oder sled auf. Das ist eine pragmatische Wahl: Du bekommst bewährte Persistenz, Crash-Recovery und Compaction gratis. Die Graphschicht bildet Knoten und Kanten auf Key-Value-Operationen ab. Der Nachteil: Die Performance-Obergrenze hängt von den Eigenschaften des Key-Value-Stores ab, und graphspezifische Optimierungen, etwa Kanten eines Knotens zusammenhängend auf der Festplatte abzulegen, damit Traversierungen cache-freundlich laufen, lassen sich schwerer umsetzen.

Abfragesprachen: Die ungeklärte Frage

SQL ist die universelle Sprache für relationale Datenbanken. Bei Graphdatenbanken gibt es keinen vergleichbaren Konsens. Cypher (von Neo4j), Gremlin (aus Apache TinkerPop), SPARQL (für RDF-Graphen) und der aufkommende GQL-Standard konkurrieren alle um Aufmerksamkeit.

Die meisten eingebetteten Graphdatenbanken umgehen das, indem sie statt einer Abfragesprache eine Builder-Pattern-API in der Host-Sprache anbieten. Du baust Traversierungen mit Methodenketten, was in typisierten Sprachen ergonomisch ist und die Komplexität vermeidet, eine Abfragesprache zu parsen und zu optimieren. Der Nachteil: Deine Abfragen sind nicht zwischen Datenbanken portierbar. Wer von einer eingebetteten Graphdatenbank zu einer anderen wechselt, muss seinen Abfragecode neu schreiben.

GQL (Graph Query Language) ist der ISO-Standard, der Graphabfragen vereinheitlichen soll. Er übernimmt viel von Cypher und gewinnt allmählich an Verbreitung. Wenn du heute eine eingebettete Graphdatenbank auswählst, lohnt sich der Blick darauf, ob GQL-Unterstützung auf der Roadmap steht. Standardisierte Abfragesprachen setzen sich langfristig meist durch, auch wenn proprietäre zunächst ausgefeilter sind.

Praktische Überlegungen

Wenn du eine eingebettete Graphdatenbank für dein Projekt evaluierst, kommt es in der Praxis auf Folgendes an:

  1. Miss mit deinem eigenen Workload. Benchmarks zu Graphdatenbanken sind notorisch irreführend. Eine Datenbank, die bei flachen, breiten Traversierungen (Freund-von-Freund im sozialen Netz) glänzt, kann bei tiefen, schmalen (Auflösung von Abhängigkeitsketten) ins Schwitzen kommen. Prototypisiere mit deinen tatsächlichen Abfragemustern.
  2. Prüf die Crash-Sicherheit. Eingebettete Datenbanken stehen und fallen mit ihren Durability-Garantien. Nutzt die Datenbank Write-Ahead-Logging? Ist sie crash-sicher? Kann sie sich nach einem Stromausfall ohne Datenverlust erholen? „Beschädigung bei unerwartetem Herunterfahren“ ist für den Produktiveinsatz keine akzeptable Antwort.
  3. Schau dir das Speichermodell an. Manche eingebetteten Graphdatenbanken mappen den gesamten Graphen per mmap in den Speicher. Das funktioniert super, bis der Graph den verfügbaren RAM übersteigt. Andere setzen auf einen Disk-first-Ansatz mit Caching. Finde heraus, welches Modell deine gewählte Datenbank nutzt und ob dein Graph hineinpasst.
  4. Achte auf die Qualität der Bindings. Wenn die Datenbank in Rust geschrieben ist, du sie aber aus Python nutzt, sind die Python-Bindings wichtiger als die Rust-Interna. Prüfe, ob die Bindings gepflegt, dokumentiert und performant sind oder nur ein Nebengedanke.
  5. Denk an Migrationen. Schemalos heißt nicht änderungsfrei. Wenn sich dein Graphmodell weiterentwickelt, wie gehst du mit bestehenden Daten um? Manche Datenbanken unterstützen Migrationsskripte oder versionierte Schemata. Andere überlassen das komplett dir.

Der Markt eingebetteter Graphdatenbanken ist im Vergleich zur relationalen Welt noch jung. SQLite wird seit über zwei Jahrzehnten verfeinert, die meisten eingebetteten Graphdatenbanken sind jünger als fünf Jahre. Das bedeutet rauere Kanten, weniger Community-Ressourcen und mehr Risiko. Der Kernnutzen, nämlich Graphabfragen ohne Serveraufwand, ist aber solide. Für den passenden Anwendungsfall kann eine eingebettete Graphdatenbank hunderte Zeilen rekursives SQL durch ein paar Zeilen Traversierungscode ersetzen, dabei sogar schneller laufen und dein Datenmodell tatsächlich zu dem Problem passend machen, das du löst.