Use "LEN" function in "WHERE" clause in "CREATE UNIQUE INDEX"
One method to workaround the filtered index restriction is with an indexed view:
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
EDIT:
How should I define the view if I have two columns in the index? CREATE UNIQUE INDEX UIX_01 ON Table01(column01, column02) WHERE LEN(column01)>=5
The indexed view approach can be extended for a composite key by adding other key columns to the view definition and index. The same filter is applied in the view definition but uniqueness of qualifying rows enforced by the composite key rather than the single column value:
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
This seems to be another of the many limitations of filtered indexes. Trying to bypass it with LIKE
using WHERE column01 LIKE '_____'
does not work either, producing the same error message ("Incorrect WHERE clause ...").
Besides the VIEW
solution, another way would be to convert the computed column to a regular column and add a CHECK
constraint so it has always valid data:
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 ;
Tested at rextester.com
Naturally, that means you need to explicitly populate column01_length
with the correct length every time you populate column01
(on inserts and updates). That may be tricky, because you need to make sure that the length is calculated the same way as the T-SQL LEN()
function does it. In particular, the trailing spaces need to be ignored, which is not necessarily how the length is calculated by default in various programming languages that client applications are written in. The logic may be easy to account for in the caller, but you need to be aware of the difference in the first place.
An option would be an INSERT/UPDATE
trigger1 to supply the correct value for the column, so it appears as computed to client applications.
1As explained in Triggers Compared to Constraints, you would need to use an INSTEAD OF trigger for this. An AFTER trigger would simply never execute, because the absent length would fail the check constraint and that, in turn, would prevent the trigger from running. INSTEAD OF triggers, however, have their own restrictions (see DML Trigger Planning Guidelines for a quick overview).