Sisipkan, pada pembaruan duplikat di PostgreSQL?

645

Beberapa bulan yang lalu saya belajar dari jawaban di Stack Overflow bagaimana melakukan beberapa pembaruan sekaligus di MySQL menggunakan sintaks berikut:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

Saya sekarang telah beralih ke PostgreSQL dan ternyata ini tidak benar. Ini merujuk pada semua tabel yang benar jadi saya menganggap itu masalah kata kunci yang berbeda yang digunakan, tetapi saya tidak yakin di mana dalam dokumentasi PostgreSQL ini dibahas.

Untuk memperjelas, saya ingin memasukkan beberapa hal dan jika sudah ada untuk memperbaruinya.

Teifion
sumber
38
Siapa pun yang menemukan pertanyaan ini harus membaca artikel Depesz, "Mengapa upert begitu rumit?" . Ini menjelaskan masalah dan kemungkinan solusi dengan sangat baik.
Craig Ringer
8
UPSERT akan ditambahkan dalam Postgres 9.5: wiki.postgresql.org/wiki/…
tommed
4
@tommed - telah dilakukan: stackoverflow.com/a/34639631/4418
warren

Jawaban:

515

PostgreSQL sejak versi 9.5 memiliki sintaks UPSERT , dengan klausa ON CONFLICT . dengan sintaks berikut (mirip dengan MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

Mencari arsip grup email postgresql untuk "upert" mengarah ke menemukan contoh melakukan apa yang mungkin ingin Anda lakukan, dalam manual :

Contoh 38-2. Pengecualian dengan UPDATE / INSERT

Contoh ini menggunakan penanganan pengecualian untuk melakukan UPDATE atau INSERT, jika perlu:

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
        -- note that "a" must be unique
        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');

Mungkin ada contoh bagaimana melakukan ini secara massal, menggunakan CTE di 9.1 dan di atas, di milis peretas :

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

Lihat jawaban a_horse_with_no_name untuk contoh yang lebih jelas.

Stephen Denne
sumber
7
Satu-satunya hal yang saya tidak suka tentang ini adalah bahwa itu akan jauh lebih lambat, karena setiap upsert akan menjadi panggilan individu ke dalam database.
baash05
@ baash05 mungkin ada cara untuk melakukannya secara massal, lihat jawaban saya yang diperbarui.
Stephen Denne
2
Satu-satunya hal yang saya lakukan secara berbeda adalah menggunakan UNTUK 1..2 LOOP bukan hanya LOOP sehingga jika beberapa kendala unik lainnya dilanggar, itu tidak akan berputar tanpa batas.
olamork
2
Apa yang dimaksud excludeddengan solusi pertama di sini?
ichbinallen
2
@ichbinallen dalam dokumen klausa SET dan WHERE dalam ON CONFLICT DO UPDATE memiliki akses ke baris yang ada menggunakan nama tabel (atau alias), dan ke baris yang diajukan untuk dimasukkan menggunakan tabel khusus yang dikecualikan . Dalam hal ini, excludedtabel khusus memberi Anda akses ke nilai yang Anda coba INSERT.
TMichel
429

Peringatan: ini tidak aman jika dijalankan dari beberapa sesi secara bersamaan (lihat peringatan di bawah).


Cara pintar lain untuk melakukan "UPSERT" di postgresql adalah dengan melakukan dua pernyataan UPDATE / INSERT berurutan yang masing-masing dirancang untuk berhasil atau tidak berpengaruh.

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

UPDATE akan berhasil jika baris dengan "id = 3" sudah ada, jika tidak maka tidak akan berpengaruh.

INSERT hanya akan berhasil jika baris dengan "id = 3" tidak ada.

Anda dapat menggabungkan keduanya menjadi satu string dan menjalankan keduanya dengan mengeksekusi pernyataan SQL tunggal dari aplikasi Anda. Sangat disarankan untuk menjalankannya bersama dalam satu transaksi.

Ini bekerja dengan sangat baik ketika dijalankan dalam isolasi atau di atas meja yang terkunci, tetapi tunduk pada kondisi balapan yang berarti ia mungkin masih gagal dengan kesalahan kunci duplikat jika sebuah baris dimasukkan secara bersamaan, atau mungkin berakhir tanpa baris yang dimasukkan ketika sebuah baris dihapus bersamaan. . SEBUAHSERIALIZABLE transaksi pada PostgreSQL 9.1 atau lebih tinggi akan menanganinya dengan andal dengan biaya tingkat kegagalan serialisasi yang sangat tinggi, artinya Anda harus banyak mencoba. Lihat mengapa begitu rumit , yang membahas kasus ini secara lebih rinci.

Pendekatan ini juga tunduk pada pembaruan yang hilang secara read committedterpisah kecuali jika aplikasi memeriksa jumlah baris yang terpengaruh dan memverifikasi bahwa salah satu insertatau baris yang updateterpengaruh .

termasuk keluarga sapi
sumber
6
Jawaban singkat: jika catatan ada, INSERT tidak melakukan apa pun. Jawaban panjang: PILIH dalam INSERT akan mengembalikan hasil sebanyak ada kecocokan dari klausa di mana. Itu paling banyak satu (jika nomor satu tidak ada dalam hasil sub-pilih), jika tidak nol. INSERT akan menambahkan satu atau nol baris.
Peter Becker
3
bagian 'di mana' dapat disederhanakan dengan menggunakan ada:... where not exists (select 1 from table where id = 3);
Endy Tjahjono
1
ini harus menjadi jawaban yang tepat .. dengan beberapa perubahan kecil, ini dapat digunakan untuk melakukan pembaruan massal .. Humm .. Saya ingin tahu apakah tabel temp dapat digunakan ..
baash05
1
@keaplogik, batasan 9,1 tersebut adalah dengan CTE yang dapat ditulis (ekspresi tabel umum) yang dijelaskan dalam jawaban lain. Sintaks yang digunakan dalam jawaban ini sangat mendasar dan telah lama didukung.
bovine
8
Peringatan, ini read committeddapat menyebabkan pembaruan yang hilang secara terpisah kecuali jika aplikasi Anda memeriksa untuk memastikan bahwa insertatau updatememiliki jumlah baris yang tidak nol. Lihat dba.stackexchange.com/q/78510/7788
Craig Ringer
227

Dengan PostgreSQL 9.1 ini dapat dicapai menggunakan CTE yang dapat ditulis ( ekspresi tabel umum ):

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

Lihat entri blog ini:


Perhatikan bahwa solusi ini tidak mencegah pelanggaran kunci unik tetapi tidak rentan terhadap pembaruan yang hilang.
Lihat tindak lanjut oleh Craig Ringer di dba.stackexchange.com

seekor kuda tanpa nama
sumber
1
@ FrançoisBeausoleil: kemungkinan kondisi balapan jauh lebih kecil daripada dengan pendekatan "coba /
pakai
2
@a_horse_with_no_name Bagaimana maksud Anda bahwa peluang pada kondisi lomba jauh lebih kecil? Ketika saya menjalankan kueri ini bersamaan dengan catatan yang sama, saya mendapatkan kesalahan "nilai kunci duplikat melanggar batasan unik" 100% dari waktu sampai kueri mendeteksi bahwa catatan telah dimasukkan. Apakah ini contoh lengkap?
Jeroen van Dijk
4
@a_horse_with_no_name Solusi Anda tampaknya berfungsi dalam situasi bersamaan ketika Anda membungkus pernyataan upsert dengan kunci berikut: BEGIN WORK; MEJA LOCK mytable DI SHARE ROW MODE EKSKLUSIF; <UPSERT HERE>; KOMIT KERJA;
Jeroen van Dijk
2
@ JoeroenvanDijk: terima kasih. Yang saya maksud dengan "jauh lebih kecil" adalah bahwa jika beberapa transaksi untuk ini (dan melakukan perubahan!) Rentang waktu antara pembaruan dan sisipan lebih kecil karena semuanya hanya satu pernyataan. Anda selalu dapat menghasilkan pelanggaran pk oleh dua pernyataan INSERT independen. Jika Anda mengunci seluruh tabel, Anda membuat serialisasi semua akses secara efektif (sesuatu yang bisa Anda capai dengan tingkat isolasi serializable juga).
a_horse_with_no_name
12
Solusi ini dapat mengalami pembaruan yang hilang jika transaksi memasukkan kembali; tidak ada pemeriksaan untuk memastikan bahwa UPDATEbaris yang terpengaruh terpengaruh.
Craig Ringer
132

Di PostgreSQL 9.5 dan yang lebih baru, Anda dapat menggunakan INSERT ... ON CONFLICT UPDATE .

Lihat dokumentasi .

MySQL INSERT ... ON DUPLICATE KEY UPDATEdapat secara langsung diulang menjadi ON CONFLICT UPDATE. Sintaks standar-SQL juga tidak, keduanya merupakan ekstensi khusus basis data. Ada alasan bagus MERGEyang tidak digunakan untuk ini , sintaks baru tidak dibuat hanya untuk bersenang-senang. (Sintaks MySQL juga memiliki masalah yang berarti tidak diadopsi secara langsung).

mis. pengaturan yang diberikan:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

permintaan MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

menjadi:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

Perbedaan:

  • Anda harus menentukan nama kolom (atau nama kendala unik) yang akan digunakan untuk pemeriksaan keunikan. Itu adalahON CONFLICT (columnname) DO

  • Kata kunci SETharus digunakan, seolah-olah ini adalah UPDATEpernyataan normal

Ini memiliki beberapa fitur bagus juga:

  • Anda dapat memiliki WHEREklausa pada Anda UPDATE(membiarkan Anda secara efektif berubah ON CONFLICT UPDATEmenjadi ON CONFLICT IGNOREuntuk nilai-nilai tertentu)

  • Nilai usulan untuk penyisipan tersedia sebagai variabel baris EXCLUDED, yang memiliki struktur yang sama dengan tabel target. Anda bisa mendapatkan nilai asli di tabel dengan menggunakan nama tabel. Jadi dalam hal ini EXCLUDED.cakan 10(karena itulah yang kami coba masukkan) dan "table".cakan 3karena itulah nilai saat ini dalam tabel. Anda dapat menggunakan salah satu atau keduanya dalam SETekspresi dan WHEREklausa.

Untuk latar belakang tentang upsert, lihat Cara UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) di PostgreSQL?

Craig Ringer
sumber
Saya telah melihat ke dalam solusi 9,5 PostgreSQL seperti yang Anda jelaskan di atas karena saya mengalami kesenjangan di bidang kenaikan otomatis saat di bawah MySQL ON DUPLICATE KEY UPDATE. Saya telah mengunduh Postgres 9.5 dan menerapkan kode Anda, tetapi anehnya masalah yang sama terjadi di bawah Postgres: bidang serial primary key tidak berurutan (ada celah antara sisipan dan pembaruan.). Adakah yang tahu apa yang terjadi di sini? Apakah ini normal? Adakah yang tahu bagaimana cara menghindari perilaku ini? Terima kasih.
WM
@ WM Itu cukup banyak melekat pada operasi yang upert. Anda harus mengevaluasi fungsi yang menghasilkan urutan sebelum mencoba memasukkan. Karena sekuens semacam itu dirancang untuk beroperasi secara bersamaan, mereka dikecualikan dari semantik transaksi normal, tetapi bahkan jika mereka bukan generasi tidak dipanggil dalam subtransaksi dan digulirkan kembali, itu menyelesaikan secara normal dan melakukan dengan sisa operasi. Jadi ini akan terjadi bahkan dengan implementasi urutan "tanpa celah". Satu-satunya cara DB dapat menghindari hal ini adalah dengan menunda evaluasi pembuatan urutan sampai setelah pemeriksaan kunci.
Craig Ringer
1
@ WM yang akan menciptakan masalah sendiri. Pada dasarnya, Anda mandek. Tetapi jika Anda mengandalkan serial / auto_increment sebagai gapless, Anda sudah memiliki bug. Anda dapat memiliki kesenjangan urutan karena rollback termasuk kesalahan sementara - reboot saat beban, kesalahan transaksi tengah, crash, dll. Anda tidak boleh pernah bergantung pada SERIAL/ SEQUENCEatau AUTO_INCREMENTtidak memiliki kesenjangan. Jika Anda membutuhkan urutan tanpa celah mereka lebih kompleks; Anda perlu menggunakan meja penghitung biasanya. Google akan memberi tahu Anda lebih banyak. Namun berhati-hatilah, sekuens gapless mencegah semua memasukkan konkurensi.
Craig Ringer
@WM Jika Anda benar-benar membutuhkan urutan tanpa celah dan upert, Anda bisa menggunakan pendekatan kenaikan berdasarkan fungsi yang dibahas dalam manual bersama dengan implementasi urutan tanpa celah yang menggunakan tabel counter. Karena proses BEGIN ... EXCEPTION ...dalam subtransaksi yang dibatalkan karena kesalahan, kenaikan urutan Anda akan dibatalkan jika INSERTgagal.
Craig Ringer
Terima kasih banyak @Craig Ringer, itu cukup informatif. Saya menyadari bahwa saya cukup menyerah untuk memiliki kunci primer kenaikan otomatis. Saya membuat primer gabungan dari 3 bidang dan untuk kebutuhan saya saat ini, benar-benar tidak perlu untuk bidang kenaikan otomatis tanpa celah. Sekali lagi terima kasih, informasi yang Anda berikan akan menghemat waktu saya di masa depan untuk mencoba mencegah perilaku DB yang alami dan sehat. Saya mengerti lebih baik sekarang.
WM
17

Saya sedang mencari hal yang sama ketika saya datang ke sini, tetapi kurangnya fungsi "upsert" generik sedikit mengganggu saya, jadi saya pikir Anda bisa melewati pembaruan dan memasukkan sql sebagai argumen pada fungsi yang membentuk manual

akan terlihat seperti ini:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

dan mungkin untuk melakukan apa yang awalnya ingin Anda lakukan, batch "upsert", Anda bisa menggunakan Tcl untuk membagi sql_update dan loop pembaruan individu, hit dalm kinerja akan sangat kecil lihat http://archives.postgresql.org/pgsql- performance / 2006-04 / msg00557.php

biaya tertinggi adalah mengeksekusi kueri dari kode Anda, di sisi database biaya eksekusi jauh lebih kecil

Paul Scheltema
sumber
3
Anda masih harus menjalankan ini dalam coba lagi dan itu cenderung untuk balapan dengan bersamaan DELETEkecuali Anda mengunci tabel atau berada dalam SERIALIZABLEisolasi transaksi pada PostgreSQL 9.1 atau lebih besar.
Craig Ringer
13

Tidak ada perintah sederhana untuk melakukannya.

Pendekatan yang paling benar adalah dengan menggunakan fungsi, seperti yang dari dokumen .

Solusi lain (meskipun tidak aman) adalah melakukan pembaruan dengan mengembalikan, memeriksa baris mana yang diperbarui, dan menyisipkan sisanya

Sesuatu di sepanjang garis:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

dengan asumsi id: 2 dikembalikan:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

Tentu saja itu akan menebus cepat atau lambat (di lingkungan bersamaan), karena ada kondisi balapan yang jelas di sini, tetapi biasanya itu akan berhasil.

Inilah artikel yang lebih panjang dan lebih komprehensif tentang topik ini .

Craig Ringer
sumber
1
Jika menggunakan opsi ini, pastikan untuk memeriksa bahwa id dikembalikan bahkan jika pembaruan tidak melakukan apa-apa. Saya telah melihat database mengoptimalkan kueri seperti "Perbarui tabel foo set bar = 4 di mana bar = 4".
thelem
10

Secara pribadi, saya telah membuat "aturan" yang terlampir pada pernyataan insert. Katakanlah Anda memiliki tabel "dns" yang mencatat hit dns per pelanggan berdasarkan per-waktu:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Anda ingin dapat memasukkan kembali baris dengan nilai yang diperbarui, atau membuatnya jika belum ada. Mengetik pada customer_id dan waktu. Sesuatu seperti ini:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

Pembaruan: Ini berpotensi gagal jika memasukkan secara bersamaan, karena akan menghasilkan pengecualian unique_violation. Namun, transaksi yang tidak dihentikan akan berlanjut dan berhasil, dan Anda hanya perlu mengulangi transaksi yang dihentikan.

Namun, jika ada banyak sisipan yang terjadi sepanjang waktu, Anda harus meletakkan kunci meja di sekitar pernyataan penyisipan: SHARE ROW EKSKLUSIF mengunci akan mencegah operasi apa pun yang bisa menyisipkan, menghapus atau memperbarui baris di tabel target Anda. Namun, pembaruan yang tidak memperbarui kunci unik aman, jadi jika Anda tidak melakukan operasi akan melakukan ini, gunakan kunci penasihat sebagai gantinya.

Selain itu, perintah COPY tidak menggunakan ATURAN, jadi jika Anda memasukkan dengan COPY, Anda harus menggunakan pemicu.

Ch'marr
sumber
9

Saya menggunakan fungsi ini menggabungkan

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql
Mise
sumber
1
Lebih efisien hanya dengan melakukan yang updatepertama dan kemudian memeriksa jumlah baris yang diperbarui. (Lihat jawaban Ahmad)
a_horse_with_no_name
8

Saya kustom fungsi "upsert" di atas, jika Anda ingin menyisipkan DAN MENGGANTI:

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

Dan setelah dieksekusi, lakukan sesuatu seperti ini:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

Sangat penting untuk menempatkan koma dolar ganda untuk menghindari kesalahan kompiler

  • periksa kecepatan ...
Felipe FMMobile
sumber
7

Mirip dengan jawaban yang paling disukai, tetapi bekerja sedikit lebih cepat:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(sumber: http://www.the-art-of-web.com/sql/upsert/ )

alexkovelsky
sumber
3
Ini akan gagal jika dijalankan secara bersamaan dalam dua sesi, karena tidak ada pembaruan akan melihat baris yang ada sehingga kedua pembaruan akan mencapai nol baris, sehingga kedua pertanyaan akan mengeluarkan sisipan.
Craig Ringer
6

Saya memiliki masalah yang sama untuk mengelola pengaturan akun sebagai pasangan nilai nama. Kriteria desain adalah bahwa klien yang berbeda dapat memiliki set pengaturan yang berbeda.

Solusi saya, mirip dengan JWP adalah menghapus dan mengganti secara massal, menghasilkan catatan gabungan dalam aplikasi Anda.

Ini cukup antipeluru, platform independen dan karena tidak pernah ada lebih dari sekitar 20 pengaturan per klien, ini hanya 3 panggilan db beban yang cukup rendah - mungkin metode tercepat.

Alternatif memperbarui setiap baris - memeriksa pengecualian kemudian memasukkan - atau kombinasi dari kode yang mengerikan, lambat dan sering rusak karena (seperti yang disebutkan di atas) penanganan pengecualian non standar SQL berubah dari db ke db - atau bahkan rilis untuk dirilis.

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION
benno
sumber
Selamat datang di SO. Pengantar yang bagus! :-)
Don Question
1
Ini lebih seperti REPLACE INTOdaripada INSERT INTO ... ON DUPLICATE KEY UPDATE, yang dapat menyebabkan masalah jika Anda menggunakan pemicu. Anda pada akhirnya akan menjalankan hapus dan masukkan pemicu / aturan, bukan yang diperbarui.
cao
5

Menurut dokumentasi INSERTpernyataan PostgreSQL , penanganan ON DUPLICATE KEYkasus tidak didukung. Itu bagian dari sintaks adalah ekstensi MySQL milik.

Christian Hang-Hicks
sumber
@Lucian MERGEjuga benar-benar lebih dari operasi OLAP; lihat stackoverflow.com/q/17267417/398670 untuk penjelasannya. Itu tidak mendefinisikan semantik konkurensi dan kebanyakan orang yang menggunakannya untuk upert hanya membuat bug.
Craig Ringer
5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT
Ahmad
sumber
5

Untuk menggabungkan set kecil, menggunakan fungsi di atas baik-baik saja. Namun, jika Anda menggabungkan data dalam jumlah besar, saya sarankan melihat ke http://mbk.projects.postgresql.org

Praktik terbaik saat ini yang saya ketahui adalah:

  1. SALINKAN data baru / diperbarui ke dalam tabel temp (pasti, atau Anda dapat melakukan INSERT jika biayanya ok)
  2. Acquire Lock [opsional] (saran lebih baik daripada kunci meja, IMO)
  3. Menggabungkan. (bagian yang menyenangkan)
jwp
sumber
5

UPDATE akan mengembalikan jumlah baris yang dimodifikasi. Jika Anda menggunakan JDBC (Java), Anda dapat memeriksa nilai ini terhadap 0 dan, jika tidak ada baris yang terpengaruh, jalankan INSERT. Jika Anda menggunakan bahasa pemrograman lain, mungkin jumlah baris yang dimodifikasi masih dapat diperoleh, periksa dokumentasi.

Ini mungkin tidak elegan, tetapi Anda memiliki SQL sederhana yang lebih sepele untuk digunakan dari kode panggilan. Secara berbeda, jika Anda menulis skrip sepuluh baris dalam PL / PSQL, Anda mungkin harus memiliki unit test dari satu atau jenis lain hanya untuk itu saja.

Audrius Meskauskas
sumber
4

Sunting: Ini tidak berfungsi seperti yang diharapkan. Tidak seperti jawaban yang diterima, ini menghasilkan pelanggaran kunci unik ketika dua proses berulang kali memanggilupsert_foo secara bersamaan.

Eureka! Saya menemukan cara untuk melakukannya dalam satu permintaan: gunakan UPDATE ... RETURNINGuntuk menguji apakah ada baris yang terpengaruh:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

The UPDATEharus dilakukan dalam prosedur yang terpisah karena, sayangnya, ini adalah kesalahan sintaks:

... WHERE NOT EXISTS (UPDATE ...)

Sekarang berfungsi sesuai keinginan:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
Joey Adams
sumber
1
Anda dapat menggabungkannya menjadi satu pernyataan jika Anda menggunakan CTE yang dapat ditulis. Tetapi seperti kebanyakan solusi yang diposting di sini, yang ini salah dan akan gagal di hadapan pembaruan bersamaan.
Craig Ringer