Boş sütunlarla benzersiz kısıtlama oluşturma


252

Bu düzen ile bir tablo var:

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

Buna benzer benzersiz bir kısıtlama oluşturmak istiyorum:

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

Ancak bu aynı olan birden fazla satır sağlayacaktır (UserId, RecipeId)eğer MenuId IS NULL. Ben izin vermek istediğiniz NULLyer MenuIdyok menü ilişkilendirdiği bir sık saklamak için, ama sadece kullanıcı / reçete çifti başına bu satırlardan en az birini istiyorum.

Şimdiye kadar sahip olduğum fikirler:

  1. Null yerine sabit kodlanmış bir UUID (tüm sıfırlar gibi) kullanın.
    Ancak, MenuIdher kullanıcının menülerinde bir FK kısıtlaması vardır, bu yüzden bir güçlük olan her kullanıcı için özel bir "boş" menü oluşturmak zorunda kalacaktım.

  2. Bunun yerine bir tetikleyici kullanarak boş bir giriş olup olmadığını kontrol edin.
    Bence bu bir güçlük ve mümkün olan yerlerde tetikleyicilerden kaçınmayı seviyorum. Ayrıca, verilerimin hiçbir zaman kötü durumda olmadığını garanti etmelerine güvenmiyorum.

  3. Sadece unutun ve orta eşyada veya bir ekleme işlevinde boş bir girdinin önceki varlığını kontrol edin ve bu kısıtlamaya sahip değilsiniz.

Postgres 9.0 kullanıyorum.

Göz ardı ettiğim bir yöntem var mı?


Neden aynı ( UserId, RecipeId) ile birden fazla satıra izin verir MenuId IS NULL?
Drux

Yanıtlar:


382

İki kısmi dizin oluşturun :

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;

Bu şekilde, yalnızca biri kombinasyon olması (user_id, recipe_id)nerede menu_id IS NULLetkili bir şekilde istenilen sınırlamayı uygulayan,.

Olası dezavantajlar: yabancı anahtar referansınız (user_id, menu_id, recipe_id)olamaz CLUSTER, kısmi bir dizine dayandırılamaz ve eşleştirme WHEREkoşulu olmayan sorgular kısmi dizini kullanamaz. (Bir FK başvurusunun üç sütun genişliğinde olmasını istemeyebilirsiniz - bunun yerine PK sütununu kullanın).

Tam bir dizine ihtiyacınız varsa , WHEREkoşulu alternatif olarak bırakabilirsiniz favo_3col_uni_idxve gereksinimleriniz yine de uygulanır.
Şimdi tüm tabloyu içeren indeks diğeriyle çakışıyor ve büyüyor. Tipik sorgulara ve NULLdeğerlerin yüzdesine bağlı olarak , bu yararlı olabilir veya olmayabilir. Aşırı durumlarda, üç indeksin de (iki kısmi ve toplamda üstte) korunmasına yardımcı olabilir.

Bir yana: PostgreSQL'de karma vaka tanımlayıcıları kullanmamanızı tavsiye ederim .


1
@Erwin Brandsetter: " karışık vaka tanımlayıcıları " ile ilgili açıklama: Çift tırnak kullanılmadığı sürece, karışık kasa tanımlayıcıları kullanmak kesinlikle iyidir. Tüm küçük harfli tanımlayıcıları kullanmanın bir farkı yoktur (tekrar: yalnızca tırnak kullanılmıyorsa)
a_horse_with_no_name

14
@a_horse_with_no_name: Bunu bildiğimi bildiğinizi varsayıyorum. Bu aslında kullanımına karşı tavsiye etmemin nedenlerinden biri . Diğer RDBMS tanımlayıcılarında olduğu gibi, özellikleri çok iyi bilmeyen insanlar (kısmen) büyük / küçük harfe duyarlıdır. Bazen insanlar kendilerini karıştırırlar. Ya da dinamik SQL oluştururlar ve gerektiği gibi quote_ident () kullanırlar ve şimdi tanımlayıcıları küçük harfli dizeler olarak geçirmeyi unuturlar! Bundan kaçınabiliyorsanız PostgreSQL'de karma vaka tanımlayıcıları kullanmayın. Burada bu ahmaklıktan kaynaklanan çaresiz istekler gördüm.
Erwin Brandstetter

3
@a_horse_with_no_name: Evet, bu elbette doğrudur. Ancak onlardan kaçınabiliyorsanız: karışık vaka tanımlayıcıları istemezsiniz . Hiçbir amaca hizmet etmezler. Onlardan kaçınabiliyorsanız: kullanmayın. Ayrıca: onlar sadece çirkin. Alıntılanan kimlikler de çirkin. İçlerinde boşluk bulunan SQL92 tanımlayıcıları bir komite tarafından yapılan bir yanlış adımdır. Onları kullanma.
wildplasser

2
@Mike: Bence SQL standartları komitesiyle bu konuda konuşmak zorundasınız, iyi şanslar :)
mu çok kısa

1
@buffer: Bakım maliyeti ve toplam depolama alanı temelde aynıdır (dizin başına küçük bir sabit ek yük hariç). Her satır yalnızca bir dizinde gösterilir. Performans: Sonuçlarınız her iki duruma da yayılıyorsa, ek bir toplam düz dizin ödenebilir. Aksi halde, kısmi bir dizin, esas olarak daha küçük boyuttan dolayı, tam bir dizinden daha hızlıdır. Postgres kendi başına kısmi bir dizin kullanabileceğini anlamazsa dizin koşulunu sorgulara (artık) ekleyin. Misal.
Erwin Brandstetter

75

MenuId'de birleşme ile benzersiz bir dizin oluşturabilirsiniz:

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

COALESCE için "gerçek hayatta" asla gerçekleşmeyecek bir UUID seçmeniz yeterlidir. Muhtemelen gerçek hayatta sıfır UUID görmek asla ama sen paranoyak ise bir CHECK kısıtlaması ekleyebilir (ve o zamandan beri onlar gerçekten vardır ... seni almak için):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')

1
Bu, menu_id = '00000000-0000-0000-0000-000000000000' içeren girişlerin yanlış benzersiz ihlalleri tetikleyebileceği (teorik) kusuru taşır - ancak yorumunuzda bunu zaten ele aldınız.
Erwin Brandstetter

2
@muistooshort: Evet, bu uygun bir çözüm. (MenuId <> '00000000-0000-0000-0000-000000000000')Yine de basitleştirin . NULLvarsayılan olarak izin verilir. Btw, üç çeşit insan var. Paranoyak olanlar ve veritabanları yapmayan insanlar. Üçüncü tür bazen şaşkınlık içinde SO hakkında sorular yayınlar. ;)
Erwin Brandstetter

2
@Erwin: "Paranoyak olanlar ve veri tabanları bozuk olanlar" demek istemiyor musunuz?
mu çok kısa

2
Bu mükemmel çözüm, benzersiz bir kısıtlamaya tamsayı gibi daha basit türde boş bir sütun eklemeyi çok kolaylaştırır.
Markus Pscheidt

2
Bir UUID'nin sadece ilgili olasılıklar için değil, aynı zamanda geçerli bir UUID olmadığı için de belirli bir dizeyle gelmeyeceği doğrudur . Bir UUID jeneratörü herhangi bir konumda herhangi bir onaltılık basamak kullanamaz, örneğin bir konum UUID'nin sürüm numarası için ayrılmıştır.
Toby 1 Kenobi

1

İlişkili menüsü olmayan favorileri ayrı bir tabloda saklayabilirsiniz:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)

İlginç bir fikir. Eklemeyi biraz daha karmaşık hale getirir. FavoriteWithoutMenuÖncelikle bir satır olup olmadığını kontrol etmem gerekir . Eğer öyleyse, sadece bir menü bağlantısı ekliyorum - aksi takdirde önce FavoriteWithoutMenusatırı oluştururum ve sonra gerekirse bir menüye bağlarım. Ayrıca, bir sorgudaki tüm sık kullanılanları seçmeyi çok zorlaştırır: Önce tüm menü bağlantılarını seçmek ve ardından ilk sorguda kimlikleri bulunmayan tüm Sık Kullanılanları seçmek gibi garip bir şey yapmak zorundayım. Bunu sevip sevmediğimden emin değilim.
Mike Christensen

Eklemeyi daha karmaşık bulmuyorum. İle bir kayıt eklemek istiyorsanız NULL MenuId, bu tabloya eklersiniz. Değilse, Favoritesmasaya. Ama sorgulamak, evet, daha karmaşık olacak.
ypercubeᵀᴹ

Aslında, tüm sık kullanılanların seçilmesi, menüyü almak için sadece tek bir LEFT birleşimi olacaktır. Hmm evet bu gitmek için bir yol olabilir ..
Mike Christensen

FavoriteWithoutMenu'da UserId / RecipeId üzerinde UNIQUE kısıtlamanız olduğundan, aynı tarifi birden fazla menüye eklemek istiyorsanız INSERT daha karmaşık hale gelir. Bu satırı yalnızca zaten yoksa oluşturmam gerekirdi.
Mike Christensen

1
Teşekkürler! Bu cevap daha fazla bir çapraz veritabanı saf SQL şey olduğu için bir +1 hak ediyor .. Ancak, bu durumda, benim şemasında hiçbir değişiklik gerektirmediği için kısmi dizin yoluna gideceğim ve bunu seviyorum :)
Mike Christensen

-1

Bence burada anlamsal bir sorun var. Benim görüşüme göre, bir kullanıcının belirli bir menüyü hazırlamak için ( sadece bir tane ) favori tarifi olabilir. (OP'nin menüsü ve tarifi karışıktır; yanlışsam: lütfen MenuId ve RecipeId'i değiştirin) Bu, {user, menu} öğesinin bu tabloda benzersiz bir anahtar olması gerektiği anlamına gelir. Ve tam olarak bir tarife işaret etmelidir . Kullanıcının bu belirli menü için favori tarifi yoksa , bu {user, menu} anahtar çifti için hiçbir satır olmamalıdır . Ayrıca: yedek anahtar (FaVouRiteId) gereksizdir: bileşik birincil anahtarlar ilişkisel eşleme tabloları için mükemmel şekilde geçerlidir.

Bu, tablo tanımının azalmasına neden olur:

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)
);

2
Evet, bu doğru. Dışında, benim durumumda herhangi bir menüye ait olmayan bir favori olmasını desteklemek istiyorum. Tarayıcınızdaki Yer İşaretleriniz gibi düşünün. Bir sayfaya "yer işareti koyabilirsiniz". Veya, yer işaretlerinin alt klasörlerini oluşturabilir ve onlara farklı şeyler koyabilirsiniz. Kullanıcıların bir tarifi favorilerine eklemesine veya menüler adı verilen favorilerin alt klasörlerini oluşturmasına izin vermek istiyorum.
Mike Christensen

1
Dediğim gibi: hepsi anlambilim ile ilgili. (Açıkçası yemek hakkında düşünüyordum) Favori "herhangi bir menüye ait değildir" olması benim için bir anlam ifade etmiyor. Var olmayan bir şeyden yana olamazsınız, IMHO.
wildplasser

Bazı db normalizasyonu yardımcı olabilir gibi görünüyor. Yemek tariflerini menülerle ilişkilendiren (veya ilişkilendirmeyen) ikinci bir tablo oluşturun. Her ne kadar problemi genelleştirir ve bir reçetenin parçası olabileceği birden fazla menüye izin verir. Ne olursa olsun, soru PostgreSQL'deki benzersiz dizinlerle ilgiliydi. Teşekkürler.
Chris
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.