Méthode idiomatique d'implémenter UPSERT dans PostgreSQL

40

J'ai lu différentes UPSERTimplémentations de PostgreSQL, mais toutes ces solutions sont relativement anciennes ou relativement exotiques (en utilisant le CTE inscriptible , par exemple).

Et je ne suis tout simplement pas un expert en psql pour savoir immédiatement si ces solutions sont anciennes, car elles sont bien recommandées ou (bien, presque toutes) sont simplement des exemples de jouets qui ne conviennent pas à une utilisation en production.

Quel est le moyen le plus sûr pour implémenter UPSERT dans PostgreSQL?

shabunc
la source

Réponses:

23

PostgreSQL a maintenant UPSERT .


La méthode préférée selon une question StackOverflow similaire est actuellement la suivante:

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
la source
7
Je préférerais utiliser un CTE inscriptible: stackoverflow.com/a/8702291/330315
a_horse_with_no_name
Quel est l'avantage d'un CTE inscriptible par rapport à une fonction?
François Beausoleil
1
@ François pour une chose, la vitesse. En utilisant un CTE, vous frappez la base de données une fois. En le faisant de cette façon, vous pourriez le frapper deux fois ou plus. En outre, l'optimiseur ne peut optimiser les procédures pl / pgsql aussi efficacement que du code SQL pur.
Adam Mackler
1
@ François D'autre part, la concurrence. Étant donné que l'exemple ci-dessus comporte plusieurs instructions SQL, vous devez vous préoccuper des conditions de concurrence (raison de la boucle klugey). Une seule instruction SQL sera atomique. Voir ce lien
Adam Mackler
1
@ FrançoisBeausoleil voir ici et ici pour pourquoi. Fondamentalement, sans boucle de réessai, vous devez soit sérialiser, soit vous avez le risque d'échecs en raison de la condition de concurrence inhérente.
Jack Douglas
27

MISE À JOUR (2015-08-20):

Il existe maintenant une implémentation officielle pour la gestion des upserts via ON CONFLICT DO UPDATE(documentation officielle). Au moment de la rédaction de cet article, cette fonctionnalité réside actuellement dans PostgreSQL 9.5 Alpha 2, disponible au téléchargement ici: Répertoires sources de Postgres .

Voici un exemple, en supposant que item_idsoit votre clé primaire:

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

Original Post ...

Voici une implémentation à laquelle je suis arrivé lorsque je souhaitais avoir une visibilité sur la présence ou non d'une insertion ou d'une mise à jour.

La définition de upsert_data consiste à consolider les valeurs dans une seule ressource, plutôt que de devoir spécifier le prix et l'id_liste deux fois: une fois pour la mise à jour, à nouveau pour l'insertion.

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

Si vous n'aimez pas l'utilisation de upsert_data, voici une autre implémentation:

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
la source
Comment ça marche?
jb.
1
@jb. pas aussi bien que je le voudrais. Vous allez voir d'importantes pénalités de performance par rapport aux insertions droites. Cependant, pour des lots plus petits (par exemple 1000 ou moins), cet exemple devrait parfaitement fonctionner.
Joshua Burns
0

Cela vous permettra de savoir si l'insertion ou la mise à jour s'est produite:

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

Si la mise à jour se produit, vous obtiendrez un insert 0, sinon insérez 1 ou une erreur.

John Fawcett
la source