关于sql server:如何创建一个也允许空值的唯一约束?

How do I create a unique constraint that also allows nulls?

我想在一列上有一个唯一的约束,我将用guid填充该列。但是,我的数据包含此列的空值。如何创建允许多个空值的约束?

下面是一个示例场景。考虑此架构:

1
2
3
4
5
6
CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
  LibraryCardId UNIQUEIDENTIFIER NULL,
  CONSTRAINT UQ_People_LibraryCardId UNIQUE (LibraryCardId)
)

然后,请参阅此代码,了解我要实现的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- This works fine:
INSERT INTO People (Name, LibraryCardId)
 VALUES ('John Doe', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This also works fine, obviously:
INSERT INTO People (Name, LibraryCardId)
VALUES ('Marie Doe', 'BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB');

-- This would *correctly* fail:
--INSERT INTO People (Name, LibraryCardId)
--VALUES ('John Doe the Second', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This works fine this one first time:
INSERT INTO People (Name, LibraryCardId)
VALUES ('Richard Roe', NULL);

-- THE PROBLEM: This fails even though I'd like to be able to do this:
INSERT INTO People (Name, LibraryCardId)
VALUES ('Marcus Roe', NULL);

最终语句失败,并显示一条消息:

Violation of UNIQUE KEY constraint 'UQ_People_LibraryCardId'. Cannot insert duplicate key in object 'dbo.People'.

如何更改模式和/或唯一性约束,使其允许多个NULL值,同时仍检查实际数据的唯一性?


您要查找的实际上是ANSI标准SQL:92、SQL:1999和SQL:2003的一部分,即唯一约束必须不允许重复的非空值,但接受多个空值。

然而,在SQL Server的Microsoft世界中,只允许一个空值,但不允许多个空值…

在SQL Server 2008中,可以基于不包含空值的谓词定义唯一的筛选索引:

1
2
3
CREATE UNIQUE NONCLUSTERED INDEX idx_yourcolumn_notnull
ON YourTable(yourcolumn)
WHERE yourcolumn IS NOT NULL;

在早期版本中,可以使用带有非空谓词的视图来强制约束。


SQL Server 2008+

您可以使用WHERE子句创建一个接受多个空值的唯一索引。请参阅下面的答案。

在SQL Server 2008之前

不能创建唯一约束并允许空值。您需要设置newID()的默认值。

在创建唯一约束之前,将现有值更新为newid(),其中为空。


SQL Server 2008及更高版本

只需筛选唯一索引:

1
2
3
CREATE UNIQUE NONCLUSTERED INDEX UQ_Party_SamAccountName
ON dbo.Party(SamAccountName)
WHERE SamAccountName IS NOT NULL;

在较低版本中,仍然不需要物化视图

对于SQL Server 2005及更早版本,您可以在没有视图的情况下执行此操作。我刚在我的一个表中添加了一个独特的约束,就像你要求的那样。鉴于我希望在列SamAccountName中具有唯一性,但我希望允许多个空值,因此我使用了物化列而不是物化视图:

1
2
3
4
ALTER TABLE dbo.Party ADD SamAccountNameUnique
   AS (Coalesce(SamAccountName, Convert(varchar(11), PartyID)))
ALTER TABLE dbo.Party ADD CONSTRAINT UQ_Party_SamAccountName
   UNIQUE (SamAccountNameUnique)

您只需在计算列中放入一些内容,当实际需要的唯一列为空时,这些内容将保证整个表的唯一性。在这种情况下,PartyID是一个标识列,数字永远不会与任何SamAccountName匹配,因此它对我很有用。您可以尝试自己的方法,确保您了解数据的域,这样就不可能与真实数据交叉。这很简单,只需预先准备一个这样的区分符:

1
Coalesce('n' + SamAccountName, 'p' + Convert(varchar(11), PartyID))

即使有一天PartyID变为非数字,并且可能与SamAccountName重合,现在也无关紧要了。

请注意,包含计算列的索引的存在隐式地导致每个表达式结果与表中的其他数据一起保存到磁盘,这确实会占用额外的磁盘空间。

请注意,如果不需要索引,您仍然可以通过将关键字PERSISTED添加到列表达式定义的末尾,使表达式预先计算到磁盘,从而节省CPU。

在SQL Server 2008及更高版本中,如果可能的话,一定要使用筛选后的解决方案!

争议

请注意,一些数据库专业人员会将此视为"代理空值"的情况,这肯定有问题(主要是由于在试图确定某个值是真实值还是丢失数据的代理值时出现的问题;也可能是非空代理值的数量与疯狂值相乘的问题)。

不过,我相信这个案子是不同的。我要添加的计算列永远不会用于确定任何内容。它本身没有意义,也不编码在其他正确定义的列中未单独找到的信息。不得选择或使用。

所以,我的故事是,这不是一个代理无效,我坚持它!由于我们实际上不希望非空值用于除欺骗UNIQUE索引忽略空值以外的任何目的,因此我们的用例没有出现正常代理空创建时出现的任何问题。

尽管如此,我并不反对使用索引视图,但是它带来了一些问题,比如使用SCHEMABINDING的需求。在基表中添加一个新列是很有趣的(您至少需要删除索引,然后删除视图或将视图更改为不受架构约束)。请参阅在SQL Server(2005)(以及更高版本)(2000)中创建索引视图所需的完整(长)列表。

更新

如果您的列是数字列,则可能存在确保使用Coalesce的唯一约束不会导致冲突的挑战。在这种情况下,有一些选择。一种可能是使用一个负数,将"surrogate nulls"只放在负数范围内,而"real values"只放在正数范围内。或者,可以使用以下模式。在表Issue中(其中IssueIDPRIMARY KEY,可能有也可能没有TicketID,但如果有,则必须是唯一的。

1
2
3
4
ALTER TABLE dbo.Issue ADD TicketUnique
   AS (CASE WHEN TicketID IS NULL THEN IssueID END);
ALTER TABLE dbo.Issue ADD CONSTRAINT UQ_Issue_Ticket_AllowNull
   UNIQUE (TicketID, TicketUnique);

如果issueid 1有票据123,那么UNIQUE约束将在值(123,空)上。如果issueid 2没有票据,它将打开(空,2)。有人认为这个约束不能对表中的任何行进行复制,并且仍然允许多个空值。


对于使用Microsoft SQL Server Manager并希望创建唯一但可以为空的索引的用户,可以像通常那样创建唯一索引,然后在新索引的索引属性中,从左侧面板中选择"筛选器",然后输入筛选器(这是WHERE子句)。它应该是这样的:

1
([YourColumnName] IS NOT NULL)

这适用于MSSQL 2012


当我应用以下唯一索引时:

1
2
3
CREATE UNIQUE NONCLUSTERED INDEX idx_badgeid_notnull
ON employee(badgeid)
WHERE badgeid IS NOT NULL;

每个非空更新和插入都失败,错误如下:

UPDATE failed because the following SET options have incorrect settings: 'ARITHABORT'.

我在msdn上找到这个

SET ARITHABORT must be ON when you are creating or changing indexes on computed columns or indexed views. If SET ARITHABORT is OFF, CREATE, UPDATE, INSERT, and DELETE statements on tables with indexes on computed columns or indexed views will fail.

为了让这个正常工作,我做了这个

Right click [Database]-->Properties-->Options-->Other
Options-->Misscellaneous-->Arithmetic Abort Enabled -->true

我相信可以在代码中使用

1
ALTER DATABASE"DBNAME" SET ARITHABORT ON

但我没有测试过这个


创建只选择非NULL列的视图,并在该视图上创建UNIQUE INDEX

1
2
3
4
5
6
7
CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

请注意,您需要在视图上执行INSERTUPDATE,而不是在表上执行。

您可以使用INSTEAD OF触发器:

1
2
3
4
5
6
7
8
9
CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END


也可以在设计师那里完成

右键单击索引>属性以获取此窗口

capture


可以在聚集索引视图上创建唯一约束

您可以这样创建视图:

1
2
3
CREATE VIEW dbo.VIEW_OfYourTable WITH SCHEMABINDING AS
SELECT YourUniqueColumnWithNullValues FROM dbo.YourTable
WHERE YourUniqueColumnWithNullValues IS NOT NULL;

唯一的约束如下:

1
2
CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)

也许考虑一个"INSTEAD OF"触发器,自己检查一下?在列上使用非聚集(非唯一)索引来启用查找。


您可以创建一个instead-of-trigger来检查是否满足特定条件和错误。在较大的表上创建索引的成本可能很高。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TRIGGER PONY.trg_pony_unique_name ON PONY.tbl_pony
 INSTEAD OF INSERT, UPDATE
 AS
BEGIN
 IF EXISTS(
    SELECT TOP (1) 1
    FROM inserted i
    GROUP BY i.pony_name
    HAVING COUNT(1) > 1    
    )
     OR EXISTS(
    SELECT TOP (1) 1
    FROM PONY.tbl_pony t
    INNER JOIN inserted i
    ON i.pony_name = t.pony_name
    )
    THROW 911911, 'A pony must have a name as unique as s/he is. --PAS', 16;
 ELSE
    INSERT INTO PONY.tbl_pony (pony_name, stable_id, pet_human_id)
    SELECT pony_name, stable_id, pet_human_id
    FROM inserted
 END

如前所述,当涉及到UNIQUE CONSTRAINT时,SQL Server不实现ANSI标准。自2007年起,Microsoft Connect上就有此问题的通知单。如前所述,目前最好的选择是使用另一个答案或计算列中所述的过滤索引,例如:

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE [Orders] (
  [OrderId] INT IDENTITY(1,1) NOT NULL,
  [TrackingId] varchar(11) NULL,
  ...
  [ComputedUniqueTrackingId] AS (
      CASE WHEN [TrackingId] IS NULL
      THEN '#' + cast([OrderId] as varchar(12))
      ELSE [TrackingId_Unique] END
  ),
  CONSTRAINT [UQ_TrackingId] UNIQUE ([ComputedUniqueTrackingId])
)


如果您使用文本框创建一个注册表,并使用insert和ur文本框,则此代码为空,然后单击Submit按钮。

1
2
CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;

1
2
3
4
5
CREATE UNIQUE NONCLUSTERED INDEX [UIX_COLUMN_NAME]
ON [dbo].[Employee]([Username] ASC) WHERE ([Username] IS NOT NULL)
WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF,
DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF, ONLINE = OFF,
MAXDOP = 0) ON [PRIMARY];

使用UNIQUE约束不能这样做,但可以在触发器中这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    CREATE TRIGGER [dbo].[OnInsertMyTableTrigger]
   ON  [dbo].[MyTable]
   INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Column1 INT;
    DECLARE @Column2 INT; -- allow nulls on this column

    SELECT @Column1=Column1, @Column2=Column2 FROM inserted;

    -- Check if an existing record already exists, if not allow the insert.
    IF NOT EXISTS(SELECT * FROM dbo.MyTable WHERE Column1=@Column1 AND Column2=@Column2 @Column2 IS NOT NULL)
    BEGIN
        INSERT INTO dbo.MyTable (Column1, Column2)
            SELECT @Column2, @Column2;
    END
    ELSE
    BEGIN
        RAISERROR('The unique constraint applies on Column1 %d, AND Column2 %d, unless Column2 is NULL.', 16, 1, @Column1, @Column2);
        ROLLBACK TRANSACTION;  
    END

END