JSON in SQLite abfragen – json_extract und der ->>-Operator
Ein JSON-Feld in der Datenbank ist bequem, bis man danach filtern will. SQLite kann direkt hineinschauen – und mit einem Index auf dem richtigen Ausdruck ist das sogar schnell.
Nicht alles verdient eine eigene Spalte. Einstellungen, ein Zustandsschnappschuss, die Antwort einer fremden API — so etwas landet bei mir oft als JSON-Text in einer Spalte. Bequem beim Schreiben, unbequem beim Suchen. Zumindest, bis man weiß, dass SQLite JSON selbst versteht.
CREATE TABLE spielstaende (
id INTEGER PRIMARY KEY,
spieler TEXT NOT NULL,
daten TEXT NOT NULL -- JSON
);Einzelne Werte herausholen
SELECT
spieler,
daten ->> '$.level' AS level,
daten ->> '$.name' AS figur,
daten ->> '$.inventar[0]' AS erstes_teil
FROM spielstaende;Der Pfad beginnt mit $ für die Wurzel, danach Punkt für Objektfelder und eckige Klammern für Array-Positionen. Verschachtelt geht genauso: '$.einstellungen.ton.lautstaerke'.
->> oder json_extract?
->> ist die Kurzform für json_extract und liefert einen einfachen Wert — Text, Zahl, oder NULL. Das ist fast immer das, was man will.
Der einfache Pfeil -> dagegen liefert wieder JSON. daten -> '$.name' ergibt "Kel" mit Anführungszeichen. Praktisch, wenn man ein Teilobjekt weiterreichen will; verwirrend, wenn man eigentlich nur den Namen wollte.
Merkhilfe: zwei Pfeile = zwei Anführungszeichen weniger.
Danach filtern und sortieren
SELECT spieler, daten ->> '$.level' AS level
FROM spielstaende
WHERE CAST(daten ->> '$.level' AS INTEGER) >= 10
ORDER BY level DESC;Das CAST ist wichtig: Was aus dem JSON kommt, ist erstmal Text — und '9' > '10' stimmt bei Text nicht.
Ein Array auffalten
Richtig interessant wird es mit json_each, das ein Array in Zeilen verwandelt:
SELECT s.spieler, j.value AS gegenstand
FROM spielstaende s, json_each(s.daten, '$.inventar') j
WHERE j.value = 'schlüssel';Damit lassen sich JSON-Listen ganz normal joinen, gruppieren und zählen — als wären sie eine eigene Tabelle.
Und die Geschwindigkeit?
Hier liegt der Haken: Ohne Index muss SQLite für jede Zeile das JSON parsen. Bei zehntausend Zeilen ist das spürbar. Die Lösung ist ein Index auf genau dem Ausdruck, nach dem du filterst:
CREATE INDEX idx_level ON spielstaende (CAST(daten ->> '$.level' AS INTEGER));Danach ist die obige Abfrage schnell — aber nur, wenn der Ausdruck in der Abfrage exakt so aussieht wie im Index. Ob es geklappt hat, sagt dir EXPLAIN QUERY PLAN.
Wann JSON, wann eine Spalte?
Meine Regel: Wonach ich regelmäßig filtere oder sortiere, bekommt eine eigene Spalte. Alles andere darf im JSON bleiben.
Ein JSON-Feld ist praktisch, weil es ohne Schema-Änderung mitwächst — und genau das ist auch seine Schwäche: Kein Fremdschlüssel, keine NOT NULL-Prüfung, kein Tippfehlerschutz. Wenn du merkst, dass du für ein Feld einen Index brauchst, ist das der Moment, es herauszuziehen.
Du hast eine Abfrage auf JSON-Daten, die spürbar langsam ist? Meistens fehlt genau dieser Ausdrucksindex. Schreib mir.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
CREATE VIEW – die Abfrage, die du nur einmal schreibst
Wenn dieselbe 15-zeilige Abfrage an vier Stellen im Code steht, ändert man sie irgendwann an dreien. Eine View gibt ihr einen Namen – danach steht sie nur noch an einer Stelle.
Volltextsuche in SQLite mit FTS5 – schneller und besser als LIKE '%wort%
LIKE '%suchwort%' kann keinen Index nutzen und liest bei jeder Suche die ganze Tabelle. FTS5 ist in SQLite eingebaut, findet auch Wortanfänge und sortiert die Treffer nach Relevanz – ohne Elasticsearch daneben.
WITH RECURSIVE – Kategoriebäume und Kommentar-Threads in einer einzigen Abfrage
Kategorien mit Unterkategorien, Kommentare mit Antworten, Ordner in Ordnern: Die meisten holen sich so etwas mit einer Schleife und einer Abfrage pro Ebene. Eine rekursive CTE holt den ganzen Baum in einem Rutsch – inklusive Tiefe und Pfad.