Come posso creare un vincolo univoco che consenta anche null?


620

Voglio avere un vincolo unico su una colonna che ho intenzione di popolare con i GUID. Tuttavia, i miei dati contengono valori null per questa colonna. Come posso creare il vincolo che consente più valori null?

Ecco uno scenario di esempio . Considera questo schema:

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

Quindi vedi questo codice per quello che sto cercando di ottenere:

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

L'istruzione finale non riesce con un messaggio:

Violazione del vincolo UNIQUE KEY 'UQ_People_LibraryCardId'. Impossibile inserire la chiave duplicata nell'oggetto "dbo.People".

Come posso modificare il mio schema e / o il vincolo di unicità in modo da consentire più NULLvalori, controllando ancora l'univocità sui dati effettivi?


Problema di connessione per la compatibilità standard con cui votare: connect.microsoft.com/SQLServer/Feedback/Details/299229
Vadzim


Vincolo UNICO e consentire NULL. ? È buon senso. Non è possibile
Flik

13
@flik, meglio non fare riferimento al "buon senso". Questo non è un argomento valido. Soprattutto se si considera che nullnon è un valore ma l'assenza di valore. Per lo standard SQL, nullnon è considerato uguale a null. Quindi perché il multiplo nulldovrebbe essere una violazione dell'unicità?
Frédéric,

Risposte:


144

SQL Server 2008 +

È possibile creare un indice univoco che accetta più NULL con una WHEREclausola. Vedi la risposta sotto .

Prima di SQL Server 2008

Non è possibile creare un vincolo UNIQUE e consentire NULL. È necessario impostare un valore predefinito di NEWID ().

Aggiorna i valori esistenti su NEWID () dove NULL prima di creare il vincolo UNIQUE.


2
e questo aggiungerà retrospettivamente valori alle righe esistenti, se è così è quello che devo fare, grazie?
Stuart,

1
Dovresti eseguire un'istruzione UPDATE per impostare i valori esistenti su NEWID () dove il campo esistente È NULL
Jose Basilio

55
Se si utilizza SQL Server 2008 o versioni successive, vedere la risposta di seguito con oltre 100 voti. Puoi aggiungere una clausola WHERE al tuo vincolo univoco.
Darren Griffith,

1
Questo stesso problema colpisce anche DataTables ADO.NET. Quindi anche se posso consentire null nel campo di supporto usando questo metodo, DataTable non mi consente di memorizzare NULL in una colonna univoca in primo luogo. Se qualcuno conosce una soluzione per questo, pubblicala qui
dotNET il

6
I ragazzi si assicurano di scorrere verso il basso e leggere la risposta con 600 voti positivi. Non è più di poco più di 100.
Luminoso

1289

Quello che stai cercando fa effettivamente parte degli standard ANSI SQL: 92, SQL: 1999 e SQL: 2003, ovvero un vincolo UNIQUE deve impedire valori duplicati non NULL ma accettare più valori NULL.

Nel mondo Microsoft di SQL Server, tuttavia, è consentito un singolo NULL ma più NULL non sono ...

In SQL Server 2008 , è possibile definire un indice filtrato univoco basato su un predicato che esclude i NULL:

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

Nelle versioni precedenti, è possibile ricorrere a VISUALIZZAZIONI con un predicato NOT NULL per applicare il vincolo.


3
questo è probabilmente il modo migliore per farlo. non sei sicuro che ci siano impatti sulle prestazioni? chiunque?
Simon_Weaver

3
Sto provando a fare esattamente questo nell'edizione di SQL Server 2008 Express e visualizzo un errore come segue: CREATE INDICE UNICO NON CONCLUSIVO UC_MailingId ON [SLS-CP] .dbo.MasterFileEntry (MailingId) DOVE MailingId NON È NULL Risultati in: Msg 156, Livello 15, stato 1, riga 3 Sintassi errata vicino alla parola chiave "WHERE". Se rimuovo la clausola where il DDL funziona bene, ma ovviamente non fa quello che mi serve. Qualche idea?
Kenneth Baltrinic,

4
A meno che non mi sbagli, non è possibile creare una chiave esterna da un indice univoco come è possibile disattivare un vincolo univoco. (Almeno SSMS si è lamentato con me quando ho provato.) Sarebbe bello poter avere una colonna nullable che sia sempre unica (quando non nulla) essere la fonte di una relazione di chiave esterna.
Vaccano,

8
Davvero un'ottima risposta. Peccato che sia stato nascosto da quello accettato come risposta. Questa soluzione non ha quasi attirato la mia attenzione, ma ora funziona come una meraviglia nella mia implementazione.
Coral Doe,

2
Un'altra alternativa a SQL 2005 e seguenti è un trucco della colonna calcolata noto anche come "Nullbuster". stackoverflow.com/a/191729/132461 Ti evita di ingombrare il database con un'altra vista, invece hai solo un'altra colonna - di solito Named ColumnA-Nullbuster se ColumnA è quella che vuoi essere ANSI nullable UNIQUE. Inserisci un indice UNICO (o un vincolo per esprimere l'intento commerciale) su ColumnA-Nullbuster e imporrà l'unicità su ColumnA
DanO,

34

SQL Server 2008 e versioni successive

Basta filtrare un indice univoco:

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

Nelle versioni inferiori, una vista materializzata non è ancora richiesta

Per SQL Server 2005 e versioni precedenti, è possibile farlo senza una vista. Ho appena aggiunto un vincolo unico come quello che stai chiedendo a uno dei miei tavoli. Dato che voglio l'univocità nella colonna SamAccountName, ma voglio consentire più NULL, ho usato una colonna materializzata piuttosto che una vista materializzata:

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

Devi semplicemente inserire qualcosa nella colonna calcolata che sarà garantita unica nell'intera tabella quando la colonna unica desiderata effettiva è NULL. In questo caso, PartyIDè una colonna identità ed essere numerico non corrisponderà mai a nessuno SamAccountName, quindi ha funzionato per me. Puoi provare il tuo metodo: assicurati di comprendere il dominio dei tuoi dati in modo che non vi sia alcuna possibilità di intersezione con dati reali. Potrebbe essere semplice come anteporre un personaggio differenziatore come questo:

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

Anche se PartyIDun giorno diventasse non numerico e potesse coincidere con un SamAccountName, ora non importa.

Si noti che la presenza di un indice che include la colonna calcolata comporta implicitamente il salvataggio di ogni risultato di espressione su disco con gli altri dati nella tabella, che occupa spazio su disco aggiuntivo.

Si noti che se non si desidera un indice, è comunque possibile salvare la CPU eseguendo il calcolo preliminare dell'espressione sul disco aggiungendo la parola chiave PERSISTEDalla fine della definizione dell'espressione di colonna.

In SQL Server 2008 e versioni successive, utilizza sicuramente la soluzione filtrata, se possibile!

Controversia

Si noti che alcuni professionisti del database vedranno questo come un caso di "NULL surrogati", che sicuramente hanno problemi (principalmente a causa di problemi nel tentativo di determinare quando qualcosa è un valore reale o un valore surrogato per i dati mancanti ; possono esserci anche problemi con il numero di valori surrogati non NULL che si moltiplicano come matti).

Tuttavia, credo che questo caso sia diverso. La colonna calcolata che sto aggiungendo non verrà mai utilizzata per determinare nulla. Non ha alcun significato e non codifica informazioni che non sono già state trovate separatamente in altre colonne correttamente definite. Non dovrebbe mai essere selezionato o utilizzato.

Quindi, la mia storia è che questo non è un NULL surrogato e mi sto attenendo ad esso! Poiché in realtà non vogliamo il valore non NULL per scopi diversi dall'inganno l' UNIQUEindice a ignorare i NULL, il nostro caso d'uso non presenta nessuno dei problemi che sorgono con la normale creazione NULL surrogata.

Detto questo, non ho alcun problema con l'utilizzo di una vista indicizzata, ma comporta alcuni problemi come il requisito dell'uso SCHEMABINDING. Divertiti ad aggiungere una nuova colonna alla tabella di base (dovrai almeno eliminare l'indice, quindi rilasciare la vista o modificare la vista per non essere associato allo schema). Vedere l' elenco completo (lungo) dei requisiti per la creazione di una vista indicizzata in SQL Server (2005) (anche versioni successive), (2000) .

Aggiornare

Se la colonna è numerica, potrebbe esserci la sfida di garantire che il vincolo univoco utilizzato Coalescenon provochi collisioni. In tal caso, ci sono alcune opzioni. Uno potrebbe essere quello di usare un numero negativo, per mettere i "NULL surrogati" solo nell'intervallo negativo, e i "valori reali" solo nell'intervallo positivo. In alternativa, è possibile utilizzare il modello seguente. Nella tabella Issue(dove si IssueIDtrova PRIMARY KEY), potrebbe esserci o meno un TicketID, ma se ce n'è uno, deve essere univoco.

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

Se IssueID 1 ha il ticket 123, il UNIQUEvincolo sarà sui valori (123, NULL). Se IssueID 2 non ha un ticket, sarà attivo (NULL, 2). Qualche pensiero mostrerà che questo vincolo non può essere duplicato per nessuna riga della tabella e consente comunque NULL multipli.


16

Per le persone che utilizzano Microsoft SQL Server Manager e desiderano creare un indice univoco ma nullable, è possibile creare un indice univoco come si farebbe normalmente nelle proprietà dell'indice per il nuovo indice, selezionare "Filtro" dal riquadro a sinistra, quindi immettere il tuo filtro (che è la tua clausola where). Dovrebbe leggere qualcosa del genere:

([YourColumnName] IS NOT NULL)

Funziona con MSSQL 2012


Come fare un indice filtrato in Microsoft SQL Server Management Studio è descritto qui e funziona perfettamente: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan

9

Quando ho applicato l'indice univoco di seguito:

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

ogni aggiornamento e inserimento non nulli non è riuscito con l'errore riportato di seguito:

AGGIORNAMENTO fallito perché le seguenti opzioni SET hanno impostazioni errate: 'ARITHABORT'.

L'ho trovato su MSDN

SET ARITHABORT deve essere ON quando si creano o si modificano gli indici su colonne calcolate o viste indicizzate. Se SET ARITHABORT è OFF, le istruzioni CREATE, UPDATE, INSERT e DELETE su tabelle con indici su colonne calcolate o viste indicizzate falliranno.

Quindi per farlo funzionare correttamente ho fatto questo

Fare clic con il tasto destro su [Database] -> Proprietà -> Opzioni -> Altre opzioni -> Misscellanea -> Aritmetica Abort abilitata -> true

Credo che sia possibile impostare questa opzione nel codice usando

ALTER DATABASE "DBNAME" SET ARITHABORT ON

ma non l'ho provato


6

Crea una vista che seleziona solo non NULLcolonne e crea UNIQUE INDEXla vista:

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

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Si noti che è necessario eseguire INSERT"e UPDATE" sulla vista anziché sulla tabella.

Puoi farlo con un INSTEAD OFtrigger:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END

quindi devo cambiare il mio dal per inserirlo nella vista?
Stuart,

1
È possibile creare un trigger INSTEAD OF INSERT.
Quassnoi,

6

Può essere fatto anche nel designer

Fare clic con il tasto destro del mouse su Indice> Proprietà per visualizzare questa finestra

catturare


Alternativa molto bella se hai accesso al designer
Francisco,

Anche se, come ho appena scoperto, una volta che hai i dati nella tua tabella, non puoi più usare il designer. Sembra ignorare il filtro e qualsiasi tentativo di aggiornamento della tabella viene soddisfatto con il messaggio "Chiave duplicata non consentita"
MortimerCat

4

È possibile creare un vincolo univoco su una vista indicizzata di gruppo

Puoi creare la vista in questo modo:

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

e il vincolo unico come questo:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)

2

Forse prendere in considerazione un " INSTEAD OF" grilletto e fare tu stesso il controllo? Con un indice non cluster (non univoco) sulla colonna per abilitare la ricerca.


1

Come affermato in precedenza, SQL Server non implementa lo standard ANSI quando si tratta di UNIQUE CONSTRAINT. Per questo esiste un ticket su Microsoft Connect dal 2007. Come suggerito qui e qui, le migliori opzioni di oggi sono utilizzare un indice filtrato come indicato in un'altra risposta o colonna calcolata, ad esempio:

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

1

È possibile creare un trigger INSTEAD OF per verificare condizioni specifiche ed errori se sono soddisfatti. La creazione di un indice può essere costosa su tabelle più grandi.

Ecco un esempio:

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

-1

Non puoi farlo con un UNIQUEvincolo, ma puoi farlo in un trigger.

    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

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

-1

questo codice se si crea un modulo di registrazione con textBox e si usa insert e il vostro textBox è vuoto e si fa clic sul pulsante di invio.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.