Utilisez la fonction «LEN» dans la clause «WHERE» dans «CREATE UNIQUE INDEX»

12

J'ai ce tableau:

CREATE TABLE Table01 (column01 nvarchar(100));

Et je veux créer un index unique sur column01 avec cette condition LEN (column01)> = 5

J'ai essayé:

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

J'ai eu:

Clause WHERE incorrecte pour l'index filtré 'UIX_01' sur la table 'Table01'.

Et :

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

Produit:

L'index filtré 'UIX_01' ne peut pas être créé sur la table 'Table01' car la colonne 'column01_length' dans l'expression de filtre est une colonne calculée. Réécrivez l'expression de filtre afin qu'elle n'inclue pas cette colonne.

geek
la source

Réponses:

15

Une méthode pour contourner la restriction d'index filtré consiste à utiliser une vue indexée:

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

ÉDITER:

Comment définir la vue si j'ai deux colonnes dans l'index? CRÉER UN INDEX UNIQUE UIX_01 ON Table01 (colonne01, colonne02) OERE LEN (colonne01)> = 5

L'approche de vue indexée peut être étendue pour une clé composite en ajoutant d'autres colonnes de clé à la définition et à l'index de la vue. Le même filtre est appliqué dans la définition de la vue mais l'unicité des lignes qualifiantes est appliquée par la clé composite plutôt que par la valeur de la colonne unique:

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
la source
Et je m'attends à ce que cela fonctionne bien mieux que ma monstruosité.
James Anderson
@Dan Guzman dois-je utiliser 'AVEC SCHEMABINDING'?
geek
2
@Jalil Oui, SCHEMABINDINGest requis pour une vue indexée. L'implication est bien sûr que vous devrez supprimer la vue avant de modifier la table. Des outils comme SSDT prendront automatiquement en charge cette dépendance.
Dan Guzman
Comment définir la vue si j'ai deux colonnes dans l'index? CRÉER UN INDEX UNIQUE UIX_01 ON Table01 (colonne01, colonne02) OERE LEN (colonne01)> = 5;
geek
@Jalil, j'ai ajouté un exemple de clé composite à ma réponse.
Dan Guzman
5

Cela semble être une autre des nombreuses limitations des index filtrés. Essayer de le contourner avec LIKEusing WHERE column01 LIKE '_____'ne fonctionne pas non plus, produisant le même message d'erreur ( "clause WHERE incorrecte ..." ).

Outre la VIEWsolution, une autre façon serait de convertir la colonne calculée en une colonne régulière et d'ajouter une CHECKcontrainte pour qu'elle ait toujours des données valides:

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 ;

Testé sur rextester.com

Naturellement, cela signifie que vous devez remplir explicitement column01_lengthavec la bonne longueur à chaque fois que vous remplissez column01(sur les insertions et les mises à jour). Cela peut être délicat, car vous devez vous assurer que la longueur est calculée de la même manière que la fonction T-SQL LEN(). En particulier, les espaces de fin doivent être ignorés, ce qui n'est pas nécessairement la façon dont la longueur est calculée par défaut dans divers langages de programmation dans lesquels les applications clientes sont écrites. La logique peut être facile à prendre en compte dans l'appelant, mais vous devez être conscient de la différence en premier lieu.

Une option serait un INSERT/UPDATEdéclencheur 1 pour fournir la valeur correcte pour la colonne, elle apparaît donc comme calculée pour les applications clientes.


1 Comme expliqué dans Déclencheurs par rapport aux contraintes , vous devez utiliser un déclencheur INSTEAD OF pour cela. Un déclencheur AFTER ne s'exécuterait tout simplement jamais, car la longueur absente ferait échouer la contrainte de vérification et, à son tour, empêcherait le déclencheur de s'exécuter. Cependant, les déclencheurs INSTEAD OF ont leurs propres restrictions (voir les directives de planification des déclencheurs DML pour un aperçu rapide).

ypercubeᵀᴹ
la source
1

Je ne sais pas comment cela fonctionnera et il y a peut-être un moyen beaucoup plus facile d'y parvenir que j'ai négligé, mais cela devrait faire ce dont vous avez besoin si vous souhaitez uniquement appliquer 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;
James Anderson
la source
2
L'utilisation d'une fonction à valeur scalaire dans une contrainte de vérification ou une définition de colonne calculée forcera toutes les requêtes qui touchent la table à s'exécuter en série, même si elles ne font pas référence à la colonne.
Erik Darling
2
@sp_BlitzErik Yep et ce n'est peut-être même pas la pire chose avec cette solution :). Je voulais juste voir si cela fonctionnerait, d'où l'avertissement de performance.
James Anderson