Wenn die einfachen Operatoren nicht mehr reichen — etwa „alle Bestellungen, deren total größer als 100 ist und mindestens ein Item-Tag urgent enthält” — kommt SQL/JSON Path ins Spiel. Eine eigene Mini-Sprache für komplexe JSONB-Selektionen, in PostgreSQL seit Version 12 verfügbar.
Wozu Pfad-Queries?
Mit den normalen Operatoren ->, @> und ? kommt man weit, aber nicht überall hin. Beispiele, bei denen es kompliziert wird:
- „Finde alle Bestellungen, deren Total über 100 ist UND mindestens ein Item Tag ‘urgent’ hat”
- „Gib mir alle E-Mail-Adressen aus dem
contacts-Array, die auf.comenden” - „Filtere Produkte, deren erste Bewertung mindestens 4 Sterne hat”
Mit Pfad-Queries werden solche Selektionen lesbar — eine Art XPath für JSON.
Die Beispiel-Tabelle
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
data jsonb
);
INSERT INTO orders (data) VALUES
('{
"customer": {"id": 1, "name": "Alice", "email": "alice@example.com"},
"total": 159.95,
"items": [
{"sku": "A1", "qty": 2, "price": 50.00, "tags": ["urgent"]},
{"sku": "B2", "qty": 1, "price": 59.95, "tags": ["gift"]}
]
}'),
('{
"customer": {"id": 2, "name": "Bob", "email": "bob@example.com"},
"total": 19.95,
"items": [
{"sku": "C3", "qty": 1, "price": 19.95, "tags": []}
]
}');Pfad-Syntax-Grundlagen
Ein JSON-Path beginnt mit $ (= „die Wurzel”) und navigiert von dort:
| Pfad-Ausdruck | Bedeutung |
|---|---|
$ | Das Wurzelobjekt |
$.field | Feld field im Wurzelobjekt |
$.a.b.c | Tiefer Pfad |
$.array[0] | Erstes Element des Arrays |
$.array[*] | Alle Elemente des Arrays |
$.array[0 to 2] | Elemente 0–2 |
$.**.email | Rekursiv: jedes email-Feld irgendwo |
$ ? (@.x > 5) | Filter (siehe nächster Abschnitt) |
myapp=> SELECT jsonb_path_query(data, '$.customer.name') FROM orders;
jsonb_path_query
------------------
"Alice"
"Bob"
myapp=> SELECT jsonb_path_query(data, '$.items[*].sku') FROM orders;
jsonb_path_query
------------------
"A1"
"B2"
"C3"[*] iteriert über alle Array-Elemente und liefert pro Treffer eine eigene Ergebniszeile. Daraus kommen drei Zeilen — eine pro Item, über beide Bestellungen hinweg.
Filter mit ? und dem @-Selector
Hinter einem Pfad-Schritt kannst du mit ? (…) einen Filter setzen. @ referenziert den aktuellen Knoten:
myapp=> SELECT jsonb_path_query(data, '$.items[*] ? (@.tags[*] == "urgent")')
FROM orders;
jsonb_path_query
--------------------------------------------------
{"sku": "A1", "qty": 2, "price": 50.0, "tags": ["urgent"]}Lese das von links nach rechts:
$.items[*]— alle Items? (@.tags[*] == "urgent")— gefiltert auf jene, derentags-Array das Element"urgent"enthält
@ ist hier das jeweilige Item — sein tags-Array wird mit [*] iteriert, und mindestens ein Element muss "urgent" sein.
-- Items mit qty > 1 UND price >= 30
SELECT jsonb_path_query(data, '$.items[*] ? (@.qty > 1 && @.price >= 30)')
FROM orders;
-- Bestellungen mit total > 100
SELECT id FROM orders
WHERE jsonb_path_exists(data, '$ ? (@.total > 100)');&& und || sind die Booleschen Operatoren in der Path-Syntax (nicht AND/OR wie in SQL).
Vier Pfad-Funktionen
| Funktion | Liefert | Wann nutzen |
|---|---|---|
jsonb_path_query(data, path) | Set von jsonb-Werten | wenn mehrere Treffer möglich sind |
jsonb_path_query_first(data, path) | erster Treffer | wenn nur einer interessiert |
jsonb_path_query_array(data, path) | Treffer als jsonb-Array | wenn das Resultat in einer Spalte stehen soll |
jsonb_path_exists(data, path) | boolean | nur Existenz-Check, kein Wert nötig |
-- Alle E-Mails aus orders (eine Zeile pro Treffer):
SELECT id,
jsonb_path_query(data, '$.customer.email') AS email
FROM orders;
-- Erster Item-SKU pro Bestellung:
SELECT id,
jsonb_path_query_first(data, '$.items[*].sku') AS first_sku
FROM orders;
-- Alle Item-SKUs als Array pro Bestellung:
SELECT id,
jsonb_path_query_array(data, '$.items[*].sku') AS skus
FROM orders;
-- Welche Bestellungen haben ein Item mit Tag 'urgent'?
SELECT id FROM orders
WHERE jsonb_path_exists(data, '$.items[*] ? (@.tags[*] == "urgent")');Die Operatoren @? und @@
Es gibt auch Operator-Schreibweisen, die jsonb_path_exists und jsonb_path_match entsprechen:
-- @? entspricht jsonb_path_exists
SELECT id FROM orders
WHERE data @? '$.items[*] ? (@.tags[*] == "urgent")';
-- @@ entspricht jsonb_path_match (für Predicate-Pfade)
SELECT id FROM orders
WHERE data @@ '$.total > 100';@@ braucht einen Pfad, der auf einen booleschen Ausdruck endet (sog. Predicate-Path). Praktisch für Filter, kompakter als jsonb_path_match().
Beide Operatoren sind GIN-Index-fähig (mit jsonb_ops-Klasse) — wichtig für Performance auf großen Tabellen.
Mathematische Funktionen und String-Methoden
SQL/JSON Path bringt eine Reihe von Funktionen mit:
-- Summe aller item-prices pro Bestellung
SELECT id,
jsonb_path_query(data, '$.items[*].price.double()')
FROM orders;
-- Items mit price.floor() == 19 (also 19.00..19.99)
SELECT jsonb_path_query(data, '$.items[*] ? (@.price.floor() == 19)')
FROM orders;
-- Größtes price im Array
SELECT jsonb_path_query(data, '$.items[*].price.max()')
FROM orders;Verfügbare Methoden (Auswahl): .size(), .type(), .double(), .floor(), .ceiling(), .abs(), .min(), .max(), .keyvalue(), .string().
Pfad-Variablen
Pfade können externe Werte über vars aufnehmen — praktisch für Prepared Statements:
-- Statt fest 'urgent' im Pfad — Variable übergeben
SELECT jsonb_path_query(
data,
'$.items[*] ? (@.tags[*] == $tag)',
jsonb_build_object('tag', 'urgent')
)
FROM orders;Der dritte Parameter ist ein jsonb-Objekt mit Variable-Werten. Im Pfad referenziert mit $varname.
Wann Pfad-Queries, wann normale Operatoren?
| Use-Case | Empfehlung |
|---|---|
Einfacher Schlüssel-Zugriff (color) | data->>'color' — knapper, schneller |
Tieferer Pfad (a.b.c) | beides geht; Path-Syntax leicht lesbarer ab 4–5 Ebenen |
| Existenz eines Schlüssels | data ? 'key' — knapp, GIN-fähig |
| Filter über Array-Elemente mit Bedingung | Pfad-Query — andere Operatoren scheitern |
| Rekursive Suche („irgendwo im Baum”) | Pfad-Query mit ** |
| Containment | @> — knapp, GIN-fähig |
Pfad-Queries sind mächtig, aber etwas verbose und haben eigene Syntax-Regeln. Für simple Sachen → normale Operatoren. Für komplexe Selektionen, die sonst in Subqueries enden würden → Pfade.
Besonderheiten
JSON Path ist eine eigene Sprache.
Sie folgt dem SQL/JSON-Standard (in Postgres seit 12) und ähnelt XPath für XML. $ ist die Wurzel, @ der aktuelle Knoten in einem Filter, * ein Wildcard, ** ein rekursives Wildcard. Wer daraus mehr machen will: die offizielle Doku-Sektion durchlesen, ist überschaubar.
Strict vs. Lax Mode.
Default ist „lax” — fehlende Pfade werden stillschweigend ignoriert, statt Fehler zu werfen. Für strikte Validierung: 'strict $.field' als Pfad. Im strict-Mode wirft eine fehlende Stelle einen Fehler — gut für Datenvalidierung, schlecht für unvollständige JSON-Werte.
Filter brauchen ==, nicht =.
SQL nutzt = für Vergleich, JSON Path nutzt ==. @.qty == 1 ist richtig, @.qty = 1 ist Syntax-Fehler. Häufige Stolperfalle für SQL-Erfahrene.
String-Vergleich braucht doppelte Quotes.
@.color == "red" mit Doppel-Quotes für den String. Mit einfachen Quotes wäre’s eine Variable, kein String. Innerhalb des SQL-Statement-Strings musst du einfache Quotes für den ganzen Pfad nutzen — also '$.items[*] ? (@.tags[*] == "urgent")'.
Funktioniert mit GIN-Index — eingeschränkt.
@? und @@-Queries können einen GIN-Index nutzen, aber nur mit der Default-jsonb_ops-Klasse, nicht mit jsonb_path_ops. Wer beides braucht (Containment-Suche schnell, Path-Queries auch index-fähig): zwei Indexe oder den Default-GIN.
Pfad-Queries sind langsamer als simple Operatoren.
Der JSON-Path-Parser hat Overhead. Für simple data->>'key' = 'x'-Queries sind die normalen Operatoren schneller. Pfade lohnen sich erst, wenn sie wirklich Code sparen — komplexe Filter über verschachtelte Arrays mit Bedingungen.
Weiterführende Ressourcen
Externe Quellen
- JSON Path – PostgreSQL Documentation
- JSON Functions: jsonb_path_*
- Release Notes 12: SQL/JSON Path
- SQL/JSON Standard (ISO/IEC 9075-2)