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

下面是一个示例场景。考虑这个模式:

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)
)

然后看看我想要实现的代码:

-- 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);

最后一个语句失败,并返回一条消息:

违反唯一键约束'UQ_People_LibraryCardId'。不能在对象'dbo.People'中插入重复的键。

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


当前回答

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];

其他回答

根据我的经验——如果你认为一个列需要允许null,但也需要为它们存在的值设置UNIQUE,那么你可能对数据建模不正确。这通常表明您在同一个表中创建了一个单独的子实体作为一个不同的实体。将这个实体放在第二个表中可能更有意义。

在提供的例子中,我将把LibraryCardId放在一个单独的LibraryCards表中,并在People表中添加一个唯一的非空外键:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
)
CREATE TABLE LibraryCards (    
  LibraryCardId UNIQUEIDENTIFIER CONSTRAINT PK_LibraryCards PRIMARY KEY,
  PersonId INT NOT NULL
  CONSTRAINT UQ_LibraryCardId_PersonId UNIQUE (PersonId),
  FOREIGN KEY (PersonId) REFERENCES People(id)
)

这样,您就不需要为列既唯一又可为空而烦恼了。如果一个人没有借书证,他就不会在借书证表上有记录。此外,如果有关于借书证的其他属性(可能是Expiration Date或其他属性),那么现在就有了一个放置这些字段的逻辑位置。

创建一个只选择非null列的视图,并在视图上创建UNIQUE INDEX:

CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

注意,您需要在视图而不是表上执行INSERT和UPDATE。

你可以使用INSTEAD OF触发器:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END

SQL Server 2008 And Up

只需过滤一个唯一的索引:

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

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

对于SQL Server 2005或更早的版本,您可以在没有视图的情况下执行此操作。我只是在其中一个表中添加了一个唯一的约束条件。假设我想在SamAccountName列中唯一性,但我想允许多个null,我使用了一个物化列而不是物化视图:

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

您只需在计算列中放入一些东西,当实际所需的惟一列为NULL时,这些东西将保证在整个表中是惟一的。在本例中,PartyID是一个标识列,数字永远不会匹配任何SamAccountName,所以它对我有用。您可以尝试自己的方法—确保了解数据的领域,这样就不可能与实际数据发生交集。这可以像这样简单地预先添加一个区分符字符:

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

即使有一天PartyID变成了非数字的,并且可能与SamAccountName重合,现在也不重要了。

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

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

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

争议

请注意,一些数据库专业人员会将此视为“代理null”的情况,这肯定会有问题(主要是由于试图确定什么时候是真实值或缺失数据的代理值的问题;也可能会出现非null代理值疯狂相乘的问题)。

然而,我认为这个案例是不同的。我添加的计算列将永远不会用于确定任何东西。它本身没有意义,并且不会对其他正确定义的列中没有的信息进行编码。永远不应该选择或使用它。

所以,我的故事是,这不是一个代理NULL,我坚持它!由于除了欺骗UNIQUE索引忽略NULL之外,我们实际上并不需要非NULL值用于任何目的,因此我们的用例不存在普通的代理NULL创建所产生的问题。

尽管如此,我对使用索引视图没有问题,但它带来了一些问题,例如使用SCHEMABINDING的要求。祝您在基表中添加新列愉快(您至少必须删除索引,然后删除视图或更改视图以不受模式约束)。请参阅在SQL Server(2005)(以及后续版本),(2000)中创建索引视图的完整(长)要求列表。

更新

如果您的列是数字的,则可能存在确保使用Coalesce的唯一约束不会导致冲突的挑战。在这种情况下,有一些选择。一种可能是使用负数,将“代理null”只放在负数范围内,而将“实值”只放在正范围内。或者,可以使用以下模式。在Issue表(其中IssueID是PRIMARY KEY)中,可能有也可能没有TicketID,但如果有,它必须是唯一的。

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,NULL)。如果IssueID 2没有票据,它将是on (NULL, 2)。一些想法将表明这个约束不能用于表中的任何行,并且仍然允许多个NULL。

这段代码如果你使一个注册表单与文本框和使用插入和你的文本框是空的,你点击提交按钮。

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

你要找的确实是ANSI标准SQL:92, SQL:1999和SQL:2003的一部分,即UNIQUE约束必须禁止重复的非NULL值,但接受多个NULL值。

然而,在微软的SQL Server世界中,一个NULL是允许的,但多个NULL是不允许的…

在SQL Server 2008中,您可以基于排除null的谓词定义一个唯一的过滤索引:

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

在早期版本中,您可以使用带有NOT NULL谓词的VIEWS来强制执行约束。