Use a função "LEN" na cláusula "WHERE" em "CREATE UNIQUE INDEX"

12

Eu tenho esta tabela:

CREATE TABLE Table01 (column01 nvarchar(100));

E eu quero criar um índice exclusivo na coluna01 com esta condição LEN (coluna01)> = 5

Eu tentei:

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

Eu tenho:

Cláusula WHERE incorreta para o índice filtrado 'UIX_01' na tabela 'Tabela01'.

E:

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

Produz:

O índice filtrado 'UIX_01' não pode ser criado na tabela 'Table01' porque a coluna 'column01_length' na expressão de filtro é uma coluna calculada. Reescreva a expressão de filtro para que ela não inclua esta coluna.

nerd
fonte

Respostas:

15

Um método para solucionar a restrição do índice filtrado é com uma exibição indexada:

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:

Como devo definir a exibição se tenho duas colunas no índice? CRIAR ÍNDICE ÚNICO UIX_01 NA Tabela01 (coluna01, coluna02) ONDE LEN (coluna01)> = 5

A abordagem de exibição indexada pode ser estendida para uma chave composta adicionando outras colunas de chave à definição e ao índice da exibição. O mesmo filtro é aplicado na definição de exibição, mas a exclusividade das linhas qualificadas aplicadas pela chave composta em vez do valor da coluna ú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
fonte
E espero que isso tenha um desempenho muito melhor do que minha monstruosidade.
James Anderson
@Dan Guzman devo usar 'WITH SCHEMABINDING'?
nerd
2
@Jalil Sim, SCHEMABINDINGé necessário para uma exibição indexada. É claro que a implicação é que você precisará interromper a exibição antes de alterar a tabela. Ferramentas como SSDT cuidam dessa dependência automaticamente.
Dan Guzman
Como devo definir a exibição se tenho duas colunas no índice? CRIAR ÍNDICE ÚNICO UIX_01 NA Tabela01 (coluna01, coluna02) WHERE LEN (coluna01)> = 5;
Geek
@ Dalil, adicionei um exemplo de chave composta à minha resposta.
Dan Guzman
5

Essa parece ser outra das muitas limitações dos índices filtrados. Tentar contorná-lo com o LIKEuso WHERE column01 LIKE '_____'também não funciona, produzindo a mesma mensagem de erro ( "Cláusula WHERE incorreta ..." ).

Além da VIEWsolução, outra maneira seria converter a coluna computada em uma coluna regular e adicionar uma CHECKrestrição para que ele sempre tenha dados 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 ;

Testado em rextester.com

Naturalmente, isso significa que você precisa preencher explicitamente column01_lengthcom o tamanho correto sempre que preencher column01(em inserções e atualizações). Isso pode ser complicado, porque você precisa garantir que o comprimento seja calculado da mesma maneira que o T-SQLLEN() função . Em particular, os espaços à direita precisam ser ignorados, o que não é necessariamente o modo como o comprimento é calculado por padrão em várias linguagens de programação nas quais os aplicativos clientes são gravados. A lógica pode ser fácil de explicar no chamador, mas é necessário ciente da diferença em primeiro lugar.

Uma opção seria um INSERT/UPDATEgatilho 1 para fornecer o valor correto para a coluna, para que apareça como calculado para aplicativos clientes.


1 Conforme explicado em Triggers Comparated to Constraints , você precisaria usar um gatilho INSTEAD OF para isso. Um gatilho AFTER nunca seria executado, porque o comprimento ausente falharia na restrição de verificação e isso, por sua vez, impediria a execução do gatilho. No entanto, gatilhos em vez de gatilhos têm suas próprias restrições (consulte Diretrizes de planejamento do gatilho DML para uma visão geral rápida).

ypercubeᵀᴹ
fonte
1

Não tenho certeza de como isso vai funcionar e pode haver uma maneira muito mais fácil de conseguir isso que eu negligenciei, mas isso deve fazer o que você precisa se estiver interessado apenas em reforçar a exclusividade.

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
fonte
2
O uso de uma função escalar com valor em uma restrição de verificação ou em uma definição de coluna calculada forçará todas as consultas que tocam na tabela a serem executadas em série, mesmo que não façam referência à coluna.
Erik Darling
2
@sp_BlitzErik Sim, e isso pode até não ser a pior coisa dessa solução :). Eu só queria ver se funcionaria, daí o aviso de desempenho.
James Anderson