Buat batasan unik dengan kolom nol

252

Saya punya tabel dengan tata letak ini:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)

Saya ingin membuat batasan unik yang serupa dengan ini:

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);

Namun, ini akan memungkinkan beberapa baris dengan yang sama (UserId, RecipeId), jika MenuId IS NULL. Saya ingin mengizinkan NULLdalam MenuIduntuk menyimpan favorit yang tidak ada menu terkait, tapi aku hanya ingin paling banyak satu baris ini per pasang user / resep.

Ide-ide yang saya miliki sejauh ini adalah:

  1. Gunakan beberapa UUID hard-coded (seperti semua nol) bukan nol.
    Namun, MenuIdmemiliki batasan FK pada menu masing-masing pengguna, jadi saya kemudian harus membuat menu "null" khusus untuk setiap pengguna yang merepotkan.

  2. Periksa keberadaan entri nol menggunakan pemicu sebagai gantinya.
    Saya pikir ini merepotkan dan saya suka menghindari pemicu sedapat mungkin. Plus, saya tidak percaya mereka untuk menjamin data saya tidak pernah dalam keadaan buruk.

  3. Lupakan saja dan periksa keberadaan entri nol sebelumnya di middle-ware atau fungsi insert, dan tidak memiliki kendala ini.

Saya menggunakan Postgres 9.0.

Apakah ada metode yang saya abaikan?

Mike Christensen
sumber
Mengapa itu akan memungkinkan beberapa baris dengan yang sama ( UserId, RecipeId), jika MenuId IS NULL?
Drux

Jawaban:

382

Buat dua indeks parsial :

CREATE UNIQUE INDEX favo_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)
WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX favo_2col_uni_idx ON favorites (user_id, recipe_id)
WHERE menu_id IS NULL;

Dengan cara ini, hanya ada satu kombinasi di (user_id, recipe_id)mana menu_id IS NULL, secara efektif mengimplementasikan kendala yang diinginkan.

Kemungkinan kelemahan: Anda tidak dapat memiliki referensi kunci asing (user_id, menu_id, recipe_id), Anda tidak dapat mendasarkan CLUSTERpada indeks parsial, dan permintaan tanpa WHEREkondisi yang cocok tidak dapat menggunakan indeks parsial. (Sepertinya tidak mungkin Anda ingin referensi FK tiga kolom lebar - gunakan kolom PK sebagai gantinya).

Jika Anda membutuhkan indeks yang lengkap , Anda dapat menghilangkan WHEREkondisi tersebut dari alternatif favo_3col_uni_idxdan persyaratan Anda masih diberlakukan.
Indeks, sekarang terdiri dari seluruh tabel, tumpang tindih dengan yang lain dan semakin besar. Bergantung pada permintaan tipikal dan persentaseNULL nilai, ini mungkin bermanfaat atau tidak. Dalam situasi ekstrem, bahkan mungkin membantu mempertahankan ketiga indeks (dua yang parsial dan total di atas).

Selain itu: Saya menyarankan untuk tidak menggunakan pengidentifikasi kasus campuran dalam PostgreSQL .

Erwin Brandstetter
sumber
1
@Erwin Brandsetter: mengenai " pengidentifikasi kasus campuran " komentar: Selama tidak ada tanda kutip ganda yang digunakan, menggunakan pengidentifikasi cased campuran benar-benar baik-baik saja. Tidak ada perbedaan dalam menggunakan semua pengenal huruf kecil (sekali lagi: hanya jika tidak ada tanda kutip yang digunakan)
a_horse_with_no_name
14
@a_horse_with_no_name: Saya menganggap Anda tahu bahwa saya tahu itu. Itu sebenarnya salah satu alasan saya menyarankan agar tidak digunakan. Orang-orang yang tidak mengetahui secara spesifik menjadi bingung, seperti pada pengidentifikasi RDBMS lainnya (sebagian) peka terhadap huruf besar-kecil. Terkadang orang membingungkan diri sendiri. Atau mereka membangun SQL dinamis dan menggunakan quote_ident () sebagaimana mestinya dan lupa untuk memberikan pengidentifikasi sebagai string huruf kecil sekarang! Jangan gunakan pengidentifikasi kasus campuran dalam PostgreSQL, jika Anda bisa menghindarinya. Saya telah melihat sejumlah permintaan putus asa di sini berasal dari kebodohan ini.
Erwin Brandstetter
3
@a_horse_with_no_name: Ya, tentu saja itu benar. Tetapi jika Anda dapat menghindari mereka: Anda tidak ingin pengidentifikasi kasus campuran . Mereka tidak memiliki tujuan. Jika Anda dapat menghindarinya: jangan menggunakannya. Selain itu: mereka benar-benar jelek. Identifikasi yang dikutip jelek juga. Pengidentifikasi SQL92 dengan spasi di dalamnya adalah kesalahan langkah yang dibuat oleh komite. Jangan gunakan itu.
wildplasser
2
@ Mike: Saya pikir Anda harus berbicara dengan komite standar SQL tentang itu, semoga sukses :)
mu terlalu pendek
1
@ buffer: Biaya pemeliharaan dan total penyimpanan pada dasarnya sama (kecuali untuk overhead tetap per indeks kecil). Setiap baris hanya diwakili dalam satu indeks. Kinerja: Jika hasil Anda mencakup kedua kasus, total indeks polos tambahan dapat membayar. Jika tidak, sebagian indeks biasanya lebih cepat daripada indeks lengkap, terutama karena ukurannya yang lebih kecil. Tambahkan kondisi indeks ke kueri (berlebihan) jika Postgres tidak mengetahuinya dapat menggunakan sebagian indeks dengan sendirinya. Contoh.
Erwin Brandstetter
75

Anda bisa membuat indeks unik dengan menyatu di MenuId:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

Anda hanya perlu memilih UUID untuk COALESCE yang tidak akan pernah terjadi di "kehidupan nyata". Anda mungkin tidak akan pernah melihat UUID nol dalam kehidupan nyata, tetapi Anda bisa menambahkan PERIKSA kendala jika Anda paranoid (dan karena mereka benar-benar keluar untuk membuat Anda ...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')
mu terlalu pendek
sumber
1
Ini membawa cacat (teoritis), bahwa entri dengan menu_id = '00000000-0000-0000-0000-000000000000' dapat memicu pelanggaran unik yang salah - tetapi Anda sudah mengatasinya dalam komentar Anda.
Erwin Brandstetter
2
@muistooshort: Yup, itu solusi yang tepat. Sederhanakan (MenuId <> '00000000-0000-0000-0000-000000000000')juga. NULLdiizinkan secara default. Btw, ada tiga jenis orang. Yang paranoid, dan orang yang tidak melakukan database. Jenis ketiga kadang-kadang memposting pertanyaan pada SO dengan bingung. ;)
Erwin Brandstetter
2
@ Erwin: Bukankah maksud Anda "yang paranoid dan yang dengan database rusak"?
mu terlalu pendek
2
Solusi luar biasa ini membuatnya sangat mudah untuk memasukkan kolom nol dari jenis yang lebih sederhana, seperti integer, dalam batasan unik.
Markus Pscheidt
2
Memang benar bahwa UUID tidak akan menghasilkan string tertentu, tidak hanya karena probabilitas yang terlibat, tetapi juga karena itu bukan UUID yang valid . Generator UUID tidak bebas untuk menggunakan digit heks apa pun di posisi apa pun, misalnya satu posisi dicadangkan untuk nomor versi UUID.
Toby 1 Kenobi
1

Anda dapat menyimpan favorit tanpa menu terkait dalam tabel terpisah:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
ypercubeᵀᴹ
sumber
Ide yang menarik. Itu membuat memasukkan sedikit lebih rumit. Saya perlu memeriksa apakah suatu baris sudah ada FavoriteWithoutMenuterlebih dahulu. Jika demikian, saya hanya menambahkan tautan menu - jika tidak, saya membuat FavoriteWithoutMenubaris pertama dan kemudian menautkannya ke menu jika perlu. Itu juga membuat memilih semua favorit dalam satu permintaan sangat sulit: Saya harus melakukan sesuatu yang aneh seperti memilih semua tautan menu terlebih dahulu, dan kemudian memilih semua Favorit yang ID-nya tidak ada di dalam permintaan pertama. Saya tidak yakin apakah saya suka itu.
Mike Christensen
Saya tidak berpikir memasukkan lebih rumit. Jika Anda ingin menyisipkan catatan NULL MenuId, Anda memasukkan ke dalam tabel ini. Jika tidak, ke Favoritesmeja. Tapi bertanya, ya, itu akan lebih rumit.
ypercubeᵀᴹ
Sebenarnya gosok itu, memilih semua favorit hanya akan menjadi satu KIRI bergabung untuk mendapatkan menu. Hmm ya ini mungkin jalan yang harus ditempuh ..
Mike Christensen
INSERT menjadi lebih rumit jika Anda ingin menambahkan resep yang sama ke lebih dari satu menu, karena Anda memiliki batasan UNIK pada UserId / RecipeId di FavoriteWithoutMenu. Saya perlu membuat baris ini hanya jika belum ada.
Mike Christensen
1
Terima kasih! Jawaban ini layak diberi +1 karena ini lebih dari hal-hal murni SQL lintas-database .. Namun, dalam hal ini saya akan pergi rute indeks parsial karena tidak memerlukan perubahan pada skema saya dan saya menyukainya :)
Mike Christensen
-1

Saya pikir ada masalah semantik di sini. Dalam pandangan saya, pengguna dapat memiliki (tetapi hanya satu ) resep favorit untuk menyiapkan menu tertentu. (OP memiliki menu dan resep yang campur aduk; jika saya salah: silakan tukar MenuId dan RecipeId di bawah) Itu menyiratkan bahwa {user, menu} harus menjadi kunci unik dalam tabel ini. Dan itu harus menunjuk tepat satu resep. Jika pengguna tidak memiliki resep favorit untuk menu khusus ini, tidak ada baris untuk pasangan kunci {user, menu} ini. Juga: kunci pengganti (FaVouRiteId) tidak perlu: kunci primer komposit sangat valid untuk tabel pemetaan relasional.

Itu akan mengarah pada definisi tabel yang dikurangi:

CREATE TABLE Favorites
( UserId uuid NOT NULL REFERENCES users(id)
, MenuId uuid NOT NULL REFERENCES menus(id)
, RecipeId uuid NOT NULL REFERENCES recipes(id)
, PRIMARY KEY (UserId, MenuId)
);
wildplasser
sumber
2
Ya ini benar. Kecuali, dalam kasus saya, saya ingin mendukung memiliki favorit yang bukan milik menu apa pun. Bayangkan itu seperti Bookmark Anda di browser Anda. Anda mungkin hanya "menandai" suatu halaman. Atau, Anda dapat membuat sub-folder bookmark dan memberi judulnya hal-hal yang berbeda. Saya ingin memungkinkan pengguna untuk favorit resep, atau membuat sub-folder favorit yang disebut menu.
Mike Christensen
1
Seperti yang saya katakan: ini semua tentang semantik. (Saya sedang berpikir tentang makanan, jelas) Memiliki favorit "yang tidak termasuk menu" tidak masuk akal bagi saya. Anda tidak dapat menyukai sesuatu yang tidak ada, IMHO.
wildplasser
Sepertinya beberapa normalisasi db bisa membantu. Buat tabel kedua yang menghubungkan resep dengan menu (atau tidak). Meskipun itu menggeneralisasi masalah dan memungkinkan lebih dari satu menu resep bisa menjadi bagian. Apapun, pertanyaannya adalah tentang indeks unik di PostgreSQL. Terima kasih.
Chris