¿Cómo creo una restricción única que también permita valores nulos?

620

Quiero tener una restricción única en una columna que voy a llenar con GUID. Sin embargo, mis datos contienen valores nulos para estas columnas. ¿Cómo creo la restricción que permite múltiples valores nulos?

Aquí hay un escenario de ejemplo . Considere este esquema:

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

Luego vea este código para lo que estoy tratando de lograr:

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

La declaración final falla con un mensaje:

Infracción de la restricción ÚNICA CLAVE 'UQ_People_LibraryCardId'. No se puede insertar una clave duplicada en el objeto 'dbo.People'.

¿Cómo puedo cambiar mi esquema y / o restricción de unicidad para que permita múltiples NULLvalores, sin dejar de verificar la unicidad en los datos reales?

Stuart
fuente
Problema de conexión por compatibilidad estándar para votar: connect.microsoft.com/SQLServer/Feedback/Details/299229
Vadzim
Restricción ÚNICA y permitir NULLs. ? Es de sentido común. No es posible
flik
13
@flik, mejor no se refiera al "sentido común". Ese no es un argumento válido. Especialmente cuando se considera que nullno es un valor sino la ausencia de valor. Según el estándar SQL, nullno se considera igual a null. Entonces, ¿por qué múltiples nulldebería ser una violación de unicidad?
Frédéric

Respuestas:

144

SQL Server 2008 +

Puede crear un índice único que acepte múltiples NULL con una WHEREcláusula. Ver la respuesta a continuación .

Antes de SQL Server 2008

No puede crear una restricción ÚNICA y permitir NULLs. Debe establecer un valor predeterminado de NEWID ().

Actualice los valores existentes a NEWID () donde NULL antes de crear la restricción UNIQUE.

Jose basilio
fuente
2
y esto agregará retrospectivamente valores a las filas existentes, si es así, esto es lo que necesito hacer, ¿gracias?
Stuart el
1
Debería ejecutar una instrucción UPDATE para establecer los valores existentes en NEWID () donde el campo existente ES NULO
Jose Basilio
55
Si está utilizando SQL Server 2008 o posterior, consulte la respuesta a continuación con más de 100 votos a favor. Puede agregar una cláusula WHERE a su restricción única.
Darren Griffith
1
Este mismo problema también afecta a las tablas de datos de ADO.NET. Entonces, incluso si puedo permitir nulos en el campo de respaldo utilizando este método, DataTable no me permite almacenar NULL en una columna única en primer lugar. Si alguien conoce una solución para eso,
publíquelo
66
Los chicos se aseguran de desplazarse hacia abajo y leer la respuesta con 600 votos a favor. Ya no es más de 100.
Luminoso
1289

Lo que está buscando es, de hecho, parte de los estándares ANSI SQL: 92, SQL: 1999 y SQL: 2003, es decir, una restricción ÚNICA debe rechazar valores duplicados que no sean NULL pero aceptar múltiples valores NULL.

Sin embargo, en el mundo de Microsoft de SQL Server, se permite un solo NULL pero no se admiten varios NULL ...

En SQL Server 2008 , puede definir un índice filtrado único basado en un predicado que excluya NULL:

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

En versiones anteriores, puede recurrir a VIEWS con un predicado NOT NULL para aplicar la restricción.

Vincent Buck
fuente
3
Esta es probablemente la mejor manera de hacer esto. ¿No está seguro si hay algún impacto en el rendimiento? ¿nadie?
Simon_Weaver
3
Estoy tratando de hacer exactamente esto en SQL Server 2008 Express Edition y obtengo el siguiente error: CREAR ÍNDICE NO CLUSIFICADO ÚNICO UC_MailingId ON [SLS-CP] .dbo.MasterFileEntry (MailingId) DONDE MailingId NO ES NULL Resultados en: Msg 156, Nivel 15, Estado 1, Línea 3 Sintaxis incorrecta cerca de la palabra clave 'WHERE'. Si elimino la cláusula where, el DDL funciona bien, pero, por supuesto, no hace lo que necesito. ¿Algunas ideas?
Kenneth Baltrinic
44
A menos que me equivoque, no puede crear una clave externa a partir de un índice único como puede hacerlo con una restricción única. (Al menos SSMS se quejó de mí cuando lo intenté). Sería bueno poder tener una columna anulable que siempre sea única (cuando no sea nula) como fuente de una relación de clave externa.
Vaccano
8
Realmente una gran respuesta. Lástima que estaba oculto por el aceptado como respuesta. Esta solución casi no me llamó la atención, pero ahora funciona de maravilla en mi implementación.
Coral Doe
2
Otra alternativa para SQL 2005 y siguientes es un truco de Columna Computada, también conocido como "Nullbuster". stackoverflow.com/a/191729/132461 Le ahorra el desorden de la base de datos con otra vista, solo tiene otra columna en su lugar, generalmente denominada ColumnA-Nullbuster si ColumnA es la que desea que sea ANSI nullable UNIQUE. Coloque un índice ÚNICO (o restricción para expresar la intención comercial) en ColumnA-Nullbuster y hará cumplir la unicidad en ColumnA
DanO
34

SQL Server 2008 y hasta

Simplemente filtre un índice único:

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

En versiones inferiores, todavía no se requiere una vista materializada

Para SQL Server 2005 y versiones anteriores, puede hacerlo sin una vista. Acabo de agregar una restricción única como la que estás pidiendo a una de mis tablas. Dado que quiero unicidad en la columna SamAccountName, pero quiero permitir múltiples NULL, utilicé una columna materializada en lugar de una vista materializada:

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

Simplemente tiene que poner algo en la columna calculada que se garantizará como único en toda la tabla cuando la columna única deseada real sea NULL. En este caso, PartyIDes una columna de identidad y ser numérico nunca coincidirá con ninguna SamAccountName, por lo que funcionó para mí. Puede probar su propio método: asegúrese de comprender el dominio de sus datos para que no haya posibilidad de intersección con datos reales. Eso podría ser tan simple como anteponer un carácter diferenciador como este:

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

Incluso si PartyIDalgún día se volviera no numérico y pudiera coincidir con un SamAccountName, ahora no importará.

Tenga en cuenta que la presencia de un índice que incluye la columna calculada hace que implícitamente cada resultado de la expresión se guarde en el disco con los otros datos de la tabla, lo que NO requiere espacio en disco adicional.

Tenga en cuenta que si no desea un índice, aún puede guardar la CPU haciendo que la expresión se calcule previamente en el disco agregando la palabra clave PERSISTEDal final de la definición de expresión de columna.

En SQL Server 2008 y versiones posteriores, ¡definitivamente use la solución filtrada si es posible!

Controversia

Tenga en cuenta que algunos profesionales de bases de datos verán esto como un caso de "NULL sustitutos", que definitivamente tienen problemas (principalmente debido a problemas relacionados con el intento de determinar cuándo algo es un valor real o un valor sustituto para los datos faltantes) ; también puede haber problemas con el número de valores sustitutos no NULL que se multiplican como locos).

Sin embargo, creo que este caso es diferente. La columna calculada que estoy agregando nunca se usará para determinar nada. No tiene ningún significado en sí mismo y no codifica ninguna información que no se encuentre por separado en otras columnas definidas correctamente. Nunca debe seleccionarse o usarse.

Por lo tanto, mi historia es que este no es un NULL sustituto, ¡y lo estoy cumpliendo! Dado que en realidad no queremos el valor no NULL para ningún otro propósito que no sea engañar alUNIQUE índice para que ignore los NULL, nuestro caso de uso no tiene ninguno de los problemas que surgen con la creación NULL sustituta normal.

Dicho todo esto, no tengo ningún problema con el uso de una vista indexada, pero trae algunos problemas, como el requisito de usar SCHEMABINDING. Diviértase agregando una nueva columna a su tabla base (como mínimo, tendrá que soltar el índice y luego soltar la vista o modificar la vista para que no esté vinculada al esquema). Consulte la lista completa (larga) de requisitos para crear una vista indizada en SQL Server (2005) (también versiones posteriores), (2000) .

Actualizar

Si su columna es numérica, puede existir el desafío de garantizar que el uso exclusivo de la restricción Coalesceno provoque colisiones. En ese caso, hay algunas opciones. Una podría ser utilizar un número negativo, poner los "NULL sustitutos" solo en el rango negativo y los "valores reales" solo en el rango positivo. Alternativamente, se podría usar el siguiente patrón. En la tabla Issue(donde IssueIDestá el PRIMARY KEY), puede haber o no un TicketID, pero si hay uno, debe ser único.

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

Si el IssueID 1 tiene el ticket 123, la UNIQUErestricción estará en los valores (123, NULL). Si IssueID 2 no tiene ticket, estará encendido (NULL, 2). Algún pensamiento mostrará que esta restricción no se puede duplicar para ninguna fila de la tabla y aún permite múltiples NULL.

ErikE
fuente
16

Para las personas que usan Microsoft SQL Server Manager y desean crear un índice único pero anulable, puede crear su índice único como lo haría normalmente en sus Propiedades de índice para su nuevo índice, seleccione "Filtro" en el panel izquierdo, luego ingrese su filtro (que es su cláusula where). Debería leer algo como esto:

([YourColumnName] IS NOT NULL)

Esto funciona con MSSQL 2012

Howard
fuente
Aquí se describe cómo hacer un índice filtrado en Microsoft SQL Server Management Studio y funciona perfectamente: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan
9

Cuando apliqué el índice único a continuación:

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

cada actualización e inserción no nula fallaron con el siguiente error:

La ACTUALIZACIÓN falló porque las siguientes opciones SET tienen configuraciones incorrectas: 'ARITHABORT'.

Encontré esto en MSDN

SET ARITHABORT debe estar ENCENDIDO cuando está creando o cambiando índices en columnas calculadas o vistas indexadas. Si SET ARITHABORT está desactivado, las instrucciones CREATE, UPDATE, INSERT y DELETE en tablas con índices en columnas calculadas o vistas indexadas fallarán.

Así que para que esto funcione correctamente hice esto

Haga clic con el botón derecho en [Base de datos] -> Propiedades -> Opciones -> Otras opciones -> Varios -> Aborto aritmético habilitado -> verdadero

Creo que es posible configurar esta opción en código usando

ALTER DATABASE "DBNAME" SET ARITHABORT ON

pero no he probado esto

Mike Taylor
fuente
6

Cree una vista que seleccione solo no NULLcolumnas y cree UNIQUE INDEXen la vista:

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

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Tenga en cuenta que deberá realizar INSERT's yUPDATE ' s en la vista en lugar de en la tabla.

Puede hacerlo con un INSTEAD OFdisparador:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
fuente
Entonces, ¿necesito cambiar mi dal para insertar en la vista?
Stuart el
1
Puede crear un activador EN LUGAR DE INSERTAR.
Quassnoi
6

También se puede hacer en el diseñador

Haga clic derecho en el Índice> Propiedades para obtener esta ventana

capturar

Yonatan Tuchinsky
fuente
Muy buena alternativa si tienes acceso al diseñador
Francisco
Aunque, como acabo de descubrir, una vez que tiene datos en su tabla, ya no puede usar el diseñador. Parece ignorar el filtro y cualquier intento de actualización de la tabla se encuentra con el mensaje "No se permite duplicar clave"
MortimerCat
4

Es posible crear una restricción única en una Vista indizada en clúster

Puede crear la Vista de esta manera:

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

y la restricción única como esta:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
Lieven Keersmaekers
fuente
2

¿Quizás considere un " INSTEAD OF" disparador y haga la verificación usted mismo? Con un índice no agrupado (no exclusivo) en la columna para habilitar la búsqueda.

Marc Gravell
fuente
1

Como se indicó anteriormente, SQL Server no implementa el estándar ANSI cuando se trata de eso UNIQUE CONSTRAINT. Hay un boleto en Microsoft Connect para esto desde 2007. Como se sugiere aquí y aquí, las mejores opciones a partir de hoy son usar un índice filtrado como se indica en otra respuesta o una columna calculada, por ejemplo:

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
fuente
1

Puedes crear un INSTEAD OF OF desencadenador para verificar condiciones específicas y errores si se cumplen. Crear un índice puede ser costoso en tablas más grandes.

Aquí hay un ejemplo:

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
Pablo
fuente
-1

No puede hacer esto con una UNIQUErestricción, pero puede hacerlo en un disparador.

    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
fuente
-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];
usuario5536124
fuente
-1

este código si realiza un formulario de registro con textBox y utiliza insert y ur textBox está vacío y hace clic en el botón Enviar.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Ahmed Soliman Flasha
fuente