restricción única condicional

93

Tengo una situación en la que necesito aplicar una restricción única en un conjunto de columnas, pero solo para un valor de una columna.

Entonces, por ejemplo, tengo una tabla como Table (ID, Name, RecordStatus).

RecordStatus solo puede tener un valor 1 o 2 (activo o eliminado), y quiero crear una restricción única en (ID, RecordStatus) solo cuando RecordStatus = 1, ya que no me importa si hay varios registros eliminados con el mismo CARNÉ DE IDENTIDAD.

Aparte de escribir desencadenantes, ¿puedo hacer eso?

Estoy usando SQL Server 2005.

np-hard
fuente
1
Este diseño es un dolor común. ¿Ha considerado cambiar el diseño para que los registros teóricamente 'eliminados' se eliminen físicamente de la tabla y tal vez se muevan a una tabla de 'archivo'?
cuando
1
... porque la incapacidad de escribir una restricción ÚNICA para hacer cumplir una clave simple debe considerarse un 'olor a código', en mi opinión. Si no puede cambiar el diseño (SQL DDL) porque muchas otras tablas hacen referencia a esta tabla, apuesto a que su SQL DML también sufre como resultado, es decir, debe recordar agregar ... AND Table.RecordStatus = 1 ' a la mayoría de las condiciones de búsqueda y las condiciones de combinación que involucran esta tabla y experimentan errores sutiles cuando inevitablemente se omite en ocasiones.
cuando

Respuestas:

36

Agregue una restricción de verificación como esta. La diferencia es que devolverá falso si Estado = 1 y Recuento> 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
fuente
Miré las restricciones de verificación a nivel de tabla pero no parece que haya ninguna forma de pasar los valores que se insertan o actualizan a la función, ¿sabe cómo hacerlo?
np-hard
De acuerdo, publiqué un guión de muestra que te ayudará a probar de lo que estoy hablando. Lo probé y funciona. Si miras las dos líneas comentadas, verás el mensaje que recibo. Nota bene, en mi implementación, simplemente me aseguro de que no pueda agregar un segundo elemento con el mismo ID que esté activo si ya hay uno activo. Puede modificar la lógica de modo que si hay uno activo, no puede agregar ningún elemento con la misma identificación. Con este patrón, las posibilidades son infinitas.
D. Patrick
Preferiría la misma lógica en un disparador. "una consulta en una función escalar ... puede crear grandes problemas si su restricción CHECK se basa en una consulta y si más de una fila se ve afectada por alguna actualización. Lo que sucede es que la restricción se verifica una vez para cada fila antes de que se complete la declaración . Eso significa que la atomicidad de la declaración está rota y la función estará expuesta a la base de datos en un estado inconsistente. Los resultados son impredecibles e inexactos ". Ver: blogs.conchango.com/davidportas/archive/2007/02/19/…
cuando
Eso es solo parcialmente cierto un día cuando. La base de datos se comporta de manera consistente y predecible. La restricción de verificación se ejecutará después de que la fila se agregue a la tabla y antes de que la dbms confirme la transacción y puede contar con eso. Ese blog hablaba de un problema bastante singular en el que necesita ejecutar la restricción contra un conjunto de inserciones en lugar de solo una inserción a la vez. ashish solicita una restricción en un inserto a la vez y esta restricción funcionará de manera precisa, predecible y consistente. Lo siento si esto sonó conciso; Me estaba quedando sin personajes.
D. Patrick
3
Esto funciona muy bien para inserciones, pero no parece funcionar para actualizaciones. Por ejemplo, agregar esto después de las otras inserciones funciona aquí cuando no lo esperaba. INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2); actualizar CheckConstraint set Recordstatus = 1 donde nombre = 'No ProblemsA'
dwidel
149

He aquí el índice filtrado . De la documentación (el énfasis es mío):

Un índice filtrado es un índice no agrupado optimizado especialmente adecuado para cubrir consultas que seleccionan de un subconjunto de datos bien definido. Utiliza un predicado de filtro para indexar una parte de las filas de la tabla. Un índice filtrado bien diseñado puede mejorar el rendimiento de las consultas y reducir los costos de almacenamiento y mantenimiento del índice en comparación con los índices de tabla completa.

Y aquí hay un ejemplo que combina un índice único con un predicado de filtro:

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

Esto esencialmente refuerza la singularidad de IDcuándo RecordStatuses1 .

Tras la creación de ese índice, una infracción de unicidad generará un error:

Msg 2601, nivel 14, estado 1, línea 13
No se puede insertar una fila de clave duplicada en el objeto 'dbo.MyTable' con índice único 'MyIndex'. El valor de la clave duplicada es (9999).

Nota: el índice filtrado se introdujo en SQL Server 2008. Para versiones anteriores de SQL Server, consulte esta respuesta .

canon
fuente
Tenga en cuenta que SQL Server requiere ansi_paddingíndices filtrados, así que asegúrese de que esta opción esté activada ejecutándola SET ANSI_PADDING ONantes de crear un índice filtrado.
naXa
10

Puede mover los registros eliminados a una tabla que carece de la restricción, y quizás usar una vista con UNION de las dos tablas para preservar la apariencia de una sola tabla.

Carl Manaster
fuente
2
De hecho, es bastante inteligente Carl. No es una respuesta a la pregunta en sí, pero es una buena solución. Si la tabla tiene muchas filas, eso también podría acelerar la búsqueda de un registro activo porque podría mirar la tabla de registros activos. También aceleraría la restricción porque la restricción única usa un índice en lugar de la restricción de verificación que escribí a continuación, que tiene que ejecutar un recuento. Me gusta.
D. Patrick
3

Puedes hacer esto de una manera realmente hacky ...

Cree una vista enlazada al esquema en su mesa.

CREAR VISTA Lo que sea SELECT * FROM Table WHERE RecordStatus = 1

Ahora cree una restricción única en la vista con los campos que desee.

Sin embargo, una nota sobre las vistas vinculadas a esquemas, si cambia las tablas subyacentes, tendrá que volver a crear la vista. Un montón de trampas por eso.

Min
fuente
Esta es una sugerencia bastante buena, y no tan "hacky". Aquí hay más información sobre esta alternativa de índice filtrado .
Scott Whitlock
Es una mala idea. La pregunta no es eso.
FabianoLothor
Usé una vista enlazada a esquema una vez y nunca he repetido el error. Puede ser un dolor real trabajar con ellos. No es que tenga que volver a crear la vista si cambia la tabla subyacente; potencialmente tiene que hacerlo para todas las vistas, al menos en el servidor SQL. Es que no puede cambiar la tabla sin primero eliminar la vista, lo que es posible que no pueda hacer sin eliminar primero las referencias a ella. Ah, además, el almacenamiento podría ser problemático, ya sea por el espacio o por el costo que implica insertar y actualizar.
MattW
1

Debido a que va a permitir duplicados, una restricción única no funcionará. Puede crear una restricción de verificación para la columna RecordStatus y un procedimiento almacenado para INSERT que verifica los registros activos existentes antes de insertar ID duplicados.

Ichibán
fuente
1

Si no puede usar NULL como RecordStatus como sugirió Bill, podría combinar su idea con un índice basado en funciones. Cree una función que devuelva NULL si RecordStatus no es uno de los valores que desea considerar en su restricción (y RecordStatus en caso contrario) y cree un índice sobre eso.

Eso tendrá la ventaja de que no tiene que examinar explícitamente otras filas de la tabla en su restricción, lo que podría causarle problemas de rendimiento.

Debo decir que no conozco el servidor SQL en absoluto, pero he utilizado con éxito este enfoque en Oracle.

Obrero temporal
fuente
buena idea, pero no hay una función indexada en el servidor sql, gracias por la respuesta
np-hard