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 .com enden”
  • „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

SQL orders mit verschachteltem JSON
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-AusdruckBedeutung
$Das Wurzelobjekt
$.fieldFeld field im Wurzelobjekt
$.a.b.cTiefer Pfad
$.array[0]Erstes Element des Arrays
$.array[*]Alle Elemente des Arrays
$.array[0 to 2]Elemente 0–2
$.**.emailRekursiv: jedes email-Feld irgendwo
$ ? (@.x > 5)Filter (siehe nächster Abschnitt)
SQL Erste Pfad-Beispiele
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:

SQL Items mit Tag 'urgent'
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:

  1. $.items[*] — alle Items
  2. ? (@.tags[*] == "urgent") — gefiltert auf jene, deren tags-Array das Element "urgent" enthält

@ ist hier das jeweilige Item — sein tags-Array wird mit [*] iteriert, und mindestens ein Element muss "urgent" sein.

SQL Mehrere Filter
-- 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

FunktionLiefertWann nutzen
jsonb_path_query(data, path)Set von jsonb-Wertenwenn mehrere Treffer möglich sind
jsonb_path_query_first(data, path)erster Trefferwenn nur einer interessiert
jsonb_path_query_array(data, path)Treffer als jsonb-Arraywenn das Resultat in einer Spalte stehen soll
jsonb_path_exists(data, path)booleannur Existenz-Check, kein Wert nötig
SQL Pro Funktion ein Beispiel
-- 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:

SQL
-- @? 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:

SQL Numerische Funktionen
-- 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:

SQL
-- 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-CaseEmpfehlung
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üsselsdata ? 'key' — knapp, GIN-fähig
Filter über Array-Elemente mit BedingungPfad-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

/ Weiter

Zurück zu JSON & JSONB

Zur Übersicht