Utilice la función "LEN" en la cláusula "WHERE" en "CREATE INDIQUE ÚNICO"

12

Tengo esta tabla:

CREATE TABLE Table01 (column01 nvarchar(100));

Y quiero crear un índice único en column01 con esta condición LEN (column01)> = 5

Lo intenté:

CREATE UNIQUE INDEX UIX_01 ON Table01(column01) WHERE LEN(column01) >= 5;

Tengo:

Cláusula WHERE incorrecta para el índice filtrado 'UIX_01' en la tabla 'Table01'.

Y:

ALTER TABLE Table01 ADD column01_length AS (LEN(column01));
CREATE UNIQUE INDEX UIX_01 ON Table01(column01) WHERE column01_length >= 5;

Produce:

El índice filtrado 'UIX_01' no se puede crear en la tabla 'Tabla01' porque la columna 'column01_length' en la expresión del filtro es una columna calculada. Vuelva a escribir la expresión del filtro para que no incluya esta columna.

friki
fuente

Respuestas:

15

Un método para solucionar la restricción del índice filtrado es con una vista indizada:

CREATE TABLE dbo.Table01 (
  Column01 NVARCHAR(100)
);
GO

CREATE VIEW dbo.vw_Table01_Column01_LenOver5Unique
WITH SCHEMABINDING AS
SELECT Column01
FROM dbo.Table01
WHERE LEN(Column01) >= 5;
GO

CREATE UNIQUE CLUSTERED INDEX cdx
    ON dbo.vw_Table01_Column01_LenOver5Unique(Column01);
GO

INSERT INTO dbo.Table01 VALUES('1'); --success
INSERT INTO dbo.Table01 VALUES('1'); --success
INSERT INTO dbo.Table01 VALUES('55555'); --success
INSERT INTO dbo.Table01 VALUES('55555'); --duplicate key error
GO

EDITAR:

¿Cómo debo definir la vista si tengo dos columnas en el índice? CREAR ÍNDICE ÚNICO UIX_01 EN Table01 (column01, column02) DONDE LEN (column01)> = 5

El enfoque de vista indexada se puede extender para una clave compuesta agregando otras columnas clave a la definición e índice de la vista. Se aplica el mismo filtro en la definición de la vista, pero la unicidad de las filas de calificación impuestas por la clave compuesta en lugar del valor de columna única:

CREATE TABLE dbo.Table01 (
   Column01 NVARCHAR(100)
  ,Column02 NVARCHAR(100)
);
GO

CREATE VIEW dbo.vw_Table01_Column01_LenOver5Unique
WITH SCHEMABINDING AS
SELECT Column01, Column02
FROM dbo.Table01
WHERE LEN(Column01) >= 5;
GO

CREATE UNIQUE CLUSTERED INDEX cdx
    ON dbo.vw_Table01_Column01_LenOver5Unique(Column01, Column02)
GO

INSERT INTO dbo.Table01 VALUES('1','A'); --success
INSERT INTO dbo.Table01 VALUES('1','A'); --success
INSERT INTO dbo.Table01 VALUES('55555','A'); --success
INSERT INTO dbo.Table01 VALUES('55555','B'); --success
INSERT INTO dbo.Table01 VALUES('55555','B'); --duplicate key error
GO
Dan Guzman
fuente
Y espero que esto funcione mucho mejor que mi monstruosidad.
James Anderson
@Dan Guzman, ¿debería usar 'WITH SCHEMABINDING'?
geek
2
@Jalil Sí, SCHEMABINDINGse requiere para una vista indizada. La implicación es, por supuesto, que deberá soltar la vista antes de modificar la tabla. Herramientas como SSDT se encargarán de esa dependencia automáticamente.
Dan Guzman el
¿Cómo debo definir la vista si tengo dos columnas en el índice? CREAR ÍNDICE ÚNICO UIX_01 EN Table01 (column01, column02) DONDE LEN (column01)> = 5;
geek
@Jalil, agregué un ejemplo de clave compuesta a mi respuesta.
Dan Guzman el
5

Esta parece ser otra de las muchas limitaciones de los índices filtrados. Intentar evitarlo con el LIKEuso WHERE column01 LIKE '_____'tampoco funciona, produciendo el mismo mensaje de error ( "Cláusula WHERE incorrecta ..." ).

Además de la VIEWsolución, otra forma sería convertir la columna calculada en una columna normal y agregar una CHECKrestricción para que siempre tenga datos válidos:

CREATE TABLE Table01 (column01 nvarchar(100),
                      column01_length int,
                      CHECK ( column01_length = len(column01)
                              AND column01 IS NOT NULL 
                              AND column01_length IS NOT NULL
                           OR column01 IS NULL 
                              AND column01_length IS NULL )
                     ) ;


CREATE UNIQUE INDEX UIX_01 ON Table01 (column01) WHERE column01_length >= 5 ;

Probado en rextester.com

Naturalmente, eso significa que debe llenar explícitamente column01_lengthcon la longitud correcta cada vez que complete column01(en inserciones y actualizaciones). Eso puede ser complicado, porque debe asegurarse de que la longitud se calcule de la misma manera que lo hace la función T-SQL LEN(). En particular, los espacios finales deben ignorarse, lo cual no es necesariamente cómo se calcula la longitud de forma predeterminada en varios lenguajes de programación en los que se escriben las aplicaciones cliente. La lógica puede ser fácil de explicar en la persona que llama, pero debe ser consciente de la diferencia en primer lugar.

Una opción sería un INSERT/UPDATEdesencadenador 1 para proporcionar el valor correcto para la columna, por lo que aparece como calculado para las aplicaciones del cliente.


1 Como se explica en Disparadores en comparación con las restricciones , necesitaría usar un disparador EN LUGAR DE PARA esto. Un disparador DESPUÉS simplemente nunca se ejecutaría, porque la longitud ausente fallaría la restricción de verificación y eso, a su vez, evitaría que el disparador se ejecute. Sin embargo, los desencadenadores INSTEAD OF tienen sus propias restricciones (consulte las Pautas de planificación de activación de DML para obtener una descripción general rápida).

ypercubeᵀᴹ
fuente
1

No estoy seguro de cómo funcionará esto y puede haber una manera mucho más fácil de lograr esto que he pasado por alto, pero esto debería hacer lo que necesita si solo está interesado en hacer cumplir la unicidad.

CREATE TABLE dbo.Table01 
(
  Column01 NVARCHAR(100)
);
GO

CREATE FUNCTION dbo.ChkUniqueColumn01OverLen5()
RETURNS BIT
AS
BEGIN
DECLARE @Result BIT, @Count BIGINT, @DistinctCount BIGINT

SELECT  @Count = COUNT(Column01),
        @DistinctCount = COUNT(DISTINCT Column01)
FROM    Table01
WHERE   LEN(Column01) >= 5 

SELECT @Result = CASE WHEN @Count = @DistinctCount THEN 1 ELSE 0 END

RETURN @Result

END;
GO

ALTER TABLE dbo.Table01
ADD CONSTRAINT Chk_UniqueColumn01OverLen5
CHECK (dbo.ChkUniqueColumn01OverLen5() = 1);
GO

INSERT dbo.Table01 (Column01)
VALUES (N'123'), (N'1234');
GO

INSERT dbo.Table01 (Column01)
VALUES (N'12345');
GO

INSERT dbo.Table01 (Column01)
VALUES (N'12345'); -- Will fail
GO

INSERT dbo.Table01 (Column01)
VALUES (N'123'); -- Will pass
GO

UPDATE dbo.Table01
SET Column01 = '12345'
WHERE Column01 = '1234' -- Will fail
GO

SELECT * FROM dbo.Table01;
GO

DROP TABLE Table01;
DROP FUNCTION dbo.ChkUniqueColumn01OverLen5;
James Anderson
fuente
2
El uso de una función de valor escalar en una restricción de verificación o definición de columna calculada obligará a todas las consultas que tocan la tabla a ejecutarse en serie, incluso si no hacen referencia a la columna.
Erik Darling
2
@sp_BlitzErik Sí, y eso puede no ser lo peor de esta solución :). Solo quería ver si funcionaría, de ahí la advertencia de rendimiento.
James Anderson el