Utwórz unikalne ograniczenie z pustymi kolumnami


251

Mam stół z tym układem:

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

Chcę utworzyć unikalne ograniczenie podobne do tego:

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

Pozwoli to jednak na wiele wierszy z tym samym (UserId, RecipeId), jeśli MenuId IS NULL. Chcę pozwolić NULLna MenuIdprzechowywanie faworyta, który nie ma menu powiązanych, ale chcę tylko co najwyżej jeden z tych wierszy na parę użytkownik / receptury.

Dotychczasowe pomysły to:

  1. Użyj jakiegoś zakodowanego UUID (takiego jak wszystkie zera) zamiast null.
    Jednak MenuIdma ograniczenie FK w menu każdego użytkownika, więc musiałbym stworzyć specjalne menu „zerowe” dla każdego użytkownika, co jest kłopotliwe.

  2. Sprawdź, czy istnieje wpis zerowy, używając wyzwalacza.
    Myślę, że to kłopot i lubię unikać wyzwalaczy tam, gdzie to możliwe. Ponadto nie ufam im, aby zagwarantować, że moje dane nigdy nie będą w złym stanie.

  3. Po prostu zapomnij o tym i sprawdź wcześniejsze istnienie pustego wpisu w oprogramowaniu pośrednim lub w funkcji wstawiania i nie masz tego ograniczenia.

Używam Postgres 9.0.

Czy przeoczyłem jakąś metodę?


Dlaczego to pozwala na wiele wierszy z tym samym ( UserId, RecipeId), jeśli MenuId IS NULL?
— Drux,

Odpowiedzi:


382

Utwórz dwa indeksy częściowe :

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;

W ten sposób może istnieć tylko jedna kombinacja (user_id, recipe_id)gdzie menu_id IS NULL, skutecznie wdrażając pożądane ograniczenie.

Możliwe wady: nie możesz mieć odwołania do klucza obcego (user_id, menu_id, recipe_id), nie możesz bazować CLUSTERna indeksie częściowym, a zapytania bez pasującego WHEREwarunku nie mogą używać indeksu częściowego. (Wydaje się mało prawdopodobne, abyś chciał mieć odniesienie FK o szerokości trzech kolumn - zamiast tego użyj kolumny PK).

Jeśli potrzebujesz pełnego indeksu, możesz alternatywnie usunąć WHEREwarunek, favo_3col_uni_idxa Twoje wymagania są nadal egzekwowane.
Indeks, który obejmuje teraz całą tabelę, pokrywa się z drugą tabelą i staje się większy. W zależności od typowych zapytań i odsetka NULLwartości może to być przydatne lub nie. W skrajnych sytuacjach może nawet pomóc utrzymać wszystkie trzy indeksy (dwa częściowe i suma na górze).

Poza tym: Radzę nie używać identyfikatorów mieszanych w PostgreSQL .


1
@Erwin Brandsetter: w odniesieniu do uwagi „ identyfikatory wieloznaczne ”: tak długo, jak nie stosuje się podwójnych cudzysłowów, stosowanie identyfikatorów wieloznacznych jest absolutnie w porządku. Nie ma różnicy w używaniu wszystkich małych liter (ponownie: tylko jeśli nie są używane cudzysłowy)
— a_horse_w_na_nazwie

14
@ a_horse_with_no_name: Zakładam, że wiesz, że ja to wiem. To jest rzeczywiście jeden z powodów, radzę przeciwko To użytkowania. Ludzie, którzy nie znają tak dobrze specyfiki, wpadają w zamieszanie, ponieważ w innych identyfikatorach RDBMS rozróżniana jest (częściowo) wielkość liter. Czasami ludzie się mylą. Lub budują dynamiczny SQL i używają quote_ident () tak, jak powinni i zapominają teraz przekazywać identyfikatory jako ciągi małych liter! Nie używaj mieszanych identyfikatorów wielkości liter w PostgreSQL, jeśli możesz tego uniknąć. Widziałem tutaj wiele desperackich próśb wynikających z tego szaleństwa.
— Erwin Brandstetter

3
@ a_horse_with_no_name: Tak, to oczywiście prawda. Ale jeśli możesz ich uniknąć: nie chcesz mieszanych identyfikatorów . Nie służą celowi. Jeśli możesz ich uniknąć: nie używaj ich. Poza tym: są po prostu brzydkie. Cytowane identyfikatory też są brzydkie. Identyfikatory SQL92 ze spacjami są błędem popełnionym przez komisję. Nie używaj ich.
— wildplasser

2
@Mike: Myślę, że powinieneś porozmawiać o tym z komitetem ds. Standardów SQL, powodzenia :)
— jest za krótki

1
@bufor: Koszt utrzymania i całkowite miejsce do przechowywania są zasadniczo takie same (z wyjątkiem niewielkiego stałego narzutu na indeks). Każdy wiersz jest reprezentowany tylko w jednym indeksie. Wydajność: jeśli wyniki obejmują oba przypadki, może zapłacić dodatkowy całkowity indeks zwykły. Jeśli nie, indeks częściowy jest zwykle szybszy niż indeks całkowity, głównie z powodu mniejszego rozmiaru. Dodaj warunek indeksu do zapytań (redundantnie), jeśli Postgres nie zorientuje się, że sam może użyć indeksu częściowego. Przykład.
— Erwin Brandstetter

75

Możesz stworzyć unikalny indeks z koalescencją w MenuId:

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

Musisz po prostu wybrać UUID dla WĘGLA, który nigdy nie nastąpi w „prawdziwym życiu”. Prawdopodobnie nigdy nie zobaczyłbyś zerowego UUID w prawdziwym życiu, ale możesz dodać ograniczenie CHECK, jeśli jesteś paranoikiem (a ponieważ oni naprawdę chcą cię dostać ...):

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

1
Niesie to (teoretyczną) wadę, że wpisy z menu_id = „00000000-0000-0000-0000-000000000000” mogą powodować fałszywe unikalne naruszenia - ale już to rozwiązałeś w swoim komentarzu.
— Erwin Brandstetter

2
@muistooshort: Tak, to właściwe rozwiązanie. Uprość to (MenuId <> '00000000-0000-0000-0000-000000000000')jednak. NULLjest domyślnie dozwolone. Przy okazji są trzy rodzaje ludzi. Te paranoiczne i ludzie, którzy nie robią baz danych. Trzeci rodzaj od czasu do czasu z zakłopotaniem zamieszcza pytania na temat SO. ;)
— Erwin Brandstetter,

2
@Erwin: Czy nie masz na myśli „tych paranoicznych i tych z uszkodzonymi bazami danych”?
— mu jest za krótki

2
To doskonałe rozwiązanie ułatwia włączenie kolumny zerowej prostszego typu, na przykład liczby całkowitej, w unikalne ograniczenie.
— Markus Pscheidt,

2
Prawdą jest, że identyfikator UUID nie wymyśli tego konkretnego ciągu, nie tylko ze względu na związane z nim prawdopodobieństwa, ale także dlatego, że nie jest to prawidłowy identyfikator UUID . Generator UUID nie może dowolnie używać cyfr szesnastkowych w żadnej pozycji, na przykład jedna pozycja jest zarezerwowana dla numeru wersji UUID.
— Toby 1 Kenobi,

1

Możesz przechowywać ulubione bez powiązanego menu w osobnej tabeli:

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

Ciekawy pomysł. To sprawia, że ​​wstawianie jest nieco bardziej skomplikowane. Musiałbym sprawdzić, czy wiersz już istnieje w FavoriteWithoutMenupierwszym. Jeśli tak, po prostu dodaję link do menu - w przeciwnym razie FavoriteWithoutMenunajpierw tworzę wiersz, a następnie w razie potrzeby łączę go z menu. Utrudnia to również wybranie wszystkich ulubionych w jednym zapytaniu: musiałbym zrobić coś dziwnego, jak najpierw wybrać wszystkie łącza menu, a następnie wybrać wszystkie Ulubione, których identyfikatory nie istnieją w pierwszym zapytaniu. Nie jestem pewien, czy mi się to podoba.
— Mike Christensen

Nie sądzę, żeby wstawianie było bardziej skomplikowane. Jeśli chcesz wstawić rekord NULL MenuId, wstawisz do tej tabeli. Jeśli nie, do Favoritesstołu. Ale zapytanie, tak, będzie bardziej skomplikowane.
— ypercubeᵀᴹ

Właściwie to zaznacz, wybranie wszystkich ulubionych byłoby tylko jednym LEWYM złączeniem, aby uzyskać menu. Hmm tak, to może być dobry sposób ...
— Mike Christensen

WSTAW staje się bardziej skomplikowane, jeśli chcesz dodać ten sam przepis do więcej niż jednego menu, ponieważ masz WYJĄTKOWE ograniczenie na UserId / RecipeId na FavoriteWithoutMenu. Musiałbym utworzyć ten wiersz tylko wtedy, gdyby jeszcze nie istniał.
— Mike Christensen

1
Dzięki! Ta odpowiedź zasługuje na +1, ponieważ jest to kwestia czystego SQL między bazami danych. Jednak w tym przypadku wybiorę trasę częściowego indeksu, ponieważ nie wymaga ona żadnych zmian w moim schemacie i podoba mi się to :)
— Mike Christensen

-1

Myślę, że jest tu problem semantyczny. Moim zdaniem użytkownik może mieć (ale tylko jeden ) ulubiony przepis na przygotowanie określonego menu. (OP ma pomieszane menu i przepis; jeśli się mylę: proszę zamień MenuId i RecipeId poniżej) Oznacza to, że {użytkownik, menu} powinien być unikalnym kluczem w tej tabeli. I powinien wskazywać dokładnie jeden przepis. Jeśli użytkownik nie ma ulubionej receptury dla tego konkretnego menu, nie powinien istnieć wiersz dla tej pary kluczy {użytkownik, menu}. Ponadto: klucz zastępczy (FaVouRiteId) jest zbędny: złożone klucze podstawowe doskonale nadają się do tabel mapowania relacyjnego.

Doprowadziłoby to do ograniczenia definicji tabeli:

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
Tak, to prawda. Tyle że w moim przypadku chcę wesprzeć posiadanie ulubionego, który nie należy do żadnego menu. Wyobraź to sobie jak Zakładki w przeglądarce. Możesz po prostu „dodać zakładkę” do strony. Możesz też utworzyć podfoldery zakładek i nadać im tytuły. Chcę pozwolić użytkownikom na ulubienie przepisu lub tworzenie podfolderów ulubionych zwanych menu.
— Mike Christensen

1
Jak powiedziałem: chodzi o semantykę. (Oczywiście myślałem o jedzeniu) Posiadanie ulubionego „nie należącego do żadnego menu” nie ma dla mnie sensu. Nie możesz faworyzować czegoś, co nie istnieje, IMHO.
— wildplasser

Wydaje się, że normalizacja db mogłaby pomóc. Utwórz drugą tabelę, która wiąże przepisy z menu (lub nie). Chociaż uogólnia problem i pozwala na utworzenie więcej niż jednego menu, którego częścią może być przepis. Niezależnie od tego pytanie dotyczyło unikalnych indeksów w PostgreSQL. Dzięki.
— Chris
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.