¿Puedo agregar una restricción única que ignore las violaciones existentes?

41

Tengo una tabla que actualmente tiene valores duplicados en una columna.

No puedo eliminar estos duplicados erróneos, pero me gustaría evitar que se agreguen valores adicionales no únicos.

¿Puedo crear uno UNIQUEque no verifique el cumplimiento existente?

He intentado usar NOCHECKpero no tuve éxito.

En este caso, tengo una tabla que vincula la información de licencia a "CompanyName"

EDITAR: Tener varias filas con el mismo "CompanyName" son datos incorrectos, pero no podemos eliminar o actualizar esos duplicados en este momento. Un enfoque es hacer que los INSERTs utilicen un procedimiento almacenado que fallará para los duplicados ... Si fuera posible que SQL verificara la unicidad por sí mismo, sería preferible.

Estos datos se consultan por el nombre de la empresa. Para los pocos duplicados existentes, esto significará que se devuelven y muestran varias filas ... Si bien esto es incorrecto, es aceptable en nuestro caso de uso. El objetivo es prevenirlo en el futuro. Me parece por los comentarios que tengo que hacer esta lógica en los procedimientos almacenados.

Mateo
fuente
¿Se le permite cambiar la tabla (agregar una columna más)?
ypercubeᵀᴹ
@ypercube desafortunadamente no.
Mateo

Respuestas:

34

La respuesta es sí". Puede hacerlo con un índice filtrado (consulte aquí la documentación).

Por ejemplo, puedes hacer:

create unique index t_col on t(col) where id > 1000;

Esto crea un índice único, solo en filas nuevas , en lugar de en las filas antiguas. Esta formulación particular permitiría duplicados con valores existentes.

Si solo tiene un puñado de duplicados, puede hacer algo como:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
Gordon Linoff
fuente
2
Si eso es bueno o no dependerá de si los elementos existentes "antiguos" deberían evitar la creación de elementos nuevos con el mismo valor.
supercat
1
@Super gato . . . Di una formulación alternativa para construir el índice en todo excepto los valores duplicados existentes.
Gordon Linoff
1
Para que este último funcione, uno tendría que asegurarse de que se omitiera de la lista una identificación para cada valor de clave distinto que tuviera duplicados, y también tendría que asegurarse de que si el elemento que se omitió deliberadamente de la lista se eliminara de la tabla , un elemento con una clave igual se eliminaría de la lista.
supercat
@Super gato . . . Estoy de acuerdo. Mantener el índice consistente para actualizaciones y eliminaciones es aún más desafiante porque no puede volver a crear el índice en un disparador. En cualquier caso, tuve la impresión del OP de que los datos, o al menos los duplicados, no cambian a menudo, si es que lo hacen.
Gordon Linoff
¿Por qué no excluir una lista de valores en lugar de una lista de ID? Entonces no tiene que excluir una ID por valor duplicado de la lista de ID excluidas
JMD Coalesce
23

Si tu puedes hacerlo.

Aquí hay una tabla con duplicados:

CREATE TABLE dbo.Party
  (
    ID INT NOT NULL
           IDENTITY ,
    CONSTRAINT PK_Party PRIMARY KEY ( ID ) ,
    Name VARCHAR(30) NOT NULL
  ) ;
GO

INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' ),
        ( 'Luke Skywalker' ),
        ( 'Luke Skywalker' ),
        ( 'Harry Potter' ) ;
GO

Ignoremos los existentes y asegurémonos de que no se puedan agregar nuevos duplicados:

-- Add a new column to mark grandfathered duplicates.
ALTER TABLE dbo.Party ADD IgnoreThisDuplicate INT NULL ;
GO

-- The *first* instance will be left NULL.
-- *Secondary* instances will be set to their ID (a unique value).
UPDATE  dbo.Party
SET     IgnoreThisDuplicate = ID
FROM    dbo.Party AS my
WHERE   EXISTS ( SELECT *
                 FROM   dbo.Party AS other
                 WHERE  other.Name = my.Name
                        AND other.ID < my.ID ) ;
GO

-- This constraint is not strictly necessary.
-- It prevents granting further exemptions beyond the ones we made above.
ALTER TABLE dbo.Party WITH NOCHECK
ADD CONSTRAINT CHK_Party_NoNewExemptions 
CHECK(IgnoreThisDuplicate IS NULL);
GO

SELECT * FROM dbo.Party;
GO

-- **THIS** is our pseudo-unique constraint.
-- It works because the grandfathered duplicates have a unique value (== their ID).
-- Non-grandfathered records just have NULL, which is not unique.
CREATE UNIQUE INDEX UNQ_Party_UniqueNewNames ON dbo.Party(Name, IgnoreThisDuplicate);
GO

Probemos esta solución:

-- cannot add a name that exists
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.

-- cannot add a name that exists and has an ignored duplicate
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Luke Skywalker' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.


-- can add a new name 
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

-- but only once
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.
Alaska
fuente
44
Excepto que no puede agregar una columna a la tabla.
Aaron Bertrand
3
Me gusta cómo esta respuesta convierte cómo los valores NULL se tratan de manera no estándar en una restricción única en algo útil. Truco astuto.
ypercubeᵀᴹ
@ ypercubeᵀᴹ, ¿podría explicar qué no es estándar sobre el manejo NULL en restricciones únicas? ¿Cómo es diferente de lo que esperabas? ¡Gracias!
Noach
1
@Noach en SQL Server, una UNIQUErestricción en una columna anulable asegura que haya como máximo un único NULLvalor. El estándar SQL (y casi todos los demás DBMS SQL) dice que debe permitir cualquier número de NULLvalores (es decir, la restricción debe ignorar los valores nulos).
ypercubeᵀᴹ
@ ypercubeᵀᴹ Entonces, para implementar esto en un DBMS diferente, solo necesitamos usar DEFAULT 0 en lugar de NULL. ¿Correcto?
Noach
16

El índice único filtrado es una idea brillante, pero tiene una desventaja menor, no importa si usa la WHERE identity_column > <current value>condición o el WHERE identity_column NOT IN (<list of ids for duplicate values here>).

Con el primer enfoque, aún podrá insertar datos duplicados en el futuro, duplicados de datos existentes (ahora). Por ejemplo, si tiene (incluso una) fila ahora con CompanyName = 'Software Inc.', el índice no prohibirá la inserción de una fila más con el mismo nombre de la compañía. Solo lo prohibirá si lo intentas dos veces.

Con el segundo enfoque hay una mejora, lo anterior no funcionará (lo cual es bueno). Sin embargo, aún podrá insertar más duplicados o duplicados existentes. Por ejemplo, si tiene (dos o más) filas ahora con CompanyName = 'DoubleData Co.', el índice no prohibirá la inserción de una fila más con el mismo nombre de la compañía. Solo lo prohibirá si lo intentas dos veces.

(Actualización) Esto se puede corregir si por cada nombre duplicado, se mantiene fuera de la lista de exclusión un ID. Si, como en el ejemplo anterior, hay 4 filas con duplicados CompanyName = DoubleData Co.e ID 4,6,8,9, la lista de exclusión debe tener solo 3 de estos ID.

Con el segundo enfoque, otra desventaja es la condición engorrosa (cuánto engorroso depende de cuántos duplicados hay en primer lugar), ya que SQL-Server parece no admitir el NOT INoperador en la WHEREparte de los índices filtrados. Ver SQL-Fiddle . En lugar de eso WHERE (CompanyID NOT IN (3,7,4,6,8,9)), tendrá que tener algo así como WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)no estoy seguro si hay implicaciones de eficiencia con tal condición, si tiene cientos de nombres duplicados.


Otra solución (similar a la de @Alex Kuznetsov) es agregar otra columna, llenarla con números de rango y agregar un índice único que incluya esta columna:

ALTER TABLE Company
  ADD Rn TINYINT DEFAULT 1;

UPDATE x
SET Rn = Rnk
FROM
  ( SELECT 
      CompanyID,
      Rn,
      Rnk = ROW_NUMBER() OVER (PARTITION BY CompanyName 
                               ORDER BY CompanyID)
    FROM Company 
  ) x ;

CREATE UNIQUE INDEX CompanyName_UQ 
  ON Company (CompanyName, Rn) ; 

Luego, la inserción de una fila con nombre duplicado fallará debido a la DEFAULT 1propiedad y al índice único. Esto todavía no es 100% infalible (mientras que Alex lo es). Los duplicados seguirán apareciendo si Rnse establece explícitamente en la INSERTdeclaración o si los Rnvalores se actualizan maliciosamente.

SQL-Fiddle-2

ypercubeᵀᴹ
fuente
-2

Otra alternativa es escribir una función escalar que verifique si ya existe un valor en la tabla y luego llamar a esa función desde una restricción de verificación.

Esto hará cosas horribles para el rendimiento.

Caminante de piedra verde
fuente
Además de los problemas señalados por Aaron, la respuesta no explica cómo se puede agregar esta restricción de verificación, por lo que ignora los duplicados existentes.
ypercubeᵀᴹ
-2

Estoy buscando lo mismo: cree un índice único no confiable para que se ignoren los datos incorrectos existentes, pero los nuevos registros no pueden ser duplicados de nada que ya exista.

Mientras leo este hilo, se me ocurre que una mejor solución es escribir un disparador que verifique [insertado] en la tabla principal para ver si hay duplicados, y si existen duplicados entre esas tablas, ROLLBACK TRAN.

Puntilla
fuente