Modo idiomatico per implementare UPSERT in PostgreSQL


40

Ho letto di diverse UPSERTimplementazioni in PostgreSQL, ma tutte queste soluzioni sono relativamente vecchie o relativamente esotiche (usando CTE scrivibile ad esempio ).

E non sono affatto un esperto di psql per scoprire immediatamente se queste soluzioni sono vecchie perché sono ben raccomandate o sono (beh, quasi tutte sono) solo esempi di giocattoli non adatti all'uso in produzione.

Qual è il modo più sicuro per implementare UPSERT in PostgreSQL?

Risposte:


23

PostgreSQL ora ha UPSERT .


Il metodo preferito secondo una domanda StackOverflow simile è attualmente il seguente:

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');

7
Preferisco utilizzare un CTE scrivibile: stackoverflow.com/a/8702291/330315
a_horse_with_no_name

Qual è il vantaggio di un CTE scrivibile rispetto a una funzione?
François Beausoleil,

1
@ François per prima cosa, velocità. Usando un CTE hai colpito il database una volta. In questo modo potresti colpirlo due o più volte. Inoltre, l'ottimizzatore non può ottimizzare le procedure pl / pgsql con la stessa efficienza del codice SQL puro.
Adam Mackler,

1
@ François Per un'altra cosa, la concorrenza. Poiché l'esempio sopra ha più istruzioni SQL, devi preoccuparti delle condizioni di gara (il motivo del ciclo klugey). Una singola istruzione SQL sarà atomica. Vedi questo link
Adam Mackler,

1
@ FrançoisBeausoleil vedi qui e qui per il perché. Fondamentalmente senza un ciclo di riprovare, devi serializzare o hai la possibilità di guasti dovuti alle condizioni di gara intrinseche.
Jack Douglas,

27

AGGIORNAMENTO (20-08-2015):

Ora esiste un'implementazione ufficiale per la gestione degli UPS tramite l'uso di ON CONFLICT DO UPDATE(documentazione ufficiale). Al momento della stesura di questo documento, questa funzione risiede attualmente in PostgreSQL 9.5 Alpha 2, che è disponibile per il download qui: directory sorgente di Postgres .

Ecco un esempio, supponendo che item_idsia la chiave primaria:

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

Posta originale ...

Ecco un'implementazione a cui sono arrivato quando desideravo ottenere visibilità sull'inserimento o l'aggiornamento.

La definizione di upsert_dataè consolidare i valori in una singola risorsa, anziché dover specificare il prezzo e item_id due volte: una volta per l'aggiornamento, di nuovo per l'inserimento.

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

Se non ti piace l'uso di upsert_data, ecco un'implementazione alternativa:

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

Come si esibisce?
jb.

1
@jb. non come vorrei. Vedrai delle penalità di prestazione significative contro l'esecuzione di inserti diritti. Tuttavia, per lotti più piccoli (diciamo 1000 o meno), questo esempio dovrebbe funzionare bene.
Joshua Burns,

0

Questo ti farà sapere se l'inserimento o l'aggiornamento sono avvenuti:

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" );

Se si verifica l'aggiornamento, otterrai un insert 0, altrimenti inserisci 1 o un errore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.