Idiomatische Methode zur Implementierung von UPSERT in PostgreSQL

40

Ich habe über verschiedene UPSERTImplementierungen in PostgreSQL gelesen , aber alle diese Lösungen sind relativ alt oder relativ exotisch ( z. B. mit beschreibbarem CTE ).

Und ich bin überhaupt kein psql-Experte, der sofort herausfindet, ob diese Lösungen alt sind, weil sie gut empfohlen werden oder (na ja, fast alle von ihnen sind) nur Spielzeugbeispiele, die für den Produktionsgebrauch nicht geeignet sind.

Was ist die threadsicherste Möglichkeit, UPSERT in PostgreSQL zu implementieren?

shabunc
quelle

Antworten:

23

PostgreSQL hat jetzt UPSERT .


Die bevorzugte Methode für eine ähnliche StackOverflow-Frage ist derzeit die folgende:

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
quelle
7
Ich würde lieber einen beschreibbaren CTE verwenden: stackoverflow.com/a/8702291/330315
a_horse_with_no_name
Was ist der Vorteil eines beschreibbaren CTE gegenüber einer Funktion?
François Beausoleil
1
@Francois für eine Sache, Geschwindigkeit. Mit einem CTE haben Sie die Datenbank einmal getroffen. Wenn Sie es so machen, können Sie es zweimal oder öfter treffen. Außerdem kann der Optimierer pl / pgsql-Prozeduren nicht so effizient optimieren wie reinen SQL-Code.
Adam Mackler
1
@ François Zum anderen Nebenläufigkeit. Da das obige Beispiel mehrere SQL-Anweisungen enthält, müssen Sie sich über die Rennbedingungen Gedanken machen (der Grund für die klugey-Schleife). Eine einzelne SQL-Anweisung ist atomar. Siehe diesen Link
Adam Mackler
1
@ FrançoisBeausoleil siehe hier und hier für warum. Grundsätzlich müssen Sie ohne eine Wiederholungsschleife entweder serialisieren oder Sie haben die Möglichkeit von Fehlern aufgrund der inhärenten Racebedingung.
Jack Douglas
27

UPDATE (20.08.2015):

Es gibt jetzt eine offizielle Implementierung für die Behandlung von Upserts durch die Verwendung von ON CONFLICT DO UPDATE(offizielle Dokumentation). Zum Zeitpunkt des Schreibens befindet sich diese Funktion derzeit in PostgreSQL 9.5 Alpha 2, das hier heruntergeladen werden kann: Postgres-Quellverzeichnisse .

Hier ist ein Beispiel, vorausgesetzt, es item_idist Ihr Primärschlüssel:

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

Ursprünglicher Beitrag ...

Hier ist eine Implementierung, auf die ich gestoßen bin, um einen Überblick darüber zu erhalten, ob eine Einfügung oder Aktualisierung stattgefunden hat.

Die Definition von upsert_dataist, die Werte in einer einzelnen Ressource zu konsolidieren, anstatt den Preis und die item_id zweimal angeben zu müssen: Einmal für die Aktualisierung, noch einmal für die Einfügung.

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

Wenn Sie die Verwendung von nicht mögen upsert_data, finden Sie hier eine alternative Implementierung:

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
quelle
Wie läuft es ab?
jb.
1
@jb. nicht so gut wie ich möchte. Sie werden erhebliche Leistungseinbußen im Vergleich zur Durchführung von Straight-Inserts sehen. Bei kleineren Chargen (z. B. 1000 oder weniger) sollte dieses Beispiel jedoch einwandfrei funktionieren.
Joshua Burns
0

Hier erfahren Sie, ob das Einfügen oder Aktualisieren stattgefunden hat:

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

Wenn das Update ausgeführt wird, erhalten Sie eine Einfügung 0, andernfalls eine Einfügung 1 oder einen Fehler.

John Fawcett
quelle