Jak wykonać operacje aktualizacji na kolumnach typu JSONB w Postgres 9.4

132

Przeglądając dokumentację JSONB typu danych Postgres 9.4, nie od razu wiem, jak wykonać aktualizacje kolumn JSONB.

Dokumentacja dotycząca typów i funkcji JSONB:

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

Jako przykłady mam taką podstawową strukturę tabeli:

CREATE TABLE test(id serial, data jsonb);

Wstawianie jest łatwe, jak w:

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

Jak zaktualizować kolumnę „dane”? To jest nieprawidłowa składnia:

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

Czy jest to udokumentowane gdzieś, że przegapiłem? Dzięki.

jvous
źródło

Odpowiedzi:

32

W idealnym przypadku nie używasz dokumentów JSON dla uporządkowanych, zwykłych danych, którymi chcesz manipulować w relacyjnej bazie danych. Zamiast tego użyj znormalizowanego projektu relacyjnego .

JSON jest przede wszystkim przeznaczony do przechowywania całych dokumentów, które nie wymagają manipulacji w RDBMS. Związane z:

Aktualizacja wiersza w Postgres zawsze powoduje zapisanie nowej wersji całego wiersza. To podstawowa zasada modelu MVCC Postgresa . Z punktu widzenia wydajności nie ma znaczenia, czy zmienisz pojedynczy fragment danych w obiekcie JSON, czy wszystkie: należy napisać nową wersję wiersza.

Dlatego rady w instrukcji :

Dane JSON podlegają tym samym zagadnieniom dotyczącym kontroli współbieżności, co każdy inny typ danych przechowywanych w tabeli. Chociaż przechowywanie dużych dokumentów jest możliwe, należy pamiętać, że każda aktualizacja powoduje zablokowanie całego wiersza na poziomie wiersza. Rozważ ograniczenie dokumentów JSON do rozmiaru umożliwiającego zarządzanie, aby zmniejszyć rywalizację o blokady między aktualizacjami transakcji. Idealnie byłoby, gdyby każdy dokument JSON reprezentował niepodzielne dane, które narzucają reguły biznesowe, nie mogą być dalej dzielone na mniejsze dane, które można modyfikować niezależnie.

Istota tego: aby zmodyfikować cokolwiek wewnątrz obiektu JSON, musisz przypisać zmodyfikowany obiekt do kolumny. Postgres zapewnia ograniczone możliwości tworzenia i manipulowania jsondanymi oprócz swoich możliwości przechowywania. Arsenał narzędzi znacznie się powiększył z każdą nową wersją od wersji 9.2. Ale zasada pozostaje: zawsze musisz przypisać całkowicie zmodyfikowany obiekt do kolumny, a Postgres zawsze zapisuje nową wersję wiersza dla każdej aktualizacji.

Niektóre techniki pracy z narzędziami Postgres 9.3 lub nowszym:

Ta odpowiedź przyciągnęła mniej więcej tyle samo głosów przeciw, co wszystkie moje inne odpowiedzi razem na SO . Ludziom nie podoba się ten pomysł: znormalizowany projekt jest lepszy w przypadku danych niedynamicznych. Ten doskonały wpis na blogu Craiga Ringera wyjaśnia bardziej szczegółowo:

Erwin Brandstetter
źródło
6
Ta odpowiedź dotyczy tylko typu JSON i ignoruje JSONB.
fiatjaf 15.07.15
7
@fiatjaf: Ta odpowiedź ma pełne zastosowanie do typów danych jsoni jsonbpodobnych. Oba przechowują dane JSON, jsonbrobią to w znormalizowanej formie binarnej, która ma pewne zalety (i kilka wad). stackoverflow.com/a/10560761/939860 Ani typ danych jest dobra dla bycia manipulowania dużo wewnątrz bazy danych. Żaden typ dokumentu nie jest. Cóż, jest to dobre dla małych, mało ustrukturyzowanych dokumentów JSON. Ale duże, zagnieżdżone dokumenty byłyby głupotą.
Erwin Brandstetter
7
„Instrukcje, jak pracować z narzędziami Postgres 9.3”, naprawdę warto być pierwszym w odpowiedzi, ponieważ odpowiada ona na zadane pytanie .. czasami warto zaktualizować json w celu zmiany konserwacji / schematu itp. Oraz powody, dla których nie należy aktualizować json don tak naprawdę
Michael Wasser
22
Przepraszam, ta odpowiedź nie jest pomocna. @jvous, czy nie chcesz przyjąć odpowiedzi Jimothy'ego, ponieważ naprawdę odpowiada ona na twoje pytanie?
Bastian Voigt,
10
Odpowiedz najpierw na pytanie, zanim dodasz własny komentarz / opinię / dyskusję.
PPP
330

Jeśli możesz zaktualizować do Postgresql 9.5, jsonb_setpolecenie jest dostępne, jak wspominali inni.

W każdej z poniższych instrukcji SQL pominąłem whereklauzulę dotyczącą zwięzłości; oczywiście chciałbyś to dodać.

Nazwa aktualizacji:

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

Zastąp tagi (w przeciwieństwie do dodawania lub usuwania tagów):

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

Zastąpienie drugiego tagu (z indeksem 0):

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

Dołącz tag ( będzie działać, o ile będzie mniej niż 999 tagów; zmiana argumentu 999 na 1000 lub więcej generuje błąd . Wydaje się, że nie ma to już miejsca w Postgres 9.5.3; można użyć znacznie większego indeksu) :

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

Usuń ostatni tag:

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

Złożona aktualizacja (usuń ostatni tag, wstaw nowy tag i zmień nazwę):

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

Należy zauważyć, że w każdym z tych przykładów w rzeczywistości nie aktualizujesz ani jednego pola danych JSON. Zamiast tego tworzysz tymczasową, zmodyfikowaną wersję danych i przypisujesz tę zmodyfikowaną wersję z powrotem do kolumny. W praktyce wynik powinien być taki sam, ale pamiętanie o tym powinno sprawić, że złożone aktualizacje, takie jak ostatni przykład, będą bardziej zrozumiałe.

W złożonym przykładzie istnieją trzy transformacje i trzy wersje tymczasowe: Po pierwsze, ostatni tag jest usuwany. Następnie ta wersja jest przekształcana przez dodanie nowego tagu. Następnie druga wersja jest przekształcana poprzez zmianę namepola. Wartość w datakolumnie zostanie zastąpiona wersją ostateczną.

Jimothy
źródło
42
otrzymujesz dodatkowe punkty za pokazanie, jak zaktualizować kolumnę w tabeli zgodnie z żądaniem PO
chadrik
1
@chadrik: Dodałem bardziej złożony przykład. Nie robi dokładnie tego, o co prosiłeś, ale powinien dać ci pomysł. Należy zauważyć, że dane wejściowe do jsonb_setwywołania zewnętrznego są danymi wyjściowymi z wywołania wewnętrznego, a dane wejściowe do tego wywołania wewnętrznego są wynikiem data #- '{tags,-1}'. To znaczy oryginalne dane z usuniętym ostatnim tagiem.
Jimothy,
1
@PranaySoni: W tym celu prawdopodobnie użyłbym procedury składowanej lub, jeśli narzut nie jest problemem, przywróciłbym te dane, zmanipulowałbym je w języku aplikacji, a następnie zapisał. Brzmi to ciężko, ale pamiętaj, że we wszystkich podanych przeze mnie przykładach nadal nie aktualizujesz ani jednego pola w JSON (B): nadpisujesz całą kolumnę w obu przypadkach. Tak więc przechowywany proces tak naprawdę nie różni się.
Jimothy,
1
@Alex: Tak, trochę włamań. Gdybym powiedział {tags,0}, oznaczałoby to „pierwszy element tablicy tags”, pozwalając mi nadać nową wartość temu elementowi. Używając dużej liczby zamiast 0, zamiast zastępować istniejący element w tablicy, dodaje nowy element do tablicy. Jeśli jednak tablica faktycznie zawiera więcej niż 999 999 999 elementów, spowoduje to zastąpienie ostatniego elementu zamiast dodania nowego.
Jimothy
1
a co jeśli pole zawiera null? wygląda na to, że nie działa. Np. Pole info jsonb ma wartość null: "UPDATE organizer SET info = jsonb_set (info, '{country}', '" FRA "') gdzie info - >> 'country' :: text IS NULL;" Otrzymuję rekord UPDATE 105, ale bez zmian na db
stackdave
24

To pojawi się w 9.5 w postaci jsonb_set autorstwa Andrew Dunstana w oparciu o istniejące rozszerzenie jsonbx, które działa z 9.4

philofinfinitejest
źródło
Inną kwestią w tym wierszu jest użycie jsonb_build_object(), ponieważ x->keynie zwraca pary klucz-obiekt, aby wypełnić ją potrzebną jsonb_set(target, path, jsonb_build_object('key',x->key)).
Peter Krauss
18

Dla tych, którzy napotkali ten problem i chcą bardzo szybkiej naprawy (i utknęli w wersji 9.4.5 lub wcześniejszej), oto co zrobiłem:

Stworzenie tabeli testowej

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

Zaktualizuj instrukcję, aby zmienić nazwę właściwości JSONB

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

Ostatecznie przyjęta odpowiedź jest prawidłowa, ponieważ nie można zmodyfikować pojedynczego fragmentu obiektu jsonb (w wersji 9.4.5 lub wcześniejszej); jednak można rzutować obiekt jsonb na łańcuch (:: TEXT), a następnie manipulować łańcuchem i rzutować z powrotem na obiekt jsonb (:: jsonb).

Istnieją dwa ważne zastrzeżenia

  1. spowoduje to zastąpienie wszystkich właściwości o nazwie „nazwa” w pliku json (w przypadku, gdy masz wiele właściwości o tej samej nazwie)
  2. nie jest to tak wydajne jak jsonb_set, jeśli używasz 9.5

Powiedziawszy to, natknąłem się na sytuację, w której musiałem zaktualizować schemat zawartości w obiektach jsonb i był to najprostszy sposób, aby osiągnąć dokładnie to, o co prosił oryginalny plakat.

Chad Capra
źródło
1
Dobry Boże, szukałem sposobu na aktualizację jsonb przez jakieś dwie godziny, abym mógł zamienić wszystkie \u0000puste znaki, przykład pokazał pełny obraz. Dzięki za to!
Joshua Robinson
3
wygląda dobrze! btw drugi argument do zastąpienia w twoim przykładzie zawiera dwukropek, a trzeci nie. Wygląda na to, że powinieneś zadzwonićreplace(data::TEXT, '"name":', '"my-other-name":')::jsonb
davidicus
Dziękuję @davidicus! Przepraszamy za bardzo opóźnioną aktualizację, ale doceniam, że udostępniliście ją innym!
Chad Capra,
12

To pytanie zostało postawione w kontekście postgres 9.4, jednak nowi widzowie przychodzący na to pytanie powinni mieć świadomość, że w postgres 9.5 pod-dokument Operacje Create / Update / Delete na polach JSONB są natywnie obsługiwane przez bazę danych, bez konieczności rozbudowy Funkcje.

Zobacz: operatory i funkcje modyfikujące JSONB

bguiz
źródło
7

zaktualizuj atrybut „name”:

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

a jeśli chcesz usunąć na przykład atrybuty „nazwa” i „tagi”:

UPDATE test SET data=data-'{"name","tags"}'::text[] WHERE id = 1;
Arthur
źródło
5

Napisałem dla siebie małą funkcję, która działa rekurencyjnie w Postgres 9.4. Miałem ten sam problem (dobrze, że rozwiązali część tego bólu głowy w Postgres 9.5). W każdym razie tutaj jest funkcja (mam nadzieję, że działa dobrze):

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;

Oto przykładowe użycie:

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)

Jak widać, analizuje głęboko i aktualizuje / dodaje wartości w razie potrzeby.

J. Raczkiewicz
źródło
To nie działa w wersji 9.4, ponieważ jsonb_build_objectzostało wprowadzone w wersji 9.5
Greg
@Greg Masz rację, właśnie sprawdziłem i teraz używam PostgreSQL 9.5 - dlatego to działa. Dziękuję za zwrócenie uwagi - moje rozwiązanie nie będzie działać w wersji 9.4.
J. Raczkiewicz
4

Może: UPDATE test SET data = '"moja-inna-nazwa"' :: json WHERE id = 1;

Zadziałało w moim przypadku, gdzie dane są typu json

Gianluigi Sartori
źródło
1
U mnie też działało na postgresql 9.4.5. Cały rekord jest przepisywany, więc nie można zaktualizować jednego bankomatu w polu.
kometen