Ідіоматичний спосіб реалізації UPSERT в PostgreSQL


40

Я читав про різні UPSERTреалізації в PostgreSQL, але всі ці рішення є відносно старими або відносно екзотичними (наприклад, використовуючи CTE , що записується ).

І я просто не фахівець з psql, щоб відразу дізнатися, чи є ці рішення старими, оскільки вони добре рекомендуються, або вони (ну, майже всі вони є) лише приклади іграшок, не підходящих для використання у виробництві.

Який найбільш безпечний для потоків спосіб впровадити UPSERT в PostgreSQL?

Відповіді:


23

PostgreSQL тепер має UPSERT .


На даний момент кращим методом відповідно до аналогічного питання StackOverflow є:

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
Я волів би використовувати записується КТР: stackoverflow.com/a/8702291/330315
a_horse_with_no_name

У чому перевага записаного CTE перед функцією?
Франсуа Бозолей

1
@ Франсуа за одне, швидкість. Використовуючи CTE, ви один раз потрапляєте в базу даних. Зробивши це таким чином, ви можете вдарити його два чи більше разів. Крім того, оптимізатор не може оптимізувати pl / pgsql процедури так само ефективно, як чистий SQL-код.
Адам Маклер

1
@ Франсуа Інша справа, паралельність. Оскільки у вищенаведеному прикладі є кілька заяв SQL, ви повинні турбуватися про умови перегонів (причина циклу klugey). Один оператор SQL буде атомним. Дивіться це посилання
Адам Маклер

1
@ FrançoisBeausoleil дивіться тут і тут чому. В основному без циклу повторної спроби вам або доведеться серіалізувати або у вас є можливість відмов через властиву умові гонки.
Джек Дуглас

27

ОНОВЛЕННЯ (2015-08-20):

Зараз існує офіційна реалізація для поводження зі збитками через використання ON CONFLICT DO UPDATE(офіційна документація). На момент написання цієї функції ця функція наразі перебуває у PostgreSQL 9.5 Alpha 2, який можна завантажити тут: Каталоги джерел Postgres .

Ось приклад, якщо припустити, що item_idце ваш первинний ключ:

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

Оригінальна публікація ...

Ось реалізація, до якої я потрапив, бажаючи зрозуміти, чи відбулося вставлення чи оновлення.

Визначення upsert_dataполягає в об'єднанні значень в єдиний ресурс, а не вказувати ціну та item_id двічі: раз для оновлення, знову для вставки.

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

Якщо вам не подобається використання upsert_data, ось альтернативна реалізація:

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

Як це виконується?
jb.

1
@jb. не так добре, як хотілося б. Ви побачите суттєві штрафи за ефективність та виконання прямих вставок. Однак для менших партій (скажімо, 1000 або менше) цей приклад повинен бути чудовим.
Джошуа Бернс

0

Це дозволить вам знати, чи відбулося вставлення чи оновлення:

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

Якщо оновлення відбудеться, ви отримаєте вставку 0, інакше вставити 1 або помилку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.