Utilizzare la funzione "LEN" nella clausola "WHERE" in "CREATE UNIQUE INDEX"


12

Ho questa tabella:

CREATE TABLE Table01 (column01 nvarchar(100));

E voglio creare un indice univoco su column01 con questa condizione LEN (column01)> = 5

Provai:

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

Ho ottenuto:

Clausola WHERE errata per l'indice filtrato 'UIX_01' sulla tabella 'Tabella01'.

E :

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

produce:

L'indice filtrato 'UIX_01' non può essere creato nella tabella 'Table01' perché la colonna 'column01_length' nell'espressione di filtro è una colonna calcolata. Riscrivi l'espressione di filtro in modo che non includa questa colonna.

Risposte:


15

Un metodo per aggirare la restrizione di indice filtrata è con una vista indicizzata:

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

MODIFICARE:

Come devo definire la vista se ho due colonne nell'indice? CREA INDICE UNICO UIX_01 SU Tabella01 (colonna01, colonna02) DOVE LEN (colonna01)> = 5

L'approccio della vista indicizzata può essere esteso per una chiave composita aggiungendo altre colonne chiave alla definizione e all'indice della vista. Lo stesso filtro viene applicato nella definizione della vista ma l'univocità delle righe qualificanti applicate dalla chiave composita anziché dal valore della singola colonna:

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

E mi aspetto che funzionerà molto meglio della mia mostruosità.
James Anderson,

@Dan Guzman dovrei usare 'WITH SCHEMABINDING'?
geek

2
@Jalil Sì, SCHEMABINDINGè richiesto per una vista indicizzata. Naturalmente, è necessario eliminare la vista prima di modificare la tabella. Strumenti come SSDT si occuperanno automaticamente di tale dipendenza.
Dan Guzman,

Come devo definire la vista se ho due colonne nell'indice? CREA INDICE UNICO UIX_01 SU Tabella01 (colonna01, colonna02) DOVE LEN (colonna01)> = 5;
geek

@Jalil, ho aggiunto un esempio di chiave composita alla mia risposta.
Dan Guzman,

5

Questa sembra essere un'altra delle molte limitazioni degli indici filtrati. Cercare di bypassarlo con l' LIKEutilizzo WHERE column01 LIKE '_____'non funziona neanche, producendo lo stesso messaggio di errore ( "clausola WHERE errata ..." ).

Oltre alla VIEWsoluzione, un altro modo sarebbe convertire la colonna calcolata in una colonna normale e aggiungere un CHECKvincolo in modo che abbia sempre dati validi:

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 ;

Testato su rextester.com

Naturalmente, ciò significa che è necessario compilare in modo esplicito column01_lengthla lunghezza corretta ogni volta che si popola column01(su inserti e aggiornamenti). Ciò può essere complicato, perché è necessario assicurarsi che la lunghezza sia calcolata nello stesso modo della funzione T-SQL LEN(). In particolare, gli spazi finali devono essere ignorati, il che non è necessariamente il modo in cui la lunghezza viene calcolata di default in vari linguaggi di programmazione in cui sono scritte le applicazioni client. La logica può essere facile da spiegare nel chiamante, ma è necessario essere consapevole della differenza in primo luogo.

Un'opzione sarebbe un INSERT/UPDATEtrigger 1 per fornire il valore corretto per la colonna, quindi appare calcolato per le applicazioni client.


1 Come spiegato in Trigger rispetto ai vincoli , per questo è necessario utilizzare un trigger INSTEAD OF. Un trigger AFTER non verrebbe mai eseguito, poiché la lunghezza assente non supererebbe il vincolo di controllo e ciò, a sua volta, impedirebbe l'esecuzione del trigger. I trigger INSTEAD OF, tuttavia, hanno le proprie restrizioni (consultare le Linee guida per la pianificazione dei trigger DML per una rapida panoramica).


1

Non sono sicuro di come questo funzionerà e potrebbe esserci un modo molto più semplice per raggiungere questo obiettivo che ho trascurato, ma questo dovrebbe fare quello che ti serve se sei interessato solo a far rispettare l'unicità.

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;

2
L'uso di una funzione a valore scalare in un vincolo di controllo o una definizione di colonna calcolata forzerà l'esecuzione in serie di tutte le query che toccano la tabella, anche se non fanno riferimento alla colonna.
Erik Darling,

2
@sp_BlitzErik Sì e potrebbe non essere la cosa peggiore di questa soluzione :). Volevo solo vedere se avrebbe funzionato, quindi l'avvertimento sulle prestazioni.
James Anderson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.