Idiomatyczny sposób implementacji UPSERT w PostgreSQL

40

Czytałem o różnych UPSERTimplementacjach w PostgreSQL, ale wszystkie te rozwiązania są stosunkowo stare lub względnie egzotyczne ( na przykład przy użyciu zapisywalnego CTE ).

I po prostu wcale nie jestem ekspertem od psql, aby dowiedzieć się od razu, czy te rozwiązania są stare, ponieważ są dobrze zalecane, czy też są (cóż, prawie wszystkie z nich) tylko zabawkowymi przykładami nieodpowiednimi do użytku produkcyjnego.

Jaki jest najbardziej bezpieczny dla wątków sposób wdrożenia UPSERT w PostgreSQL?

shabunc
źródło

Odpowiedzi:

23

PostgreSQL ma teraz UPSERT .


Preferowana metoda według podobnego pytania StackOverflow jest obecnie następująca:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
Leigh Riffel
źródło
7
Wolałbym użyć zapisywalnego CTE: stackoverflow.com/a/8702291/330315
a_konia_na_nazwa
Jaka jest zaleta zapisywalnego CTE w porównaniu z funkcją?
François Beausoleil
1
@ François, po pierwsze, prędkość. Za pomocą CTE trafiłeś do bazy danych jeden raz. Robiąc to w ten sposób możesz trafić go dwa lub więcej razy. Ponadto optymalizator nie może optymalizować procedur PL / PGSQL tak skutecznie jak czysty kod SQL.
Adam Mackler,
1
@ François Po drugie, współbieżność. Ponieważ powyższy przykład zawiera wiele instrukcji SQL, musisz się martwić warunkami wyścigu (przyczyną pętli klugey). Pojedyncza instrukcja SQL będzie atomowa. Zobacz ten link
Adam Mackler,
1
@ FrançoisBeausoleil zobacz tutaj i tutaj, dlaczego. Zasadniczo bez pętli ponownej próby albo musisz wykonać serializację, albo masz możliwość awarii z powodu nieodłącznych warunków wyścigu.
Jack Douglas,
27

AKTUALIZACJA (2015-08-20):

Istnieje teraz oficjalna implementacja obsługi wstawek za pomocą ON CONFLICT DO UPDATE(oficjalnej dokumentacji). W chwili pisania tego tekstu ta funkcja znajduje się obecnie w PostgreSQL 9.5 Alpha 2, który można pobrać tutaj: katalogi źródłowe Postgres .

Oto przykład, zakładając, że item_idjest to Twój klucz podstawowy:

INSERT INTO my_table
    (item_id, price)
VALUES
    (123456, 10.99)
ON
    CONFLICT (item_id)
DO UPDATE SET
    price = EXCLUDED.price

Oryginalny post ...

Oto implementacja, do której doszedłem, chcąc uzyskać wgląd w to, czy wstawka lub aktualizacja miała miejsce.

Definicja upsert_datapolega na konsolidacji wartości w jednym zasobie zamiast konieczności dwukrotnego podawania ceny i item_id: raz dla aktualizacji, ponownie dla wkładki.

WITH upsert_data AS (
    SELECT
    '19.99'::numeric(10,2) AS price,
    'abcdefg'::character varying AS item_id
),
update_outcome AS (
    UPDATE pricing_tbl
    SET price = upsert_data.price
    FROM upsert_data
    WHERE pricing_tbl.item_id = upsert_data.item_id
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        upsert_data.price AS price,
        upsert_data.item_id AS item_id
    FROM upsert_data
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome

Jeśli nie podoba ci się użycie upsert_data, oto alternatywna implementacja:

WITH update_outcome AS (
    UPDATE pricing_tbl
    SET price = '19.99'
    WHERE pricing_tbl.item_id = 'abcdefg'
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        '19.99' AS price,
        'abcdefg' AS item_id
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome
Joshua Burns
źródło
Jak to działa?
JB.
1
@jb. nie tak dobrze jak bym chciał. Zobaczysz znaczące kary wydajnościowe w porównaniu do wykonywania prostych wstawek. Jednak w przypadku mniejszych partii (powiedzmy 1000 lub mniej) ten przykład powinien działać dobrze.
Joshua Burns
0

Dzięki temu dowiesz się, czy wstawienie lub aktualizacja nastąpiła:

with "update_items" as (
  -- Update statement here
  update items set price = 3499, name = 'Uncle Bob'
  where id = 1 returning *
)
-- Insert statement here
insert into items (price, name)
-- But make sure you put your values like so
select 3499, 'Uncle Bob'
where not exists ( select * from "update_items" );

Jeśli nastąpi aktualizacja, otrzymasz wstawkę 0, w przeciwnym razie wstaw 1 lub błąd.

John Fawcett
źródło