Eine JSONB-Spalte ist nicht einfach „read-only” — du kannst gezielt einzelne Felder ändern, ohne den Rest des Dokuments anzufassen. Drei Werkzeuge: jsonb_set für Punkt-Updates, || für Merging, und - / #- zum Entfernen. Hier die Patterns, die in echten Web-Apps am häufigsten gebraucht werden.

Beispiel-Tabelle

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

INSERT INTO products (sku, attrs) VALUES
    ('A1', '{
        "name": "Widget",
        "price": 19.95,
        "stock": 100,
        "dimensions": {"width": 10, "height": 20},
        "tags": ["new"]
    }');

Einfaches Feld setzen mit jsonb_set

jsonb_set(target, path, new_value [, create_missing])

SQL Stock-Wert ändern
UPDATE products
SET attrs = jsonb_set(attrs, '{stock}', '85')
WHERE sku = 'A1';

Der Pfad ist ein Postgres-Array-Literal — '{stock}' heißt: Schlüssel stock auf der obersten Ebene. Der neue Wert muss ein jsonb-Wert sein — '85' wird automatisch als jsonb-Zahl geparst.

Mit String:

SQL
UPDATE products
SET attrs = jsonb_set(attrs, '{name}', '"Widget Pro"')
WHERE sku = 'A1';
--                                       ^^^^^^^^^^^^
-- Das ist ein JSON-String — Anführungszeichen IM Wert!

Ergebnis nach beiden Updates:

SQL
myapp=> SELECT jsonb_pretty(attrs) FROM products WHERE sku = 'A1';
{
    "name": "Widget Pro",
    "price": 19.95,
    "stock": 85,
    "dimensions": {"width": 10, "height": 20},
    "tags": ["new"]
}

Verschachtelte Felder

Bei tieferen Pfaden gibt der Pfad-Array die Reihenfolge an:

SQL Width tief in dimensions ändern
UPDATE products
SET attrs = jsonb_set(attrs, '{dimensions, width}', '15')
WHERE sku = 'A1';

'{dimensions, width}' ist ein zwei-elementiger Pfad — Postgres steigt von oben nach unten ab und ändert dimensions.width von 10 auf 15. Der Rest der dimensions (z. B. height) bleibt unangetastet.

Wert hinzufügen, der noch nicht existiert

jsonb_set hat ein viertes Argument create_missing (default true):

SQL
-- Neues Feld 'discount' hinzufügen
UPDATE products
SET attrs = jsonb_set(attrs, '{discount}', '15', true)
WHERE sku = 'A1';

-- Mit create_missing = false: nur ändern, wenn schon vorhanden
UPDATE products
SET attrs = jsonb_set(attrs, '{nonexistent}', '"x"', false)
WHERE sku = 'A1';
-- Wirkungslos — Feld existiert nicht und wird nicht angelegt.

Default true ist meistens das, was man will: „setze, oder lege an wenn nicht da”. Mit false kannst du explizit verhindern, dass typo’d Schlüssel angelegt werden.

Mehrere Felder auf einmal mit ||

Für mehrere Felder ist || (Merge-Operator) oft kürzer als verschachtelte jsonb_set-Aufrufe:

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

Schlüssel, die schon existieren, werden überschrieben. Schlüssel, die neu sind, kommen dazu. Andere bleiben unangetastet — solange sie nicht im Merge-Objekt vorkommen.

Aber Achtung — flacher Merge:

SQL
-- Das überschreibt dimensions KOMPLETT, nicht nur 'depth':
UPDATE products
SET attrs = attrs || '{"dimensions": {"depth": 5}}'::jsonb
WHERE sku = 'A1';
-- attrs.dimensions ist jetzt {"depth": 5}, width und height sind weg!

Wer wirklich nur depth hinzufügen will, ohne den Rest zu verlieren, muss jsonb_set nehmen:

SQL Saubere Variante mit jsonb_set
UPDATE products
SET attrs = jsonb_set(attrs, '{dimensions, depth}', '5')
WHERE sku = 'A1';
-- Jetzt: dimensions = {"width": 10, "height": 20, "depth": 5}

Felder entfernen

Mit - (oberste Ebene) oder #- (tiefer Pfad):

SQL
-- Schlüssel auf oberster Ebene entfernen
UPDATE products
SET attrs = attrs - 'discount'
WHERE sku = 'A1';

-- Mehrere Schlüssel auf einmal
UPDATE products
SET attrs = attrs - ARRAY['discount', 'promo_until']
WHERE sku = 'A1';

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

- mit einem String entfernt einen Schlüssel. - mit einem Array von Strings entfernt mehrere. #- mit einem Pfad-Array entfernt einen tiefen Pfad.

Arrays verändern

Arrays innerhalb von jsonb sind etwas eigenwillig — sie werden meistens komplett neu geschrieben:

SQL Tag anhängen
-- Neuen Tag ans Ende
UPDATE products
SET attrs = jsonb_set(
    attrs,
    '{tags}',
    (attrs->'tags') || '"sale"'
)
WHERE sku = 'A1';

-- Oder kürzer mit dem ||-Operator auf dem Subarray:
UPDATE products
SET attrs = jsonb_set(attrs, '{tags}', attrs->'tags' || '"bestseller"'::jsonb)
WHERE sku = 'A1';

jsonb_insert ist für gezielte Array-Inserts an bestimmter Position:

SQL
-- Tag an Position 0 (Anfang) einfügen
UPDATE products
SET attrs = jsonb_insert(attrs, '{tags, 0}', '"featured"')
WHERE sku = 'A1';

-- Tag NACH Position 0 einfügen (also an Position 1)
UPDATE products
SET attrs = jsonb_insert(attrs, '{tags, 0}', '"featured"', true)
WHERE sku = 'A1';
--                                                    ^^^^ insert_after

Element an bestimmter Position entfernen:

SQL
UPDATE products
SET attrs = attrs #- '{tags, 0}'
WHERE sku = 'A1';
-- Entfernt das erste Element des tags-Arrays.

Bedingte Updates

Mit WHERE-Klausel auf JSONB-Werten:

SQL Nur wenn Bedingung erfüllt
-- Stock auf 0 setzen, wenn aktuell unter 10
UPDATE products
SET attrs = jsonb_set(attrs, '{stock}', '0')
WHERE (attrs->>'stock')::int < 10;

-- Discount nur entfernen, wenn er existiert
UPDATE products
SET attrs = attrs - 'discount'
WHERE attrs ? 'discount';

WHERE attrs ? 'discount' macht den Update nur effektiv, wo das Feld existiert — vermeidet unnötige Row-Writes.

Atomarer Counter in jsonb

Häufiger Use-Case: einen Zähler hochzählen, ohne Race Condition.

SQL View-Counter erhöhen
UPDATE products
SET attrs = jsonb_set(
    attrs,
    '{view_count}',
    to_jsonb(COALESCE((attrs->>'view_count')::int, 0) + 1)
)
WHERE id = 1;

Schritt für Schritt:

  1. attrs->>'view_count' — aktueller Wert als text (oder NULL, wenn fehlt)
  2. ::int — als integer interpretieren
  3. COALESCE(…, 0) — Default 0, falls NULL
  4. + 1 — inkrementieren
  5. to_jsonb(…) — zurück in jsonb (sonst Type-Mismatch)
  6. jsonb_set(…) — ins Dokument schreiben

UPDATE ist atomar — selbst bei parallelen Aufrufen geht kein Inkrement verloren (Postgres-Locking macht das automatisch).

FAQ

Wie merge ich zwei verschachtelte jsonb-Werte tief?

Standard-|| ist flach. Für Deep-Merge gibt es kein Built-in. Optionen: PL/pgSQL-Funktion selbst schreiben, eine Community-Extension wie jsonb_deep_merge, oder im Anwendungs-Code mergen und kompletten Wert schreiben. Die meisten Apps brauchen das nur selten — meist reicht jsonb_set für den einen Pfad.

Warum funktioniert mein UPDATE attrs->'name' = '...' nicht?

Operator-Resultate sind nicht zuweisbar. attrs->'name' ist ein Ausdruck, kein Lvalue. Du musst den ganzen attrs-Wert neu setzen — typisch via jsonb_set oder ||.

Performance: ist UPDATE auf einer großen jsonb-Spalte teuer?

Ja — Postgres schreibt die ganze Zeile neu, nicht nur das geänderte Feld. Bei einem 100-KB-jsonb-Wert kostet ein einzelnes Feld-Update genauso viel wie ein kompletter Rewrite. Bei sehr aktiven jsonb-Spalten ist das ein Argument für klassische Spalten oder Aufteilung.

JSONB Subscripting (PG 14+) als Alternative.

Seit PG 14 kann man auch UPDATE products SET attrs['stock'] = '85'::jsonb schreiben — eine schönere Syntax als jsonb_set. Funktional dasselbe. Bei gemischten Stack-Versionen lieber bei jsonb_set bleiben, das funktioniert überall.

NULL-Werte gezielt setzen.

jsonb_set(attrs, '{field}', 'null') setzt den Wert auf JSON-null (existiert mit Wert null). attrs - 'field' entfernt das Feld komplett. Beides hat unterschiedliches Verhalten in ?-Checks und Containment.

Casting bei Werten oft nötig.

jsonb_set erwartet beide Argumente als jsonb. Wenn du z. B. einen Text einsetzt, schreib '"foo"'::jsonb (mit doppelten Quotes — JSON-String). Ein nackter Text wäre Syntax-Fehler. Zahlen brauchen keine Quotes ('42'::jsonb), Booleans auch nicht ('true'::jsonb).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu JSON & JSONB

Zur Übersicht