warunkowe ograniczenie unikalne

95

Mam sytuację, w której muszę wymusić unikalne ograniczenie na zestawie kolumn, ale tylko dla jednej wartości kolumny.

Na przykład mam tabelę taką jak Table (ID, Name, RecordStatus).

RecordStatus może mieć tylko wartość 1 lub 2 (aktywny lub usunięty) i chcę utworzyć unikalne ograniczenie dla (ID, RecordStatus) tylko wtedy, gdy RecordStatus = 1, ponieważ nie obchodzi mnie, czy istnieje wiele usuniętych rekordów z tym samym ID.

Czy oprócz pisania wyzwalaczy mogę to zrobić?

Używam SQL Server 2005.

np-trudne
źródło
1
Ten projekt to częsty ból. Czy rozważałeś zmianę projektu, tak aby hipotetycznie „usunięte” rekordy zostały fizycznie usunięte z tabeli i być może przeniesione do tabeli „archiwum”?
onedaywhen
1
... ponieważ niemożność napisania UNIKALNEGO ograniczenia w celu wymuszenia prostego klucza należy uznać za „zapach kodu”, IMO. Jeśli nie możesz zmienić projektu (SQL DDL), ponieważ wiele innych tabel odwołuje się do tej tabeli, założę się, że Twój SQL DML również ucierpi w wyniku, tj. Musisz pamiętać o dodaniu ... AND Table.RecordStatus = 1 ' do większości warunków wyszukiwania i warunków łączenia związanych z tą tabelą i doświadczających subtelnych błędów, gdy nieuchronnie jest on czasami pomijany.
onedaywhen

Odpowiedzi:

37

Dodaj takie ograniczenie sprawdzające. Różnica polega na tym, że zwrócisz wartość false, jeśli Status = 1 i Count> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
D. Patrick
źródło
Przyjrzałem się ograniczeniom sprawdzającym na poziomie tabeli, ale nie wygląda na to, że nie ma sposobu na przekazanie wartości, które są wstawiane lub aktualizowane do funkcji, czy wiesz, jak to zrobić?
np-ciężko
OK, opublikowałem przykładowy skrypt, który pomoże ci udowodnić, o czym mówię. Przetestowałem to i działa. Jeśli spojrzysz na dwie skomentowane linie, zobaczysz komunikat, który otrzymuję. Nota bene, w mojej implementacji zapewniam jedynie, że nie można dodać drugiego elementu o tym samym identyfikatorze, który jest aktywny, jeśli już istnieje. Możesz zmodyfikować logikę w taki sposób, że jeśli jest aktywna, nie możesz dodać żadnego elementu o tym samym identyfikatorze. Dzięki temu wzorowi możliwości są prawie nieograniczone.
D. Patrick
Wolałbym tę samą logikę w wyzwalaczu. "zapytanie w funkcji skalarnej ... może powodować duże problemy, jeśli twoje ograniczenie CHECK opiera się na zapytaniu i jeśli aktualizacja ma wpływ na więcej niż jeden wiersz. Dzieje się tak, że ograniczenie jest sprawdzane raz dla każdego wiersza przed zakończeniem instrukcji Oznacza to, że niepodzielność instrukcji jest zepsuta i funkcja zostanie ujawniona w bazie danych w niespójnym stanie. Wyniki są nieprzewidywalne i niedokładne. " Zobacz: blogs.conchango.com/davidportas/archive/2007/02/19/...
onedaywhen
To tylko częściowo prawda pewnego dnia. Baza danych zachowuje się spójnie i przewidywalnie. Ograniczenie sprawdzające zostanie wykonane po dodaniu wiersza do tabeli i przed zatwierdzeniem transakcji przez dbms i możesz na to liczyć. Ten blog mówił o dość wyjątkowym problemie, w którym trzeba wykonać ograniczenie względem zestawu wkładek, a nie tylko jednej wkładki na raz. ashish prosi o ograniczenie jednej płytki na raz, a to ograniczenie będzie działać dokładnie, przewidywalnie i konsekwentnie. Przepraszam, jeśli zabrzmiało to lakonicznie; Kończyło mi się znaków.
D. Patrick
3
Działa to świetnie w przypadku wstawek, ale wydaje się, że nie działa w przypadku aktualizacji. EG Dodanie tego po innych wstawkach działa tutaj, gdy się tego nie spodziewałem. WSTAWIĆ WARTOŚCI CheckConstraint (1, 'No ProblemsA', 2); zaktualizuj CheckConstraint set Recordstatus = 1 gdzie name = 'No ProblemsA'
dwidel
152

Oto przefiltrowany indeks . Z dokumentacji (wyróżnienie moje):

Indeks filtrowany to zoptymalizowany indeks nieklastrowy, szczególnie nadający się do obsługi zapytań, które wybierają z dobrze zdefiniowanego podzbioru danych. Używa predykatu filtru do indeksowania części wierszy w tabeli. Dobrze zaprojektowany filtrowany indeks może poprawić wydajność zapytań, a także obniżyć koszty utrzymania indeksu i przechowywania w porównaniu z indeksami pełnotabelowymi.

A oto przykład łączący unikalny indeks z predykatem filtru:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

To zasadniczo wymusza wyjątkowość tego, IDkiedy RecordStatusjest 1.

Po utworzeniu tego indeksu, naruszenie unikalności podniesie wynik:

Msg 2601, poziom 14, stan 1, wiersz 13
Nie można wstawić zduplikowanego wiersza klucza w obiekcie „dbo.MyTable” z unikalnym indeksem „MyIndex”. Zduplikowana wartość klucza to (9999).

Uwaga: filtrowany indeks został wprowadzony w SQL Server 2008. W przypadku wcześniejszych wersji SQL Server, zobacz tę odpowiedź .

kanon
źródło
Zauważ, że SQL Server wymaga ansi_paddingfiltrowanych indeksów, więc upewnij się, że ta opcja jest włączona, wykonując SET ANSI_PADDING ONprzed utworzeniem filtrowanego indeksu.
naXa
10

Możesz przenieść usunięte rekordy do tabeli, w której brakuje ograniczenia, i być może użyć widoku z UNION dwóch tabel, aby zachować wygląd pojedynczej tabeli.

Carl Manaster
źródło
2
To całkiem sprytne Carl. To nie jest odpowiedź na pytanie per se, ale to dobre rozwiązanie. Jeśli tabela zawiera wiele wierszy, może to również przyspieszyć wyszukiwanie aktywnego rekordu, ponieważ można spojrzeć na tabelę aktywnego rekordu. Przyspieszyłoby to również ograniczenie, ponieważ unikalne ograniczenie używa indeksu w przeciwieństwie do ograniczenia sprawdzającego, które napisałem poniżej, które musi wykonać zliczanie. Lubię to.
D. Patrick
3

Możesz to zrobić w naprawdę hakerski sposób ...

Utwórz schematyczny widok na swoim stole.

UTWÓRZ WIDOK Cokolwiek SELECT * FROM Table WHERE RecordStatus = 1

Teraz utwórz unikalne ograniczenie widoku z wybranymi polami.

Jedna uwaga na temat widoków schematu, jeśli zmienisz tabele bazowe, będziesz musiał ponownie utworzyć widok. Mnóstwo pułapek z tego powodu.

Min
źródło
To całkiem dobra sugestia, a nie taka „hakerska”. Oto więcej informacji na temat tej alternatywy dla filtrowanego indeksu .
Scott Whitlock,
To zły pomysł. Nie o to chodzi.
FabianoLothor
Kiedyś użyłem schematu i nigdy nie powtórzyłem tego błędu. Mogą być królewskim bólem w pracy. Nie chodzi o to, że musisz ponownie utworzyć widok, jeśli zmienisz tabelę bazową - potencjalnie musisz to zrobić dla wszystkich widoków, przynajmniej na serwerze SQL. Chodzi o to, że nie możesz zmienić tabeli bez uprzedniego usunięcia widoku, czego możesz nie być w stanie zrobić bez uprzedniego usunięcia odniesień do niego. Aha, plus przechowywanie może być problematyczne - albo z powodu miejsca, albo z powodu kosztów, które dodaje do wkładania i aktualizacji.
MattW
1

Ponieważ zamierzasz zezwolić na duplikaty, unikalne ograniczenie nie zadziała. Możesz utworzyć ograniczenie sprawdzające dla kolumny RecordStatus i procedurę składowaną dla INSERT, która sprawdza istniejące aktywne rekordy przed wstawieniem zduplikowanych identyfikatorów.

ichiban
źródło
1

Jeśli nie możesz użyć NULL jako RecordStatus, jak sugerował Bill, możesz połączyć jego pomysł z indeksem opartym na funkcjach. Utwórz funkcję, która zwraca wartość NULL, jeśli RecordStatus nie jest jedną z wartości, które chcesz uwzględnić w ograniczeniu (a w przeciwnym razie RecordStatus), i utwórz indeks dla tego.

Będzie to miało tę zaletę, że nie musisz jawnie sprawdzać innych wierszy w tabeli w swoim ograniczeniu, co może powodować problemy z wydajnością.

Powinienem powiedzieć, że w ogóle nie znam serwera SQL, ale z powodzeniem zastosowałem to podejście w Oracle.

Włóczęga
źródło
dobry pomysł, ale nie ma indeksowanych funkcji na serwerze sql, ale dzięki za odpowiedź
np-hard