2009-05-14 33 views
72

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

Así que, 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 múltiples borrados registros con la misma ID.

Además de escribir disparadores, ¿puedo hacer eso?

estoy usando SQL Server 2005.

+1

Este diseño es un dolor común. ¿Ha considerado cambiar el diseño para que los registros "eliminados" teóricamente se eliminen físicamente de la tabla y tal vez se muevan a una tabla de "archivo"? – onedaywhen

+0

... porque la imposibilidad de escribir una restricción ÚNICA para aplicar una clave simple se debe considerar un 'olor a código', OMI. Si no puede cambiar el diseño (SQL DDL) porque muchas otras tablas hacen referencia a esta tabla, entonces apostaría que su SQL DML también sufre como resultado, es decir, debe recordar agregar ... Y Table.RecordStatus = 1 ' a la mayoría de las condiciones de búsqueda y condiciones de participación que involucran esta tabla y experimentar errores sutiles cuando inevitablemente se omite en ocasiones. – onedaywhen

Respuesta

33

Agregue una restricción de verificación como esta. La diferencia es, volverá falso si el estado = 1 y contador> 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; 
+0

miré las restricciones de verificación del nivel de la tabla, pero no veo que haya alguna forma de pasar los valores que se insertan o actualizan a la función, ¿sabes cómo hacerlo? –

+0

De acuerdo, publiqué un script de muestra que te ayudará a demostrar 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 puede agregar un segundo artículo con el mismo Id que está activo si ya hay uno activo. Puede modificar la lógica de manera que si hay una activa, no puede agregar ningún elemento con la misma identificación. Con este patrón, las posibilidades son infinitas. –

+0

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 por cada fila antes de que la declaración finalice Eso significa que la atomicidad de la declaración se ha roto y la función se expondrá a la base de datos en un estado inconsistente. Los resultados son impredecibles e inexactos ". Ver: http://blogs.conchango.com/davidportas/archive/2007/02/19/Trouble-with-CHECK-Constraints.aspx – onedaywhen

1

Porque, se le va a permitir duplicados, una restricción única no funcionará. Puede crear una restricción de comprobación para la columna RecordStatus y un procedimiento almacenado para INSERT que verifique los registros activos existentes antes de insertar identificadores duplicados.

9

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

+2

Eso es realmente muy inteligente Carl. No es una respuesta a la pregunta per se, pero es una buena solución. Si la tabla tiene muchas filas, 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 oposición a la restricción de verificación que escribí a continuación, que tiene que ejecutar un conteo. Me gusta. –

3

Usted puede hacer esto de una manera muy hacky ...

Crear una vista enlazada a un esquema en tu mesa

CREATE VIEW Cualquiera que sea SELECT * FROM tabla DONDE RecordStatus = 1

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

Una nota acerca de las vistas esquemáticas aunque, si cambia las tablas subyacentes, tendrá que volver a crear la vista. Un montón de trampas por eso.

+0

Esta es una sugerencia bastante buena, y no tan "hacky". Aquí hay más información sobre esta [alternativa de índice filtrado] (http://noelmckinney.com/2010/10/filtered-index-alternative/). –

+0

Es una mala idea.La pregunta no es así. – FabianoLothor

+0

Utilicé una vista esquemática unida, y nunca he repetido el error. Pueden ser un dolor real para trabajar. No es que tenga que volver a crear la vista si cambia la tabla subyacente; es posible que tenga que hacer eso para todas las vistas, al menos en el servidor SQL. Es que no puede cambiar la tabla sin antes abandonar la vista, lo que no podrá hacer sin antes eliminar las referencias a la misma. Ah, además, el almacenamiento podría ser problemático, ya sea por el espacio o por el costo que agrega para insertar y actualizar. – MattW

1

Si no puede usar NULL como RecordStatus como sugiere 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 en 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.

+0

buena idea, pero no hay función basada en el índice en el servidor sql gracias por la respuesta –

104

He aquí, the filtered index. De la documentación (énfasis mío):

Un índice filtrado es un índice optimizado no agrupado 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 la consulta y reducir el mantenimiento del índice y los costos de almacenamiento en comparación con los índices de tabla completa.

Y aquí es un ejemplo combinando un índice único con un predicado de filtro:

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

Esto refuerza esencialmente singularidad de ID cuando RecordStatus es 1.

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

+0

Tenga en cuenta que SQL Server requiere 'ansi_padding' para índices filtrados, así que asegúrese de que esta opción esté activada al ejecutar' SET ANSI_PADDING ON' antes creando un índice filtrado – naXa

Cuestiones relacionadas