jsonb glänzt durch seine reiche Operator-Sammlung. Mit ein paar wenigen Zeichen kannst du Werte aus tief verschachteltem JSON ziehen, prüfen ob ein Schlüssel existiert, zwei Objekte mergen oder einen Pfad löschen. Hier alle wichtigen Operatoren mit klaren Beispielen.

Die Beispiel-Tabelle

Für die folgenden Beispiele nutzen wir diese Tabelle:

SQL Beispiel-Daten
CREATE TABLE products (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sku   text NOT NULL,
    attrs jsonb
);

INSERT INTO products (sku, attrs) VALUES
    ('A1', '{
        "color": "red",
        "weight_kg": 1.2,
        "tags": ["new", "sale"],
        "dimensions": {"width": 10, "height": 20, "depth": 5},
        "in_stock": true
    }'),
    ('B2', '{
        "color": "blue",
        "weight_kg": 0.5,
        "tags": ["bestseller"],
        "dimensions": {"width": 5, "height": 8, "depth": 3},
        "in_stock": false
    }');

-> und ->>: Wert ziehen

-> liefert das Ergebnis als jsonb (kann weitergegeben werden), ->> als text (für direkte Verwendung in WHERE oder Application-Code).

SQL
myapp=> SELECT
          attrs->'color'   AS color_jsonb,
          attrs->>'color'  AS color_text
        FROM products WHERE sku = 'A1';
 color_jsonb | color_text
-------------+------------
 "red"       | red

-- Achtung: color_jsonb hat Anführungszeichen (es ist ein JSON-String).
-- color_text ist ein "richtiger" String ohne Anführungszeichen.

Mit Array-Index:

SQL
myapp=> SELECT
          attrs->'tags'      AS all_tags,
          attrs->'tags'->>0  AS first_tag,
          attrs->'tags'->>1  AS second_tag
        FROM products WHERE sku = 'A1';
      all_tags     | first_tag | second_tag
-------------------+-----------+------------
 ["new", "sale"]  | new       | sale

Arrays sind 0-basiert (anders als Postgres-Arrays). attrs->'tags'->>0 liest: „nimm den tags-Schlüssel, dann das 0. Element, gib’s als Text”.

#> und #>>: Pfad-Zugriff

Wenn du tief verschachtelt zugreifen musst, gibt es Kurzschreibweisen über Array-Pfade:

SQL
myapp=> SELECT
          attrs#>'{dimensions,width}'   AS w_jsonb,
          attrs#>>'{dimensions,width}'  AS w_text
        FROM products WHERE sku = 'A1';
 w_jsonb | w_text
---------+--------
 10      | 10

-- Äquivalent zur längeren Form:
myapp=> SELECT attrs->'dimensions'->>'width' FROM products WHERE sku='A1';

Die Pfad-Notation '{dimensions, width}' ist ein Postgres-Array von Strings — Postgres dröselt das auf und navigiert Schritt für Schritt. Bei 4–5 Ebenen tief lesbarer als die Kette von ->.

@>: Containment

Der wichtigste Such-Operator: „enthält der Wert dieses Teilobjekt?”

SQL Containment-Suche
-- Produkte mit color = 'red'
SELECT sku FROM products
WHERE attrs @> '{"color": "red"}';

-- Produkte mit Tag "sale"
SELECT sku FROM products
WHERE attrs @> '{"tags": ["sale"]}';

-- Mehrere Bedingungen kombiniert
SELECT sku FROM products
WHERE attrs @> '{"color": "red", "in_stock": true}';

Wichtig: @> ist strukturell. Das rechte Objekt muss als Teil-Struktur des linken vorkommen. Bei Arrays bedeutet das: alle Elemente rechts müssen auch links sein:

SQL
-- Produkte mit ALLEN Tags 'new' und 'sale':
SELECT sku FROM products
WHERE attrs @> '{"tags": ["new", "sale"]}';
-- Liefert nur A1 — B2 hat zwar 'bestseller', aber nicht 'new' und 'sale'.

@> ist GIN-Index-fähig — der wichtigste Operator für performante JSONB-Filter.

?, ?| und ?&: Schlüssel-Existenz

OperatorBedeutung
data ? 'key'Existiert dieser Schlüssel in data?
data ?| array['k1','k2']Existiert MINDESTENS einer dieser Schlüssel?
data ?& array['k1','k2']Existieren ALLE diese Schlüssel?
SQL
myapp=> SELECT
          sku,
          attrs ? 'color'     AS has_color,
          attrs ? 'discount'  AS has_discount
        FROM products;
 sku | has_color | has_discount
-----+-----------+--------------
 A1  | t         | f
 B2  | t         | f

? funktioniert auch auf Arrays — prüft, ob ein String als Element vorkommt:

SQL
-- Produkte mit Tag 'sale' (Array-Mitgliedschaft)
SELECT sku FROM products WHERE attrs->'tags' ? 'sale';
-- A1

?| und ?& funktionieren analog, mit Array-Argument:

SQL
-- Schlüssel: hat 'discount' ODER 'promo' ODER 'sale_price'
SELECT sku FROM products
WHERE attrs ?| array['discount', 'promo', 'sale_price'];

-- Hat 'color' UND 'weight_kg' UND 'in_stock'
SELECT sku FROM products
WHERE attrs ?& array['color', 'weight_kg', 'in_stock'];

||: Mergen

|| kombiniert zwei jsonb-Werte. Bei gleichen Schlüsseln gewinnt der rechte:

SQL
myapp=> SELECT '{"a": 1, "b": 2}'::jsonb || '{"b": 99, "c": 3}'::jsonb;
        ?column?
----------------------------
 {"a": 1, "b": 99, "c": 3}

Praktisch in Updates:

SQL Felder hinzufügen oder überschreiben
-- Discount-Feld hinzufügen, ohne andere zu verlieren
UPDATE products
SET attrs = attrs || '{"discount": 15}'::jsonb
WHERE sku = 'A1';

-- Mehrere Felder auf einmal
UPDATE products
SET attrs = attrs || '{"discount": 15, "promo_until": "2026-06-01"}'::jsonb
WHERE sku = 'A1';

Achtung: || macht nur einen flachen Merge. Bei verschachtelten Objekten überschreibt der rechte komplett:

SQL
SELECT '{"dim": {"width": 10, "height": 20}}'::jsonb
    || '{"dim": {"depth": 5}}'::jsonb;
--                ?column?
-- ---------------------------------
--  {"dim": {"depth": 5}}
-- "width" und "height" sind weg!

Für Deep-Merge muss man jsonb_set rekursiv anwenden — siehe Artikel JSONB-Felder updaten.

- und #-: Entfernen

- entfernt einen Schlüssel oder Array-Index:

SQL
-- Schlüssel entfernen
SELECT '{"a": 1, "b": 2}'::jsonb - 'b';
--     ?column?
-- ---------------
--  {"a": 1}

-- Mehrere Schlüssel entfernen
SELECT '{"a": 1, "b": 2, "c": 3}'::jsonb - ARRAY['b', 'c'];
--     ?column?
-- ---------------
--  {"a": 1}

-- Aus Array das Element an Index n entfernen
SELECT '["a", "b", "c"]'::jsonb - 1;
--   ?column?
-- ------------
--  ["a", "c"]

#- entfernt einen tiefen Pfad:

SQL
-- discount-Feld entfernen
UPDATE products
SET attrs = attrs - 'discount'
WHERE sku = 'A1';

-- Tieferes Feld entfernen
UPDATE products
SET attrs = attrs #- '{dimensions, depth}'
WHERE sku = 'A1';

Operator-Spickzettel — Komplettübersicht

OperatorBeispielResultat-TypZweck
->data->'k'jsonbWert als jsonb
->>data->>'k'textWert als text
#>data#>'{a,b}'jsonbPfad-Zugriff (jsonb)
#>>data#>>'{a,b}'textPfad-Zugriff (text)
@>data @> '{}'booleanContainment
<@'{}' <@ databooleanumgekehrt
?data ? 'k'booleanSchlüssel/Element-Existenz
?|data ?| array[…]booleanirgendeiner existiert
?&data ?& array[…]booleanalle existieren
||a || bjsonbMerge (flach)
-data - 'k'jsonbSchlüssel/Index entfernen
#-data #- '{a,b}'jsonbPfad entfernen
@?data @? '$.path'booleanJSON-Path matcht?
@@data @@ '$.x > 5'booleanJSON-Path-Predicate

@? und @@ sind die JSON-Path-Operatoren — eigene Syntax, eigener Artikel: JSONB Pfad-Queries.

Besonderheiten

-> liefert NULL bei fehlendem Schlüssel.

data->'unknown_key' liefert NULL — keinen Fehler. Das ist meistens das, was man will (für LEFT JOIN-artige Logik), aber kann bei Bugs verwirren. data ? 'unknown_key' gibt expliziter false zurück, wenn der Schlüssel fehlt.

->> in WHERE — der Klassiker.

WHERE data->>'status' = 'active' ist die häufigste Form. Mit -> müsste man den Vergleich gegen einen jsonb-String machen (= '"active"'::jsonb), was unintuitiver ist. Faustregel: in WHERE fast immer ->>.

? wird mit GIN-Index optimiert.

Schlüssel-Existenz-Queries sind nicht nur Convenience — sie sind auch index-fähig. Ein GIN-Index auf der jsonb-Spalte beschleunigt data ? 'key'-Filter genauso wie @>. Für Tabellen mit „dynamischen Schlüsseln” sehr nützlich.

|| ist flach — Deep-Merge muss man bauen.

Wer ein verschachteltes Feld update, ohne andere Sub-Felder zu verlieren, braucht jsonb_set(data, '{path}', new_value). || überschreibt komplett. Für tief verschachteltes Merging gibt es kein Built-in — eigene PL/pgSQL-Funktion oder externe Bibliothek (z. B. jsonb_deep_merge aus Community-Repos).

Array-Index ist 0-basiert, anders als Postgres-Arrays.

Im jsonb-Array startet der Index bei 0 (JSON-Standard). In Postgres-Arrays bei 1. attrs->'tags'->>0 ist das erste Element; array_col[1] auch. Kleine, aber stetige Quelle von Bugs beim Wechsel zwischen den beiden Welten.

Casting bei Vergleichen ist wichtig.

data->'count' > 10 schlägt fehl, weil ein jsonb mit einem int verglichen wird. Korrekt: (data->>'count')::int > 10 oder data->'count' > '10'::jsonb. Erstere Variante ist üblicher.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu JSON & JSONB

Zur Übersicht