Manera idiomática de implementar UPSERT en PostgreSQL


40

He leído sobre diferentes UPSERTimplementaciones en PostgreSQL, pero todas estas soluciones son relativamente antiguas o relativamente exóticas (usando CTE grabable , por ejemplo).

Y simplemente no soy un experto en psql para averiguar de inmediato, si estas soluciones son viejas porque son bien recomendadas o si son (bueno, casi todas) son solo ejemplos de juguetes que no son apropiados para el uso de producción.

¿Cuál es la forma más segura de subprocesos para implementar UPSERT en PostgreSQL?

Respuestas:


23

PostgreSQL ahora tiene UPSERT .


El método preferido según una pregunta similar de StackOverflow es actualmente el siguiente:

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

77
Prefiero usar un CTE grabable: stackoverflow.com/a/8702291/330315
a_horse_with_no_name

¿Cuál es la ventaja de un CTE grabable frente a una función?
François Beausoleil

1
@ François por un lado, la velocidad. Usando un CTE, golpeas la base de datos una vez. Haciéndolo de esta manera podría golpearlo dos o más veces. Además, el optimizador no puede optimizar los procedimientos pl / pgsql de manera tan eficiente como el código SQL puro.
Adam Mackler el

1
@ François Por otra parte, la concurrencia. Dado que el ejemplo anterior tiene múltiples sentencias SQL, debe preocuparse por las condiciones de carrera (el motivo del bucle klugey). Una sola declaración SQL será atómica. Ver este enlace
Adam Mackler el

1
@ FrançoisBeausoleil vea aquí y aquí por qué. Básicamente, sin un ciclo de reintento, tienes que serializar o tienes la posibilidad de fallas debido a la condición de carrera inherente.
Jack Douglas

27

ACTUALIZACIÓN (2015-08-20):

Ahora hay una implementación oficial para el manejo de upserts mediante el uso de ON CONFLICT DO UPDATE(documentación oficial). Al momento de escribir este artículo, esta característica actualmente reside en PostgreSQL 9.5 Alpha 2, que está disponible para descargar aquí: directorios fuente de Postgres .

Aquí hay un ejemplo, suponiendo que item_idsea ​​su clave principal:

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

Publicación original ...

Aquí hay una implementación a la que llegué cuando deseaba obtener visibilidad sobre si se produjo una inserción o actualización.

La definición de upsert_dataes consolidar los valores en un solo recurso, en lugar de tener que especificar el precio y item_id dos veces: una vez para la actualización, otra vez para la inserción.

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 no le gusta el uso de upsert_data, aquí hay una implementación 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

¿Cómo funciona?
jb.

1
@jb. No tan bien como me gustaría. Vas a ver penalizaciones de rendimiento significativas vs. realizar inserciones rectas. Sin embargo, para lotes más pequeños (digamos 1000 o menos), este ejemplo debería funcionar bien.
Joshua Burns

0

Esto le permitirá saber si se realizó la inserción o actualización:

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 se produce la actualización, obtendrá una inserción 0; de lo contrario, inserte 1 o un error.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.