Jak utworzyć unikalne ograniczenie, które również dopuszcza wartości zerowe?

619

Chcę mieć unikalne ograniczenie w kolumnie, którą zamierzam wypełnić GUID. Jednak moje dane zawierają wartości null dla tych kolumn. Jak utworzyć ograniczenie, które zezwala na wiele wartości null?

Oto przykładowy scenariusz . Rozważ ten schemat:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
  LibraryCardId UNIQUEIDENTIFIER NULL,
  CONSTRAINT UQ_People_LibraryCardId UNIQUE (LibraryCardId)
)

Następnie zobacz ten kod dla tego, co próbuję osiągnąć:

-- This works fine:
INSERT INTO People (Name, LibraryCardId) 
 VALUES ('John Doe', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This also works fine, obviously:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marie Doe', 'BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB');

-- This would *correctly* fail:
--INSERT INTO People (Name, LibraryCardId) 
--VALUES ('John Doe the Second', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This works fine this one first time:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Richard Roe', NULL);

-- THE PROBLEM: This fails even though I'd like to be able to do this:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marcus Roe', NULL);

Końcowa instrukcja kończy się niepowodzeniem i pojawia się komunikat:

Naruszenie ograniczenia UNIKALNEGO KLUCZA „UQ_People_LibraryCardId”. Nie można wstawić duplikatu klucza w obiekcie „dbo.People”.

Jak mogę zmienić mój schemat i / lub ograniczenie unikatowości, aby zezwalało na wiele NULLwartości, jednocześnie sprawdzając unikalność na rzeczywistych danych?

Stuart
źródło
Problem z połączeniem dla standardowej kompatybilności, aby głosować na: connect.microsoft.com/SQLServer/Feedback/Details/299229
Vadzim
WYJĄTKOWE ograniczenie i zezwól na wartości NULL. ? To zdrowy rozsądek. Nie jest to możliwe
flik
13
@flik, lepiej nie odnosić się do „zdrowego rozsądku”. To nie jest prawidłowy argument. Zwłaszcza biorąc pod uwagę, że nullnie jest to wartość, ale brak wartości. Zgodnie ze standardem SQL nullnie jest uważany za równy null. Dlaczego więc wielokrotność nullpowinna być naruszeniem wyjątkowości?
Frédéric

Odpowiedzi:

144

SQL Server 2008 +

Możesz utworzyć unikalny indeks, który akceptuje wiele wartości NULL z WHEREklauzulą. Zobacz odpowiedź poniżej .

Przed SQL Server 2008

Nie można utworzyć ograniczenia UNIKALNEGO i zezwolić na wartości NULL. Musisz ustawić wartość domyślną NEWID ().

Zaktualizuj istniejące wartości do NEWID (), gdzie NULL, przed utworzeniem ograniczenia UNIQUE.

Jose Basilio
źródło
2
i to retrospektywnie doda wartości do istniejących wierszy, jeśli tak, to właśnie muszę zrobić, dzięki?
Stuart,
1
Trzeba by uruchomić instrukcję UPDATE, aby ustawić istniejące wartości na NEWID (), gdzie istniejące pole ma wartość NULL
Jose Basilio,
54
Jeśli używasz programu SQL Server 2008 lub nowszego, zobacz odpowiedź poniżej z ponad 100 głosami upvotes. Możesz dodać klauzulę WHERE do swojego wyjątkowego ograniczenia.
Darren Griffith
1
Ten bardzo problem dotyczy również tabel danych ADO.NET. Więc nawet jeśli mogę pozwolić na wartości null w polu podkładu za pomocą tej metody, DataTable nie pozwoli mi przede wszystkim przechowywać wartości NULL w unikalnej kolumnie. Jeśli ktoś zna rozwiązanie tego problemu, prosimy o opublikowanie go tutaj
dotNET
6
Faceci powinni przewinąć w dół i przeczytać odpowiedź z 600 głosami pozytywnymi. Nie jest już nieco ponad 100.
Luminous
1287

To, czego szukasz, jest rzeczywiście częścią standardów ANSI SQL: 92, SQL: 1999 i SQL: 2003, tzn. Ograniczenie UNIKALNE musi uniemożliwiać powielanie wartości innych niż NULL, ale akceptować wiele wartości NULL.

Jednak w świecie SQL Server firmy Microsoft dozwolona jest jedna wartość NULL, ale wiele wartości NULL nie jest ...

W SQL Server 2008 można zdefiniować unikalny filtrowany indeks na podstawie predykatu, który wyklucza wartości NULL:

CREATE UNIQUE NONCLUSTERED INDEX idx_yourcolumn_notnull
ON YourTable(yourcolumn)
WHERE yourcolumn IS NOT NULL;

We wcześniejszych wersjach można zastosować WIDOKI z predykatem NOT NULL, aby wymusić ograniczenie.

Vincent Buck
źródło
3
jest to prawdopodobnie najlepszy sposób na zrobienie tego. nie jesteś pewien, czy ma to wpływ na wydajność? ktoś?
Simon_Weaver
3
Próbuję zrobić to dokładnie w wersji SQL Server 2008 Express i otrzymuję następujący błąd: UTWÓRZ UNIKALNY INDEKS NIEKLUSTRACYJNY UC_MailingId NA [SLS-CP] .dbo.MasterFileEntry (MailingId) GDZIE MailingId NIE JEST NULL Wynik w: Msg 156, Poziom 15, stan 1, wiersz 3 Niepoprawna składnia w pobliżu słowa kluczowego „GDZIE”. Jeśli usunę klauzulę where, DDL działa dobrze, ale oczywiście nie robi tego, czego potrzebuję. Jakieś pomysły?
Kenneth Baltrinic
4
O ile się nie mylę, nie możesz utworzyć klucza obcego na podstawie unikalnego indeksu, tak jak możesz wyłączyć unikalne ograniczenie. (Przynajmniej SSMS narzekał na mnie, kiedy próbowałem.) Byłoby miło mieć możliwość zerowania kolumny, która jest zawsze unikalna (gdy nie jest zerowa), która jest źródłem relacji klucza obcego.
Vaccano
8
Naprawdę świetna odpowiedź. Szkoda, że ​​został ukryty przez osobę zaakceptowaną jako odpowiedź. To rozwiązanie prawie nie zwróciło mojej uwagi, ale teraz działa jak cuda w mojej implementacji.
Coral Doe
2
Inną alternatywą dla SQL 2005 i niższych jest Computing Column aka „Nullbuster”. stackoverflow.com/a/191729/132461 Dzięki temu nie musisz zaśmiecać bazy danych innym widokiem, zamiast tego masz po prostu inną kolumnę - zwykle o nazwie Kolumna A-Nullbuster, jeśli Kolumna A to ta, dla której chcesz mieć wartość ANSI dopuszczalną dla wartości UNIQUE. Umieść indeks UNIQUE (lub ograniczenie wyrażające zamiary biznesowe) w kolumnie A-Nullbuster, a wymusi on wyjątkowość w kolumnie A
DanO
34

SQL Server 2008 i nowsze wersje

Po prostu odfiltruj unikalny indeks:

CREATE UNIQUE NONCLUSTERED INDEX UQ_Party_SamAccountName
ON dbo.Party(SamAccountName)
WHERE SamAccountName IS NOT NULL;

W niższych wersjach widok zmaterializowany nadal nie jest wymagany

W przypadku SQL Server 2005 i wcześniejszych można to zrobić bez widoku. Właśnie dodałem unikalne ograniczenie, tak jakbyś prosił o jeden z moich stolików. Biorąc pod uwagę, że chcę wyjątkowość w kolumnie SamAccountName, ale chcę pozwolić na wiele wartości NULL, użyłem zmaterializowanej kolumny zamiast zmaterializowanego widoku:

ALTER TABLE dbo.Party ADD SamAccountNameUnique
   AS (Coalesce(SamAccountName, Convert(varchar(11), PartyID)))
ALTER TABLE dbo.Party ADD CONSTRAINT UQ_Party_SamAccountName
   UNIQUE (SamAccountNameUnique)

Musisz po prostu umieścić w kolumnie obliczeniowej coś, co będzie gwarantowane jako unikalne w całej tabeli, gdy rzeczywista pożądana unikalna kolumna ma wartość NULL. W tym przypadku PartyIDjest kolumna tożsamości, a numeryczne nigdy nie pasuje do żadnej SamAccountName, więc zadziałało dla mnie. Możesz wypróbować własną metodę - upewnij się, że rozumiesz domenę swoich danych, aby nie było możliwości krzyżowania się z rzeczywistymi danymi. Może to być tak proste, jak przygotowanie znaku wyróżniającego, takiego jak ten:

Coalesce('n' + SamAccountName, 'p' + Convert(varchar(11), PartyID))

Nawet jeśli PartyIDkiedyś stanie się nieliczbowy i może się pokrywać z SamAccountName, teraz nie będzie to miało znaczenia.

Zauważ, że obecność indeksu zawierającego kolumnę obliczoną domyślnie powoduje zapisanie każdego wyniku wyrażenia na dysku z innymi danymi w tabeli, co NIE wymaga dodatkowego miejsca na dysku.

Zauważ, że jeśli nie chcesz indeksu, możesz nadal oszczędzać CPU, ustawiając wyrażenie na dysk, dodając słowo kluczowe PERSISTEDna końcu definicji wyrażenia kolumny.

W SQL Server 2008 i nowszych wersjach zdecydowanie użyj filtrowanego rozwiązania, jeśli to możliwe!

Spór

Należy pamiętać, że niektórzy specjaliści od baz danych uznają to za przypadek „zastępczych wartości NULL”, które zdecydowanie mają problemy (głównie z powodu problemów z ustaleniem, kiedy coś jest rzeczywistą wartością lub wartością zastępczą dla brakujących danych ; mogą też występować problemy z liczbą mnogich wartości zastępczych innych niż NULL mnożącymi się jak szalone).

Uważam jednak, że ta sprawa jest inna. Dodana kolumna obliczeniowa nigdy nie zostanie wykorzystana do ustalenia czegokolwiek. Nie ma ono żadnego znaczenia i nie koduje żadnych informacji, które nie zostały jeszcze znalezione osobno w innych, poprawnie zdefiniowanych kolumnach. Nigdy nie należy go wybierać ani używać.

Tak więc, moja historia jest taka, że ​​to nie jest zastępcza wartość NULL i trzymam się tego! Ponieważ tak naprawdę nie chcemy wartości innej niż NULL w jakimkolwiek celu innym niż nakłonienie UNIQUEindeksu do zignorowania wartości NULL, w naszym przypadku użycia nie ma żadnych problemów, które pojawiają się podczas normalnego tworzenia zastępczej wartości NULL.

To powiedziawszy, zamiast tego nie mam problemu z użyciem widoku indeksowanego, ale wiąże się to z pewnymi problemami, takimi jak wymóg użycia SCHEMABINDING. Baw się dobrze, dodając nową kolumnę do tabeli podstawowej (musisz przynajmniej upuścić indeks, a następnie upuścić widok lub zmienić widok, aby nie był powiązany ze schematem). Zobacz pełną (długą) listę wymagań dotyczących tworzenia indeksowanego widoku w SQL Server (2005) (także późniejsze wersje), (2000) .

Aktualizacja

Jeśli twoja kolumna jest liczbowa, może istnieć wyzwanie, aby upewnić się, że użycie unikalnego ograniczenia Coalescenie spowoduje kolizji. W takim przypadku istnieje kilka opcji. Można użyć liczby ujemnej, aby „zastępować wartości NULL” tylko w zakresie ujemnym, a „wartości rzeczywiste” tylko w zakresie dodatnim. Alternatywnie można zastosować następujący wzór. W tabeli Issue(gdzie IssueIDjest PRIMARY KEY) może istnieć TicketID, ale nie musi , ale jeśli istnieje, musi być unikalna.

ALTER TABLE dbo.Issue ADD TicketUnique
   AS (CASE WHEN TicketID IS NULL THEN IssueID END);
ALTER TABLE dbo.Issue ADD CONSTRAINT UQ_Issue_Ticket_AllowNull
   UNIQUE (TicketID, TicketUnique);

Jeśli IssueID 1 ma bilet 123, UNIQUEograniczenie będzie dotyczyło wartości (123, NULL). Jeśli IssueID 2 nie ma biletu, będzie włączony (NULL, 2). Pewna myśl pokaże, że tego ograniczenia nie można powielić dla żadnego wiersza w tabeli i nadal dopuszcza wiele wartości NULL.

ErikE
źródło
16

W przypadku osób korzystających z programu Microsoft SQL Server Manager i chcących utworzyć indeks Unique, ale dopuszczający wartość Nullable, możesz utworzyć swój unikalny indeks, tak jak to zwykle bywało we właściwościach indeksu dla nowego indeksu, wybierz „Filtr” z lewego panelu, a następnie wpisz twój filtr (który jest twoją klauzulą ​​where). Powinien przeczytać coś takiego:

([YourColumnName] IS NOT NULL)

Działa to z MSSQL 2012

Howard
źródło
Jak utworzyć filtrowany indeks w Microsoft SQL Server Management Studio opisano tutaj i działa idealnie: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan
9

Kiedy zastosowałem poniższy unikalny indeks:

CREATE UNIQUE NONCLUSTERED INDEX idx_badgeid_notnull
ON employee(badgeid)
WHERE badgeid IS NOT NULL;

każda aktualizacja o wartości innej niż zero i wstawianie nie powiodło się z powodu błędu poniżej:

UPDATE nie powiodło się, ponieważ następujące opcje SET mają niepoprawne ustawienia: „ARITHABORT”.

Znalazłem to na MSDN

USTAW ARITHABORT musi być WŁĄCZONY podczas tworzenia lub zmiany indeksów w kolumnach obliczeniowych lub widokach indeksowanych. Jeśli SET ARITHABORT jest WYŁĄCZONY, instrukcje CREATE, UPDATE, INSERT i DELETE w tabelach z indeksami w kolumnach obliczeniowych lub widokach indeksowanych zakończą się niepowodzeniem.

Więc żeby to działało poprawnie, zrobiłem to

Kliknij prawym przyciskiem myszy [Baza danych] -> Właściwości -> Opcje -> Inne opcje -> Brak danych -> Włączone przerwanie arytmetyczne -> prawda

Wierzę, że można ustawić tę opcję w kodzie za pomocą

ALTER DATABASE "DBNAME" SET ARITHABORT ON

ale nie przetestowałem tego

Mike Taylor
źródło
6

Utwórz widok, który wybiera tylko nie- NULLkolumny i utwórz UNIQUE INDEXwidok:

CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Pamiętaj, że musisz wykonać INSERTiUPDATE ' na widoku zamiast tabeli.

Możesz to zrobić za pomocą INSTEAD OFwyzwalacza:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
źródło
więc czy muszę zmienić dal, aby wstawić do widoku?
Stuart,
1
Możesz utworzyć wyzwalacz INSTEAD OF INSERT.
Quassnoi,
6

Można to zrobić również u projektanta

Kliknij prawym przyciskiem myszy Indeks> Właściwości, aby wyświetlić to okno

zdobyć

Yonatan Tuchinsky
źródło
Bardzo fajna alternatywa, jeśli masz dostęp do projektanta
Francisco
Chociaż, jak właśnie odkryłem, gdy masz już dane w tabeli, nie możesz już używać projektanta. Wydaje się ignorować filtr i każdą próbę aktualizacje tabeli są spełnione z komunikatem „zdublowany klucz niedozwolone”
MortimerCat
4

Możliwe jest utworzenie unikalnego ograniczenia dla widoku indeksowanego klastrowanego

Możesz utworzyć widok w następujący sposób:

CREATE VIEW dbo.VIEW_OfYourTable WITH SCHEMABINDING AS
SELECT YourUniqueColumnWithNullValues FROM dbo.YourTable
WHERE YourUniqueColumnWithNullValues IS NOT NULL;

i wyjątkowe ograniczenie takie jak to:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
Lieven Keersmaekers
źródło
2

Może rozważyć „ INSTEAD OF” wyzwalacz i samemu sprawdzić? Z nieklastrowanym (nieunikalnym) indeksem w kolumnie, aby umożliwić wyszukiwanie.

Marc Gravell
źródło
1

Jak wspomniano wcześniej, SQL Server nie implementuje standardu ANSI, jeśli chodzi o UNIQUE CONSTRAINT. Jest na to bilet w Microsoft Connect od 2007 roku. Jak sugerowano tutaj i tutaj, najlepsze opcje na dziś to użycie filtrowanego indeksu, jak podano w innej odpowiedzi lub kolumnie obliczeniowej, np .:

CREATE TABLE [Orders] (
  [OrderId] INT IDENTITY(1,1) NOT NULL,
  [TrackingId] varchar(11) NULL,
  ...
  [ComputedUniqueTrackingId] AS (
      CASE WHEN [TrackingId] IS NULL
      THEN '#' + cast([OrderId] as varchar(12))
      ELSE [TrackingId_Unique] END
  ),
  CONSTRAINT [UQ_TrackingId] UNIQUE ([ComputedUniqueTrackingId])
)
Baris Akar
źródło
1

Możesz utworzyć wyzwalacz INSTEAD OF w celu sprawdzenia określonych warunków i błędu, jeśli są one spełnione. Tworzenie indeksu może być kosztowne w przypadku większych tabel.

Oto przykład:

CREATE TRIGGER PONY.trg_pony_unique_name ON PONY.tbl_pony
 INSTEAD OF INSERT, UPDATE
 AS
BEGIN
 IF EXISTS(
    SELECT TOP (1) 1 
    FROM inserted i
    GROUP BY i.pony_name
    HAVING COUNT(1) > 1     
    ) 
     OR EXISTS(
    SELECT TOP (1) 1 
    FROM PONY.tbl_pony t
    INNER JOIN inserted i
    ON i.pony_name = t.pony_name
    )
    THROW 911911, 'A pony must have a name as unique as s/he is. --PAS', 16;
 ELSE
    INSERT INTO PONY.tbl_pony (pony_name, stable_id, pet_human_id)
    SELECT pony_name, stable_id, pet_human_id
    FROM inserted
 END
Paweł
źródło
-1

Nie możesz tego zrobić z UNIQUEograniczeniem, ale możesz to zrobić za pomocą wyzwalacza.

    CREATE TRIGGER [dbo].[OnInsertMyTableTrigger]
   ON  [dbo].[MyTable]
   INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @Column1 INT;
    DECLARE @Column2 INT; -- allow nulls on this column

    SELECT @Column1=Column1, @Column2=Column2 FROM inserted;

    -- Check if an existing record already exists, if not allow the insert.
    IF NOT EXISTS(SELECT * FROM dbo.MyTable WHERE Column1=@Column1 AND Column2=@Column2 @Column2 IS NOT NULL)
    BEGIN
        INSERT INTO dbo.MyTable (Column1, Column2)
            SELECT @Column2, @Column2;
    END
    ELSE
    BEGIN
        RAISERROR('The unique constraint applies on Column1 %d, AND Column2 %d, unless Column2 is NULL.', 16, 1, @Column1, @Column2);
        ROLLBACK TRANSACTION;   
    END

END
Michael Brown
źródło
-1
CREATE UNIQUE NONCLUSTERED INDEX [UIX_COLUMN_NAME]
ON [dbo].[Employee]([Username] ASC) WHERE ([Username] IS NOT NULL) 
WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF, ONLINE = OFF, 
MAXDOP = 0) ON [PRIMARY];
użytkownik5536124
źródło
-1

kod ten, jeśli utworzysz formularz rejestracyjny w textBox i użyjesz insert, a ur textBox jest pusty i klikniesz przycisk Prześlij.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Ahmed Soliman Flasha
źródło