Verwenden Sie die LEN-Funktion in der WHERE-Klausel in CREATE UNIQUE INDEX.

12

Ich habe diesen Tisch:

CREATE TABLE Table01 (column01 nvarchar(100));

Und ich möchte einen eindeutigen Index für column01 mit dieser Bedingung LEN (column01)> = 5 erstellen

Ich habe es versucht:

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

Ich habe:

Falsche WHERE-Klausel für den gefilterten Index 'UIX_01' in Tabelle 'Table01'.

Und :

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

Erzeugt:

Der gefilterte Index 'UIX_01' kann für die Tabelle 'Table01' nicht erstellt werden, da die Spalte 'column01_length' im Filterausdruck eine berechnete Spalte ist. Schreiben Sie den Filterausdruck so um, dass er diese Spalte nicht enthält.

Aussenseiter
quelle

Antworten:

15

Eine Methode, um die Einschränkung des gefilterten Index zu umgehen, ist eine indizierte Sicht:

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

BEARBEITEN:

Wie definiere ich die Ansicht, wenn der Index zwei Spalten enthält? CREATE UNIQUE INDEX UIX_01 ON Table01 (Spalte01, Spalte02) WHERE LEN (Spalte01)> = 5

Der Ansatz der indizierten Ansicht kann für einen zusammengesetzten Schlüssel erweitert werden, indem der Ansichtsdefinition und dem Index weitere Schlüsselspalten hinzugefügt werden. Derselbe Filter wird in der Ansichtsdefinition angewendet, aber die Eindeutigkeit der qualifizierenden Zeilen wird durch den zusammengesetzten Schlüssel und nicht durch den Einzelspaltenwert erzwungen:

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
quelle
Und ich erwarte, dass dies viel besser funktioniert als meine Monstrosität.
James Anderson
@ Dan Guzman sollte ich 'WITH SCHEMABINDING' verwenden?
Aussenseiter
2
@Jalil Ja, SCHEMABINDINGist für eine indizierte Ansicht erforderlich. Die Implikation ist natürlich, dass Sie die Ansicht löschen müssen, bevor Sie die Tabelle ändern. Werkzeuge wie SSDT kümmern sich automatisch um diese Abhängigkeit.
Dan Guzman
Wie definiere ich die Ansicht, wenn der Index zwei Spalten enthält? CREATE UNIQUE INDEX UIX_01 ON Table01 (Spalte01, Spalte02) WHERE LEN (Spalte01)> = 5;
Aussenseiter
@ Jalil, ich habe meiner Antwort ein zusammengesetztes Schlüsselbeispiel hinzugefügt.
Dan Guzman
5

Dies scheint eine weitere der vielen Einschränkungen von gefilterten Indizes zu sein. Der Versuch, es mit LIKEusing zu umgehen , WHERE column01 LIKE '_____'funktioniert ebenfalls nicht und erzeugt dieselbe Fehlermeldung ( "Incorrect WHERE-Klausel ..." ).

Neben der VIEWLösung besteht eine andere Möglichkeit darin, die berechnete Spalte in eine reguläre Spalte umzuwandeln und eine CHECKEinschränkung hinzuzufügen , damit sie immer gültige Daten enthält:

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 ;

Getestet bei rextester.com

Das bedeutet natürlich, dass Sie column01_lengthbei jedem Auffüllen column01(bei Einfügungen und Aktualisierungen) explizit die richtige Länge angeben müssen. Dies kann schwierig sein, da Sie sicherstellen müssen, dass die Länge auf dieselbe Weise berechnet wird wie in T-SQLLEN() Funktion. Insbesondere müssen die nachgestellten Leerzeichen ignoriert werden. Dies muss nicht unbedingt der Fall sein, wenn die Länge in verschiedenen Programmiersprachen, in denen Clientanwendungen geschrieben sind, standardmäßig berechnet wird erstens bewusst den Unterschied.

Eine Option wäre ein INSERT/UPDATETrigger 1 , um den korrekten Wert für die Spalte bereitzustellen, sodass diese für Clientanwendungen als berechnet angezeigt wird.


1 Wie in Trigger im Vergleich zu Constraints erläutert , müssten Sie hierfür einen INSTEAD OF-Trigger verwenden. Ein AFTER-Trigger würde einfach niemals ausgeführt, da die fehlende Länge die Prüfbedingung nicht erfüllen würde und dies wiederum die Ausführung des Triggers verhindern würde. STATT Trigger haben jedoch ihre eigenen Einschränkungen ( eine schnelle Übersicht finden Sie in den DML-Trigger-Planungsrichtlinien ).

ypercubeᵀᴹ
quelle
1

Ich bin nicht sicher, wie sich dies auswirken wird, und es gibt möglicherweise einen viel einfacheren Weg, dies zu erreichen, den ich übersehen habe, aber dies sollte tun, was Sie brauchen, wenn Sie nur an der Durchsetzung der Eindeutigkeit interessiert sind.

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
quelle
2
Die Verwendung einer skalarwertigen Funktion in einer Prüfbedingung oder einer berechneten Spaltendefinition zwingt alle Abfragen, die die Tabelle berühren, zur seriellen Ausführung, auch wenn sie nicht auf die Spalte verweisen.
Erik Darling
2
@sp_BlitzErik Yep und das ist vielleicht nicht einmal das Schlimmste an dieser Lösung :). Ich wollte nur sehen, ob es funktionieren würde, daher die Leistungswarnung.
James Anderson