PostgreSQLにUPSERTを実装する慣用的な方法


40

UPSERTPostgreSQLのさまざまな実装について読んだことがありますが、これらのソリューションはすべて比較的古く、または比較的エキゾチックです(たとえば、書き込み可能なCTEを使用)。

そして、これらのソリューションが推奨されているため古くなっているのか、それとも(ほとんどすべてがそうである)実稼働での使用に適さない単なるおもちゃの例であるのかをすぐに調べるのは、psqlの専門家ではありません。

PostgreSQLでUPSERTを実装する最もスレッドセーフな方法は何ですか?

回答:


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
@Françoisの1つは、スピードです。CTEを使用すると、データベースに1回ヒットします。このようにすると、2回以上ヒットする可能性があります。また、オプティマイザは、純粋なSQLコードほど効率的にpl / pgsqlプロシージャを最適化できません。
アダムマックラー

1
@François別のこと、並行性。上記の例には複数のSQLステートメントがあるため、競合状態(klugeyループの理由)を心配する必要があります。単一のSQLステートメントはアトミックです。このリンクを
アダムマックラー

1
@FrançoisBeausoleil 理由については、ここここを参照してください。基本的に再試行ループなしでは、シリアル化する必要があるか、固有の競合状態が原因で障害が発生する可能性があります。
ジャックダグラス14

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を2回指定するのではなく、値を単一のリソースに統合することです。更新のために1回、挿入のために1回。

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以下)の場合、この例は問題なく機能します。
ジョシュアバーンズ14年

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またはエラーが挿入されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.