Cara idiomatis untuk mengimplementasikan UPSERT di PostgreSQL

40

Saya sudah membaca tentang UPSERTimplementasi yang berbeda di PostgreSQL, tetapi semua solusi ini relatif lama atau relatif eksotis (menggunakan CTE yang dapat ditulis , misalnya).

Dan saya bukan ahli psql sama sekali untuk mencari tahu segera, apakah solusi ini sudah lama karena mereka direkomendasikan atau mereka (well, hampir semuanya) hanya contoh mainan yang tidak sesuai untuk penggunaan produksi.

Apa cara yang paling aman untuk menerapkan UPSERT di PostgreSQL?

shabunc
sumber

Jawaban:

23

PostgreSQL sekarang memiliki UPSERT .


Metode yang disukai berdasarkan pertanyaan StackOverflow serupa saat ini adalah sebagai berikut:

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
sumber
7
Saya lebih suka menggunakan CTE yang dapat ditulisi: stackoverflow.com/a/8702291/330315
a_horse_with_no_name
Apa keuntungan dari CTE yang dapat ditulisi vs suatu fungsi?
François Beausoleil
1
@ François untuk satu hal, kecepatan. Menggunakan CTE Anda menekan database sekali. Melakukannya dengan cara ini Anda mungkin memukulnya dua kali atau lebih. Juga, pengoptimal tidak dapat mengoptimalkan prosedur pl / pgsql seefisien kode SQL murni.
Adam Mackler
1
@ François Untuk hal lain, konkurensi. Karena contoh di atas memiliki beberapa pernyataan SQL, Anda harus khawatir tentang kondisi balapan (alasan untuk loop klugey). Pernyataan SQL tunggal akan menjadi atom. Lihat tautan ini
Adam Mackler
1
@ FrançoisBeausoleil lihat di sini dan di sini untuk alasannya. Pada dasarnya tanpa pengulangan coba, Anda harus membuat cerita bersambung atau mengalami kegagalan karena kondisi lomba yang melekat.
Jack Douglas
27

UPDATE (2015-08-20):

Sekarang ada implementasi resmi untuk menangani upert melalui penggunaan ON CONFLICT DO UPDATE(dokumentasi resmi). Pada saat penulisan ini, fitur ini saat ini berada di PostgreSQL 9.5 Alpha 2, yang tersedia untuk diunduh di sini: Direktori sumber postgres .

Berikut ini sebuah contoh, dengan asumsi item_idadalah Kunci Utama Anda:

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

Pos Asli ...

Ini adalah implementasi yang saya dapatkan ketika ingin mendapatkan visibilitas ke apakah penyisipan atau pembaruan terjadi.

Definisi upsert_dataadalah untuk mengkonsolidasikan nilai menjadi satu sumber daya, daripada harus menentukan harga dan item_id dua kali: Sekali untuk pembaruan, sekali lagi untuk memasukkan.

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

Jika Anda tidak menyukai penggunaan upsert_data, berikut ini adalah implementasi alternatif:

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
sumber
Bagaimana kinerjanya?
jb.
1
@ jb. tidak sebaik yang saya inginkan. Anda akan melihat hukuman kinerja yang signifikan vs. melakukan sisipan lurus. Namun untuk batch yang lebih kecil (katakanlah 1000 atau kurang,) contoh ini akan bekerja dengan baik.
Joshua Burns
0

Ini akan memberi tahu Anda apakah penyisipan atau pembaruan terjadi:

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

Jika pembaruan terjadi, Anda akan mendapatkan sisipan 0, jika tidak masukkan 1 atau kesalahan.

John Fawcett
sumber