Postgres 9.5 menerapkan UPSERT
. Lihat di bawah.
Postgres 9.4 atau lebih lama
Ini adalah masalah yang rumit. Anda mengalami pembatasan ini (per dokumentasi):
Dalam
VALUES
daftar yang muncul di tingkat atasINSERT
, ekspresi dapat diganti denganDEFAULT
untuk menunjukkan bahwa nilai default kolom tujuan harus dimasukkan.DEFAULT
tidak dapat digunakan saatVALUES
muncul dalam konteks lain.
Penekanan saya yang berani. Nilai default tidak ditentukan tanpa tabel untuk dimasukkan. Jadi tidak ada langsung solusi untuk pertanyaan Anda, tetapi ada beberapa kemungkinan rute alternatif, tergantung pada persyaratan yang tepat .
Ambil nilai default dari katalog sistem?
Anda bisa ambil dari katalog sistem pg_attrdef
like @Patrick berkomentar atau dari information_schema.columns
. Instruksi lengkap di sini:
- Dapatkan nilai default kolom tabel di Postgres?
Tapi kemudian Anda tetap hanya memiliki daftar baris dengan representasi teks dari ekspresi untuk memasak nilai default. Anda harus membangun dan mengeksekusi pernyataan secara dinamis untuk mendapatkan nilai untuk digunakan. Membosankan dan berantakan. Sebagai gantinya, kita dapat membiarkan fungsi Postgres bawaan melakukannya untuk kita :
Pintasan sederhana
Sisipkan baris dummy dan kembalikan untuk menggunakan default yang dihasilkan:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Masalah / cakupan solusi
- Ini hanya dijamin berfungsi untuk
STABLE
atauIMMUTABLE
ekspresi default . KebanyakanVOLATILE
fungsi akan bekerja dengan baik, tetapi tidak ada jaminan.current_timestamp
keluarga fungsi memenuhi syarat sebagai stabil, karena nilainya tidak berubah dalam suatu transaksi.
Khususnya, ini memiliki efek samping padaserial
kolom (atau gambar default lainnya dari urutan). Tapi itu seharusnya tidak menjadi masalah, karena Anda biasanya tidak menulis keserial
kolom secara langsung. Itu tidak boleh dicantumkan diINSERT
pernyataan sama sekali.
Sisa kekurangan untukserial
kolom:urutannya masih dimajukan oleh satu panggilan untuk mendapatkan baris default, menghasilkan celah dalam penomoran. Sekali lagi, itu seharusnya tidak menjadi masalah, karena kesenjangan umumnya diharapkan dalamserial
kolom.
Dua masalah lagi dapat diselesaikan:
-
Jika Anda memiliki kolom yang ditentukan
NOT NULL
, Anda harus memasukkan nilai dummy dan menggantinya denganNULL
dalam hasilnya. -
Kami sebenarnya tidak ingin menyisipkan baris tiruan . Kita bisa menghapus nanti (dalam transaksi yang sama), tapi itu mungkin memiliki lebih banyak efek samping, seperti pemicu
ON DELETE
. Ada cara yang lebih baik:
Hindari baris tiruan
Mengkloning tabel sementara termasuk kolom default dan masukkan ke itu :
BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
ON COMMIT DROP; -- drop at end of transaction
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
Hasil yang sama, efek samping yang lebih sedikit. Karena ekspresi default disalin kata demi kata, kloning mengambil dari urutan yang sama jika ada. Tetapi efek samping lain dari baris atau pemicu yang tidak diinginkan dapat dihindari sepenuhnya.
Penghargaan untuk Igor atas idenya:
- Postgresql, pilih baris "palsu"
Hapus NOT NULL
kendala
Anda harus memberikan nilai dummy untuk NOT NULL
kolom, karena (per dokumentasi):
Batasan bukan nol selalu disalin ke tabel baru.
Baik mengakomodasi mereka yang ada di INSERT
pernyataan atau (lebih baik) menghilangkan kendala:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Ada cara cepat dan kotor dengan hak pengguna super:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Ini hanya tabel sementara tanpa data dan tujuan lain, dan akan dihapus di akhir transaksi. Jadi jalan pintasnya menggoda. Namun, aturan dasarnya adalah:jangan pernah mengutak-atik katalog sistem secara langsung.
Jadi, mari kita lihat cara bersih :Otomatiskan dengan SQL dinamis dalam DO
penyataan. Anda hanya perlu hak istimewa biasa Anda dijamin memilikinya sejak peran yang sama membuat tabel sementara.
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$
Jauh lebih bersih dan masih sangat cepat. Jalankan perawatan dengan perintah dinamis dan waspadai injeksi SQL. Pernyataan ini aman. Saya telah memposting beberapa jawaban terkait dengan lebih banyak penjelasan.
Solusi umum (9.4 dan yang lebih lama)
BEGIN;
CREATE TEMP TABLE tmp_playlist_items
(LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$;
LOCK TABLE playlist_items IN EXCLUSIVE MODE; -- forbid concurrent writes
WITH default_row AS (
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
)
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES
(651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL, 21, 1, 'b', 34, 2, NULL)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
)
, upsert AS ( -- *not* replacing existing values in UPDATE (?)
UPDATE playlist_items m
SET ( playlist, item, group_name, duration, sort, legacy)
= (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
-- ..., COALESCE(n.legacy, m.legacy) -- see below
FROM new_values n
WHERE n.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items
(playlist, item, group_name, duration, sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
, COALESCE(n.legacy, d.legacy)
FROM new_values n, default_row d -- single row can be cross-joined
WHERE NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;
COMMIT;
Anda hanya perlu LOCK
jika Anda memiliki transaksi bersamaan yang mencoba menulis ke tabel yang sama.
Seperti yang diminta, ini hanya menggantikan nilai NULL di kolom legacy
di baris input untuk INSERT
kasus. Dapat dengan mudah diperluas untuk bekerja untuk kolom lain atau di UPDATE
kasus juga. Misalnya, Anda dapat UPDATE
juga secara kondisional:hanya jika nilai inputnya NOT NULL
. Saya menambahkan baris komentar ke UPDATE
di atas.
Selain:Anda tidak perlu melempar nilai di baris mana pun kecuali yang pertama dalam VALUES
ekspresi, karena tipe diturunkan dari pertama baris.
Postgres 9.5
mengimplementasikan UPSERT dengan INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Ini sangat menyederhanakan operasi:
INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT) -- !
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
= (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
, EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (..., COALESCE(l.legacy, EXCLUDED.legacy)) -- see below
RETURNING m.id;
Kami dapat melampirkan VALUES
klausa ke INSERT
secara langsung, yang memungkinkan DEFAULT
kata kunci. Dalam kasus pelanggaran unik pada (id)
, pembaruan Postgres sebagai gantinya. Kita dapat menggunakan baris yang dikecualikan di UPDATE
. Panduan:
SET
danWHERE
klausa dalamON CONFLICT DO UPDATE
memiliki akses ke baris yang ada menggunakan nama tabel (atau alias), dan ke baris yang diusulkan untuk disisipkan menggunakanexcluded
khusus tabel.
Dan:
Perhatikan bahwa efek dari semua per-baris
BEFORE INSERT
pemicu dicerminkan dalam nilai yang dikecualikan, karena efek tersebut mungkin telah menyebabkan baris dikecualikan dari penyisipan.
Sisa casing sudut
Anda memiliki berbagai opsi untuk UPDATE
:Anda bisa ...
- ... tidak memperbarui sama sekali:tambahkan
WHERE
klausa keUPDATE
untuk hanya menulis ke baris yang dipilih. - ... hanya perbarui kolom yang dipilih.
- ... hanya perbarui jika kolom saat ini NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... hanya perbarui jika nilai baru adalah
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Tetapi tidak ada cara untuk membedakan DEFAULT
nilai dan nilai yang sebenarnya disediakan di INSERT
. Hanya menghasilkan EXCLUDED
baris terlihat. Jika Anda membutuhkan perbedaan, kembali ke solusi sebelumnya, di mana Anda memiliki keduanya.