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

40

Я читал о различных UPSERTреализациях в PostgreSQL, но все эти решения относительно старые или относительно экзотические (например, с использованием записываемого CTE ).

И я просто не являюсь экспертом в psql, чтобы сразу выяснить, устарели ли эти решения, потому что они хорошо рекомендованы, или они (ну, почти все они) являются просто игрушечными примерами, не подходящими для промышленного использования.

Какой самый потокобезопасный способ реализовать UPSERT в PostgreSQL?

shabunc
источник

Ответы:

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
Я бы предпочел использовать CTE с возможностью записи: stackoverflow.com/a/8702291/330315
a_horse_with_no_name
В чем преимущество записываемого CTE перед функцией?
Франсуа Босолей
1
@ Во-первых, Франсуа, скорость. Используя CTE, вы попадаете в базу данных один раз. Делая это таким образом, вы можете ударить его два или более раз. Кроме того, оптимизатор не может оптимизировать процедуры pl / pgsql так же эффективно, как чистый SQL-код.
Адам Маклер
1
@ Франсуа Во-вторых, параллелизм. Поскольку в приведенном выше примере есть несколько операторов SQL, вам нужно беспокоиться о состояниях гонки (причина цикла Клюги). Один оператор SQL будет атомарным. Смотрите эту ссылку
Адам Маклер
1
@ FrancoisBeausoleil смотрите здесь и здесь, почему. По сути, без повторной попытки вы либо должны сериализоваться, либо у вас есть вероятность сбоев из-за состояния гонки.
Джек Дуглас
27

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

В настоящее время существует официальная реализация для обработки upserts посредством использования 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 или ошибка.

Джон Фосетт
источник