Memperjelas ON CONFLICT DO UPDATE
perilaku
Pertimbangkan manual di sini :
Untuk setiap baris yang diusulkan untuk disisipkan, baik hasil penyisipan, atau, jika batasan arbiter atau indeks yang ditentukan oleh
conflict_target
dilanggar, alternatif conflict_action
diambil.
Penekanan berani saya. Jadi Anda tidak perlu mengulangi predikat untuk kolom termasuk dalam indeks yang unik dalam WHERE
klausul ke UPDATE
(yang conflict_action
):
INSERT INTO test_upsert AS tu
(name , status, test_field , identifier, count)
VALUES ('shaun', 1 , 'test value', 'ident' , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'
Pelanggaran unik sudah menetapkan apa yang WHERE
akan diterapkan klausa tambahan Anda secara berlebihan.
Jelaskan indeks parsial
Tambahkan WHERE
klausa untuk menjadikannya sebagai indeks parsial aktual seperti yang Anda sebutkan sendiri (tetapi dengan logika terbalik):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Untuk menggunakan indeks parsial ini di UPSERT Anda, Anda perlu pencocokan seperti @ypercube menunjukkan :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Sekarang indeks parsial di atas disimpulkan. Namun , sebagaimana manual juga mencatat :
[...] indeks unik non-parsial (indeks unik tanpa predikat) akan disimpulkan (dan karenanya digunakan oleh ON CONFLICT
) jika indeks tersebut memenuhi setiap kriteria lain tersedia.
Jika Anda memiliki indeks tambahan (atau hanya) hanya (name, status)
itu akan (juga) digunakan. Indeks aktif tidak akan disimpulkan (name, status, test_field)
secara eksplisit . Ini tidak menjelaskan masalah Anda, tetapi mungkin menambah kebingungan saat pengujian.
Larutan
AIUI, belum ada yang menyelesaikan masalah Anda di atas . Dengan indeks parsial, hanya kasus khusus dengan nilai NULL yang cocok yang akan ditangkap. Dan baris duplikat lainnya akan dimasukkan jika Anda tidak memiliki indeks / kendala unik yang cocok lainnya, atau meningkatkan pengecualian jika Anda melakukannya. Saya kira itu bukan yang Anda inginkan. Anda menulis:
Kunci komposit terdiri dari 20 kolom, 10 di antaranya dapat nullable.
Apa tepatnya yang Anda anggap duplikat? Postgres (sesuai dengan standar SQL) tidak menganggap dua nilai NULL sama. Manual:
Secara umum, batasan unik dilanggar jika ada lebih dari satu baris dalam tabel di mana nilai semua kolom yang termasuk dalam kendala sama. Namun, dua nilai nol tidak pernah dianggap sama dalam perbandingan ini. Itu berarti bahkan dengan adanya batasan unik, dimungkinkan untuk menyimpan baris duplikat yang berisi nilai nol di setidaknya salah satu kolom yang dibatasi. Perilaku ini sesuai dengan standar SQL, tetapi kami telah mendengar bahwa database SQL lain mungkin tidak mengikuti aturan ini. Jadi berhati-hatilah saat mengembangkan aplikasi yang dimaksudkan untuk portable.
Terkait:
Saya berasumsi Anda inginNULL
nilai di semua 10 kolom nullable dianggap sama. Elegan & praktis untuk menutupi satu kolom yang dapat dibatalkan dengan indeks parsial tambahan seperti yang diperlihatkan di sini:
Tapi ini keluar dari tangan dengan cepat untuk kolom yang lebih dapat dibatalkan. Anda memerlukan indeks parsial untuk setiap kombinasi kolom nullable yang berbeda. Untuk hanya 2 dari yang 3 indeks parsial untuk (a)
, (b)
dan (a,b)
. Jumlah ini meningkat secara eksponensial 2^n - 1
. Untuk 10 kolom yang tidak dapat dibatalkan, untuk mencakup semua kemungkinan kombinasi nilai NULL, Anda sudah membutuhkan 1023 indeks parsial. Tidak pergi.
Solusi sederhana: ganti nilai NULL dan tentukan kolom yang terlibat NOT NULL
, dan semuanya akan bekerja dengan baik dengan UNIQUE
kendala sederhana .
Jika itu bukan opsi, saya sarankan indeks ekspresi dengan COALESCE
untuk mengganti NULL dalam indeks:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
String kosong ( ''
) adalah calon yang jelas untuk jenis karakter, tetapi Anda dapat menggunakan setiap nilai hukum yang baik tidak pernah muncul atau dapat dilipat dengan NULL menurut Anda definisi "unik".
Kemudian gunakan pernyataan ini:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun', 1, null , 'ident', 11) -- works with
, ('bob' , 2, 'test value', 'ident', 22) -- and without NULL
ON CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE -- match expr. index
SET count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);
Seperti @ypercube, saya berasumsi Anda benar-benar ingin menambah count
jumlah yang ada. Karena kolom bisa NULL, menambahkan NULL akan mengatur kolom NULL. Jika Anda mendefinisikan count NOT NULL
, Anda dapat menyederhanakan.
Gagasan lain adalah dengan hanya men-drop konflik_target dari pernyataan untuk mencakup semua pelanggaran unik . Kemudian Anda dapat menentukan berbagai indeks unik untuk definisi yang lebih canggih tentang apa yang seharusnya "unik". Tapi itu tidak akan terbang ON CONFLICT DO UPDATE
. Manual sekali lagi:
Karena ON CONFLICT DO NOTHING
, opsional untuk menentukan konflik_target; ketika dihilangkan, konflik dengan semua batasan yang dapat digunakan (dan indeks unik) ditangani. Sebab ON CONFLICT DO UPDATE
, konflik_target harus disediakan.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
dapat disederhanakancount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
Saya pikir masalahnya adalah bahwa Anda tidak memiliki indeks parsial dan
ON CONFLICT
sintaks tidak cocok dengantest_upsert_upsert_id_idx
indeks tetapi kendala unik lainnya.Jika Anda mendefinisikan indeks sebagai parsial (dengan
WHERE test_field IS NULL
):dan baris-baris ini sudah ada di tabel:
maka kueri akan berhasil:
dengan hasil sebagai berikut:
sumber