Ausführen von Aktualisierungsvorgängen für Spalten vom Typ JSONB in ​​Postgres 9.4

131

Wenn ich mir die Dokumentation zum Postgres 9.4-Datentyp JSONB ansehe, ist mir nicht sofort klar, wie Aktualisierungen an JSONB-Spalten durchgeführt werden.

Dokumentation für JSONB-Typen und -Funktionen:

http://www.postgresql.org/docs/9.4/static/functions-json.html http://www.postgresql.org/docs/9.4/static/datatype-json.html

Als Beispiel habe ich diese grundlegende Tabellenstruktur:

CREATE TABLE test(id serial, data jsonb);

Das Einfügen ist einfach wie in:

INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Wie würde ich nun die Spalte "Daten" aktualisieren? Dies ist eine ungültige Syntax:

UPDATE test SET data->'name' = 'my-other-name' WHERE id = 1;

Ist dies irgendwo offensichtlich dokumentiert, was ich verpasst habe? Vielen Dank.

jvous
quelle

Antworten:

31

Im Idealfall verwenden Sie keine JSON-Dokumente für strukturierte, reguläre Daten, die Sie in einer relationalen Datenbank bearbeiten möchten. Verwenden Sie stattdessen ein normalisiertes relationales Design .

JSON soll in erster Linie ganze Dokumente speichern, die nicht im RDBMS bearbeitet werden müssen. Verbunden:

Durch das Aktualisieren einer Zeile in Postgres wird immer eine neue Version der gesamten Zeile geschrieben. Das ist das Grundprinzip des MVCC-Modells von Postgres . Aus Sicht der Leistung spielt es kaum eine Rolle, ob Sie ein einzelnes Datenelement in einem JSON-Objekt oder alles ändern: Es muss eine neue Version der Zeile geschrieben werden.

So der Rat im Handbuch :

JSON-Daten unterliegen denselben Überlegungen zur Parallelitätskontrolle wie alle anderen Datentypen, wenn sie in einer Tabelle gespeichert werden. Obwohl das Speichern großer Dokumente praktikabel ist, sollten Sie berücksichtigen, dass jedes Update eine Sperre auf Zeilenebene für die gesamte Zeile erhält. Ziehen Sie in Betracht, JSON-Dokumente auf eine überschaubare Größe zu beschränken, um Sperrenkonflikte bei der Aktualisierung von Transaktionen zu verringern. Im Idealfall sollten JSON-Dokumente jeweils ein Atomdatum darstellen, das von den Geschäftsregeln vorgeschrieben wird, und können nicht vernünftigerweise weiter in kleinere Daten unterteilt werden, die unabhängig voneinander geändert werden können.

Das Wesentliche: Um etwas in einem JSON-Objekt zu ändern , müssen Sie der Spalte ein geändertes Objekt zuweisen. Postgres jsonbietet zusätzlich zu seinen Speicherfunktionen nur begrenzte Möglichkeiten zum Erstellen und Bearbeiten von Daten. Das Arsenal an Tools ist mit jeder neuen Version seit Version 9.2 erheblich gewachsen. Aber die Haupt bleibt: Sie immer ein komplettes modifizierte Objekt in die Spalte zuweisen und Postgres schreibt immer eine neue Zeile Version für jedes Update.

Einige Techniken zum Arbeiten mit den Werkzeugen von Postgres 9.3 oder höher:

Diese Antwort hat ungefähr so ​​viele Downvotes angezogen wie alle meine anderen Antworten auf SO zusammen . Die Idee scheint den Leuten nicht zu gefallen: Ein normalisiertes Design ist für nicht dynamische Daten überlegen. Dieser ausgezeichnete Blog-Beitrag von Craig Ringer erklärt ausführlicher:

Erwin Brandstetter
quelle
6
Diese Antwort betrifft nur den Typ JSON und ignoriert JSONB.
Fiatjaf
7
@fiatjaf: Diese Antwort gilt für alle Datentypen jsonund jsonbgleichermaßen. Beide speichern JSON-Daten jsonbin einer normalisierten Binärform, die einige Vor- (und wenige Nachteile) aufweist. stackoverflow.com/a/10560761/939860 Keiner der Datentypen eignet sich für viele Manipulationen in der Datenbank. Kein Dokumenttyp ist. Nun, es ist gut für kleine, kaum strukturierte JSON-Dokumente. Aber große, verschachtelte Dokumente wären auf diese Weise eine Torheit.
Erwin Brandstetter
7
"Anweisungen zum Arbeiten mit den Tools von Postgres 9.3" sollte in Ihrer Antwort wirklich an erster Stelle stehen, da es die gestellte Frage beantwortet. Manchmal ist es sinnvoll, json für Wartungs- / Schemaänderungen usw. und die Gründe für die Nichtaktualisierung von json don zu aktualisieren Nicht wirklich zutreffen
Michael Wasser
22
Diese Antwort ist leider nicht hilfreich. @jvous, willst du Jimothys Antwort nicht akzeptieren, da sie deine Frage wirklich beantwortet?
Bastian Voigt
10
Beantworten Sie die Frage zuerst, bevor Sie Ihren eigenen Kommentar / Ihre eigene Meinung / Diskussion hinzufügen.
Ppp
330

Wenn Sie ein Upgrade auf Postgresql 9.5 durchführen können, ist der jsonb_setBefehl verfügbar, wie bereits erwähnt.

In jeder der folgenden SQL-Anweisungen habe ich die whereKlausel der Kürze halber weggelassen . Natürlich möchten Sie das wieder hinzufügen.

Name aktualisieren:

UPDATE test SET data = jsonb_set(data, '{name}', '"my-other-name"');

Ersetzen Sie die Tags (im Gegensatz zum Hinzufügen oder Entfernen von Tags):

UPDATE test SET data = jsonb_set(data, '{tags}', '["tag3", "tag4"]');

Ersetzen des zweiten Tags (0-indiziert):

UPDATE test SET data = jsonb_set(data, '{tags,1}', '"tag5"');

Fügen Sie ein Tag hinzu ( dies funktioniert, solange weniger als 999 Tags vorhanden sind . Wenn Sie das Argument 999 auf 1000 oder höher ändern, wird ein Fehler generiert . Dies scheint in Postgres 9.5.3 nicht mehr der Fall zu sein. Es kann ein viel größerer Index verwendet werden.) ::

UPDATE test SET data = jsonb_set(data, '{tags,999999999}', '"tag6"', true);

Entfernen Sie das letzte Tag:

UPDATE test SET data = data #- '{tags,-1}'

Komplexes Update (Löschen Sie das letzte Tag, fügen Sie ein neues Tag ein und ändern Sie den Namen):

UPDATE test SET data = jsonb_set(
    jsonb_set(data #- '{tags,-1}', '{tags,999999999}', '"tag3"', true), 
    '{name}', '"my-other-name"');

Es ist wichtig zu beachten, dass Sie in jedem dieser Beispiele kein einzelnes Feld der JSON-Daten aktualisieren. Stattdessen erstellen Sie eine temporäre, geänderte Version der Daten und weisen diese geänderte Version wieder der Spalte zu. In der Praxis sollte das Ergebnis das gleiche sein, aber wenn Sie dies berücksichtigen, sollten komplexe Aktualisierungen wie im letzten Beispiel verständlicher werden.

In dem komplexen Beispiel gibt es drei Transformationen und drei temporäre Versionen: Erstens wird das letzte Tag entfernt. Diese Version wird dann durch Hinzufügen eines neuen Tags transformiert. Als nächstes wird die zweite Version durch Ändern des nameFeldes transformiert . Der Wert in der dataSpalte wird durch die endgültige Version ersetzt.

Jimothy
quelle
42
Sie erhalten Bonuspunkte, wenn Sie zeigen, wie eine Spalte in einer Tabelle
gemäß
1
@chadrik: Ich habe ein komplexeres Beispiel hinzugefügt. Es macht nicht genau das, was Sie angefordert haben, aber es sollte Ihnen eine Idee geben. Beachten Sie, dass die Eingabe für den äußeren jsonb_setAnruf die Ausgabe des inneren Anrufs ist und dass die Eingabe für diesen inneren Anruf das Ergebnis von ist data #- '{tags,-1}'. Dh die Originaldaten mit dem zuletzt entfernten Tag.
Jimothy
1
@PranaySoni: Zu diesem Zweck würde ich wahrscheinlich eine gespeicherte Prozedur verwenden oder, wenn der Overhead keine Rolle spielt, diese Daten zurückbringen, in der Sprache der Anwendung bearbeiten und dann zurückschreiben. Das klingt schwer, aber denken Sie daran, dass Sie in allen Beispielen, die ich gegeben habe, immer noch kein einzelnes Feld im JSON aktualisieren (B): Sie überschreiben die gesamte Spalte so oder so. Ein gespeicherter Prozess ist also wirklich nicht anders.
Jimothy
1
@ Alex: Ja, ein bisschen ein Hack. Wenn ich das sagen {tags,0}würde, würde das "das erste Element des Arrays tags" bedeuten und mir erlauben, diesem Element einen neuen Wert zu geben. Indem Sie eine große Zahl anstelle von 0 verwenden, anstatt ein vorhandenes Element im Array zu ersetzen, wird dem Array ein neues Element hinzugefügt. Wenn das Array jedoch tatsächlich mehr als 999.999.999 Elemente enthält, würde dies das letzte Element ersetzen, anstatt ein neues hinzuzufügen.
Jimothy
1
Was ist, wenn das Feld null enthält? sieht nicht aus. Beispiel: Das Feld jsonb info ist null: "UPDATE-Organisator SET info = jsonb_set (info, '{country}', '" FRA "') wobei info - >> 'country' :: text NULL ist;" Ich erhalte aber den UPDATE 105-Datensatz Keine Änderungen an db
stackdave
24

Dies kommt in 9.5 in Form von jsonb_set von Andrew Dunstan, basierend auf einer vorhandenen Erweiterung jsonbx , die mit 9.4 funktioniert

philofinfinitejest
quelle
Ein weiteres Problem in dieser Zeile ist die Verwendung von jsonb_build_object(), da x->keykein Schlüssel-Objekt-Paar zurückgegeben wird, um die von Ihnen benötigten Daten zu füllen jsonb_set(target, path, jsonb_build_object('key',x->key)).
Peter Krauss
18

Für diejenigen, die auf dieses Problem stoßen und eine sehr schnelle Lösung wünschen (und auf 9.4.5 oder früher stecken bleiben), habe ich Folgendes getan:

Erstellung einer Testtabelle

CREATE TABLE test(id serial, data jsonb);
INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Aktualisieren Sie die Anweisung, um den Namen der jsonb-Eigenschaft zu ändern

UPDATE test 
SET data = replace(data::TEXT,'"name":','"my-other-name":')::jsonb 
WHERE id = 1;

Letztendlich ist die akzeptierte Antwort insofern richtig, als Sie ein einzelnes Stück eines jsonb-Objekts (in 9.4.5 oder früher) nicht ändern können. Sie können das jsonb-Objekt jedoch in eine Zeichenfolge (:: TEXT) umwandeln und dann die Zeichenfolge bearbeiten und in das jsonb-Objekt (:: jsonb) zurückverwandeln.

Es gibt zwei wichtige Vorbehalte

  1. Dies ersetzt alle Eigenschaften, die im json als "Name" bezeichnet werden (falls Sie mehrere Eigenschaften mit demselben Namen haben).
  2. Dies ist nicht so effizient wie jsonb_set, wenn Sie 9.5 verwenden

Nachdem dies gesagt wurde, stieß ich auf eine Situation, in der ich das Schema für den Inhalt in den jsonb-Objekten aktualisieren musste, und dies war der einfachste Weg, um genau das zu erreichen, was das ursprüngliche Poster verlangte.

Chad Capra
quelle
1
Guter Herr, ich habe zwei Stunden lang nach einer Möglichkeit gesucht, ein Update für jsonb durchzuführen, damit ich alle \u0000Nullzeichen ersetzen kann. Das Beispiel zeigt das vollständige Bild. Danke dafür!
Joshua Robinson
3
sieht gut aus! Übrigens enthält das zweite Argument, das in Ihrem Beispiel ersetzt werden soll, den Doppelpunkt und das dritte nicht. Sieht so aus, als ob Ihr Anruf sein solltereplace(data::TEXT, '"name":', '"my-other-name":')::jsonb
Davidicus
Vielen Dank, dass Sie @davidicus! Entschuldigen Sie das sehr verspätete Update, aber ich freue mich, dass Sie es für andere teilen!
Chad Capra
12

Diese Frage wurde im Zusammenhang mit Postgres 9.4 gestellt. Neue Betrachter, die zu dieser Frage kommen, sollten sich jedoch darüber im Klaren sein, dass in Postgres 9.5 die Vorgänge zum Erstellen / Aktualisieren / Löschen von Unterdokumenten für JSONB-Felder von der Datenbank nativ unterstützt werden, ohne dass eine Erweiterung erforderlich ist Funktionen.

Siehe: JSONB zum Ändern von Operatoren und Funktionen

bguiz
quelle
7

Aktualisieren Sie das Attribut 'Name':

UPDATE test SET data=data||'{"name":"my-other-name"}' WHERE id = 1;

und wenn Sie zum Beispiel die Attribute 'name' und 'tags' entfernen möchten:

UPDATE test SET data=data-'{"name","tags"}'::text[] WHERE id = 1;
Arthur
quelle
5

Ich habe eine kleine Funktion für mich geschrieben, die in Postgres 9.4 rekursiv funktioniert. Ich hatte das gleiche Problem (gut, dass sie einige dieser Kopfschmerzen in Postgres 9.5 gelöst haben). Wie auch immer, hier ist die Funktion (ich hoffe, es funktioniert gut für Sie):

CREATE OR REPLACE FUNCTION jsonb_update(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
    result JSONB;
    v RECORD;
BEGIN
    IF jsonb_typeof(val2) = 'null'
    THEN 
        RETURN val1;
    END IF;

    result = val1;

    FOR v IN SELECT key, value FROM jsonb_each(val2) LOOP

        IF jsonb_typeof(val2->v.key) = 'object'
            THEN
                result = result || jsonb_build_object(v.key, jsonb_update(val1->v.key, val2->v.key));
            ELSE
                result = result || jsonb_build_object(v.key, v.value);
        END IF;
    END LOOP;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

Hier ist ein Beispiel für die Verwendung:

select jsonb_update('{"a":{"b":{"c":{"d":5,"dd":6},"cc":1}},"aaa":5}'::jsonb, '{"a":{"b":{"c":{"d":15}}},"aa":9}'::jsonb);
                            jsonb_update                             
---------------------------------------------------------------------
 {"a": {"b": {"c": {"d": 15, "dd": 6}, "cc": 1}}, "aa": 9, "aaa": 5}
(1 row)

Wie Sie sehen können, analysieren Sie tief im Inneren und aktualisieren / fügen Sie Werte hinzu, wo dies erforderlich ist.

J. Raczkiewicz
quelle
Dies funktioniert nicht in 9.4, da jsonb_build_objectes in 9.5 eingeführt wurde
Greg
@ Greg Du hast recht, ich habe es gerade überprüft und ich verwende jetzt PostgreSQL 9.5 - deshalb funktioniert es. Vielen Dank für den Hinweis - meine Lösung wird in 9.4 nicht funktionieren.
J. Raczkiewicz
4

Vielleicht: UPDATE test SET data = '"mein-anderer-Name"' :: json WHERE id = 1;

Es hat in meinem Fall funktioniert, in dem Daten vom Typ JSON sind

Gianluigi Sartori
quelle
1
Arbeitete auch für mich auf postgresql 9.4.5. Der gesamte Datensatz wird neu geschrieben, sodass kein einziges Feld atm aktualisiert werden kann.
Kometen