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
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])
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:
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:
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:
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):
-- 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:
-- 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:
-- 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:
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):
-- 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:
-- 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:
-- 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_afterElement an bestimmter Position entfernen:
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:
-- 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.
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:
attrs->>'view_count'— aktueller Wert als text (oder NULL, wenn fehlt)::int— als integer interpretierenCOALESCE(…, 0)— Default 0, falls NULL+ 1— inkrementierento_jsonb(…)— zurück in jsonb (sonst Type-Mismatch)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).