PostgreSQL multi-kolom batasan unik dan nilai NULL

94

Saya punya tabel seperti berikut:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

Dan saya ingin (id_A, id_B, id_C)tampil beda dalam situasi apa pun. Jadi, dua sisipan berikut harus menghasilkan kesalahan:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

Tapi itu tidak berperilaku seperti yang diharapkan karena menurut dokumentasi, dua NULLnilai tidak dibandingkan satu sama lain, sehingga kedua sisipan lulus tanpa kesalahan.

Bagaimana saya bisa menjamin kendala unik saya bahkan jika id_Cbisa NULLdalam kasus ini? Sebenarnya, pertanyaan sebenarnya adalah: dapatkah saya menjamin keunikan seperti ini di "pure sql" atau apakah saya harus mengimplementasikannya pada level yang lebih tinggi (java dalam kasus saya)?

Manuel Leduc
sumber
Jadi, katakan Anda memiliki nilai (1,2,1)dan (1,2,2)di (A,B,C)kolom. Haruskah (1,2,NULL)diizinkan untuk ditambahkan atau tidak?
ypercubeᵀᴹ
A dan B tidak boleh nol tetapi C bisa nol atau nilai integer positif apa pun. Jadi (1,2,3) dan (2,4, null) valid tetapi (null, 2,3) atau (1, null, 4) tidak valid. Dan [(1,2, null), (1,2,3)] tidak melanggar batasan unik tetapi [(1,2, null), (1,2, null)] harus memecahkannya.
Manuel Leduc
2
Apakah ada nilai yang tidak akan pernah muncul di kolom tersebut (seperti nilai negatif?)
a_horse_with_no_name
Anda tidak perlu memberi label batasan Anda di hal. Ini akan secara otomatis menghasilkan nama. Hanya FYI.
Evan Carroll

Jawaban:

94

Anda dapat melakukannya dalam SQL murni . Buat indeks unik parsial selain yang Anda miliki:

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

Dengan cara ini Anda dapat memasukkan untuk (a, b, c)di tabel Anda:

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

Tapi tidak ada yang kedua kalinya.

Atau gunakan duaUNIQUE indeks parsial dan tidak ada indeks lengkap (atau kendala). Solusi terbaik tergantung pada detail kebutuhan Anda. Membandingkan:

Meskipun ini elegan dan efisien untuk satu kolom yang dapat dibatalkan dalam UNIQUEindeks, ini menjadi cepat hilang untuk lebih banyak. Membahas ini - dan bagaimana menggunakan UPSERT dengan indeks parsial:

Selain itu

Tidak digunakan untuk pengidentifikasi kasus campuran tanpa tanda kutip ganda di PostgreSQL.

Anda mungkin menganggap serialkolom sebagai kunci utama atau IDENTITYkolom di Postgres 10 atau lebih baru. Terkait:

Begitu:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

Jika Anda tidak mengharapkan lebih dari 2 miliar baris (> 2147483647) selama masa hidup tabel Anda (termasuk baris buang dan hapus), pertimbangkan integer(4 byte) alih-alih bigint(8 byte).

Erwin Brandstetter
sumber
1
Dokumen menganjurkan metode ini, Menambahkan kendala unik akan secara otomatis membuat indeks B-tree unik pada kolom atau grup kolom yang tercantum dalam kendala. Pembatasan keunikan yang hanya mencakup beberapa baris tidak dapat ditulis sebagai batasan unik, tetapi dimungkinkan untuk menerapkan pembatasan semacam itu dengan membuat indeks parsial unik.
Evan Carroll
12

Saya memiliki masalah yang sama dan saya menemukan cara lain untuk memiliki NULL yang unik ke dalam tabel.

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

Dalam kasus saya, bidang foreign_key_fieldadalah bilangan bulat positif dan tidak akan pernah -1.

Jadi, untuk menjawab Manual Leduc, solusi lain bisa jadi

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

Saya berasumsi bahwa id tidak akan -1.

Apa keuntungan membuat indeks parsial?
Jika Anda tidak memiliki klausa NOT NULL id_a,, id_bdan id_cdapat NULL bersama hanya sekali.
Dengan indeks parsial, 3 bidang bisa NULL lebih dari sekali.

Luc M
sumber
3
> Apa keuntungan membuat indeks parsial? Cara Anda melakukannya COALESCEbisa efektif dalam membatasi duplikat, tetapi indeks tidak akan sangat berguna dalam permintaan sebagai indeks ekspresi yang mungkin tidak akan cocok dengan ekspresi permintaan. Yaitu, kecuali Anda SELECT COALESCE(col, -1) ...tidak akan memukul indeks.
Bo Jeanes
@BoJeanes Indeks belum dibuat untuk masalah kinerja. Itu telah dibuat untuk memenuhi kebutuhan bisnis.
Luc M
8

Null dapat berarti bahwa nilai tidak diketahui untuk baris tersebut saat ini tetapi akan ditambahkan, ketika diketahui, di masa mendatang (contoh FinishDateuntuk menjalankan Project) atau bahwa tidak ada nilai yang dapat diterapkan untuk baris itu (contoh EscapeVelocityuntuk lubang hitam Star).

Menurut pendapat saya, biasanya lebih baik untuk menormalkan tabel dengan menghilangkan semua Nulls.

Dalam kasus Anda, Anda ingin memperbolehkan NULLsdi kolom Anda, namun Anda hanya menginginkan satu yang NULLdiizinkan. Mengapa? Hubungan macam apa ini di antara kedua tabel?

Mungkin Anda bisa dengan mudah mengubah kolom ke NOT NULLdan menyimpan, alih-alih NULL, nilai khusus (seperti -1) yang diketahui tidak pernah muncul. Ini akan menyelesaikan masalah kendala keunikan (tetapi mungkin memiliki efek samping lain yang mungkin tidak diinginkan. Misalnya, menggunakan -1berarti "tidak dikenal / tidak berlaku" akan memiringkan perhitungan jumlah atau rata-rata pada kolom. Atau semua perhitungan seperti itu harus diambil memperhitungkan nilai khusus dan mengabaikannya.)

ypercubeᵀᴹ
sumber
2
Dalam kasus saya, NULL benar-benar NULL (id_C adalah kunci asing untuk table_c untuk contoh sehingga tidak dapat memiliki nilai -1), itu berarti mereka tidak ada hubungan antara "my_table" dan "table_c". Jadi itu memiliki makna fungsional. Ngomong-ngomong [(1, 1,1, null), (2, 1,2, null), (3,2,4, null)] adalah daftar data yang dimasukkan yang valid.
Manuel Leduc
1
Ini tidak benar-benar Null seperti yang digunakan dalam SQL karena Anda ingin hanya satu di semua baris. Anda bisa mengubah skema database Anda dengan menambahkan -1 ke table_c atau dengan menambahkan tabel lain (yang akan menjadi supertype ke subtype table_c).
ypercubeᵀᴹ
3
Saya hanya ingin menunjukkan kepada @Manuel bahwa pendapat tentang nol dalam jawaban ini tidak dimiliki secara universal, dan banyak diperdebatkan. Banyak orang, seperti saya, berpikir bahwa nol dapat digunakan untuk tujuan apa pun yang Anda inginkan (tetapi seharusnya hanya berarti satu hal untuk setiap bidang dan didokumentasikan, mungkin dalam nama bidang atau komentar kolom)
Jack Douglas
1
Anda tidak dapat menggunakan nilai dummy ketika kolom Anda adalah KUNCI LUAR NEGERI.
Luc M
1
+1 Saya bersama Anda: jika kami ingin beberapa kombinasi kolom menjadi unik, maka Anda perlu mempertimbangkan entitas di mana kombinasi kolom ini adalah PK. Skema basis data OPs mungkin harus berubah menjadi tabel induk dan tabel induk.
AK