如何创建一个也允许空值的唯一约束?

时间:2009-04-20 10:12:56

标签: sql-server tsql

我希望在列中使用GUID填充唯一约束。但是,我的数据包含此列的空值。如何创建允许多个空值的约束?

这是一个example scenario。考虑这个架构:

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

最终声明失败并显示一条消息:

  

违反UNIQUE KEY约束'UQ_People_LibraryCardId'。无法在对象'dbo.People'中插入重复键。

如何更改我的架构和/或唯一性约束,以便它允许多个NULL值,同时仍然检查实际数据的唯一性?

15 个答案:

答案 0 :(得分:1204)

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

然而,在SQL Server的Microsoft世界中,允许单个NULL,但多个NULL不是......

SQL Server 2008 中,您可以根据排除NULL的谓词定义唯一的筛选索引:

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

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

答案 1 :(得分:129)

SQL Server 2008 +

您可以使用WHERE子句创建一个接受多个NULL的唯一索引。请参阅answer below

在SQL Server 2008之前

您无法创建UNIQUE约束并允许NULL。您需要设置NEWID()的默认值。

在创建UNIQUE约束之前,将现有值更新为NEWID(),其中为NULL。

答案 2 :(得分:31)

SQL Server 2008及以上

只需过滤唯一索引:

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重合,现在也无关紧要。

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

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

在SQL Server 2008及更高版本中,如果可能,请务必使用已过滤的解决方案!

<强>争

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

但是,我认为这种情况有所不同。我正在添加的计算列永远不会用于确定任何内容。它没有任何意义,并且没有编码在其他正确定义的列中未单独找到的信息。永远不应该选择或使用它。

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

所有这一切,我对使用索引视图没有任何问题 - 但它带来了一些问题,例如使用SCHEMABINDING的要求。在基表中添加新列很有趣(您至少必须删除索引,然后删除视图或将视图更改为不受模式限制)。查看完整(长)list of requirements for creating an indexed view in SQL Server (2005)(以及更高版本),(2000)

<强>更新

如果您的列是数字,则可能存在确保使用Coalesce的唯一约束不会导致冲突的挑战。在这种情况下,有一些选择。一种可能是使用负数,将“代理空值”仅设置在负范围内,将“实际值”仅设置在正范围内。或者,可以使用以下模式。在表Issue(其中IssueIDPRIMARY 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); ,但如果有,则必须是唯一的。

UNIQUE

如果IssueID 1具有票证123,则{{1}}约束将在值(123,NULL)上。如果IssueID 2没有票证,则它将打开(NULL,2)。一些想法会表明这个约束不能复制到表中的任何行,并且仍然允许多个NULL。

答案 3 :(得分:16)

对于使用 Microsoft SQL Server管理器且想要创建唯一但可以为空的索引的用户,您可以按照通常在新索引的索引属性中创建唯一索引,选择“从左侧面板过滤,然后输入您的过滤器(这是您的where子句)。它应该是这样的:

([YourColumnName] IS NOT NULL)

这适用于MSSQL 2012

答案 4 :(得分:9)

当我应用下面的唯一索引时:

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

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

  

UPDATE失败,因为以下SET选项的设置不正确:'ARITHABORT'。

我在MSDN

上找到了这个
  在计算列或索引视图上创建或更改索引时,

SET ARITHABORT必须为ON。如果SET ARITHABORT为OFF,则具有计算列或索引视图索引的表上的CREATE,UPDATE,INSERT和DELETE语句将失败。

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

  

右键单击[数据库] - &gt;属性 - &gt;选项 - &gt;其他   选项 - &gt;杂项 - &gt;启用算术中止 - &gt; true

我相信可以使用

在代码中设置此选项
ALTER DATABASE "DBNAME" SET ARITHABORT ON

但我没有测试过这个

答案 5 :(得分:6)

创建一个仅选择非NULL列并在视图上创建UNIQUE INDEX的视图:

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

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

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

您可以使用INSTEAD OF触发器执行此操作:

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

答案 6 :(得分:4)

可以在聚集的索引视图

上创建唯一约束

您可以像这样创建视图:

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

和这样的唯一约束:

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

答案 7 :(得分:4)

也可以在设计师中完成

右键点击索引&gt;获取此窗口的属性

capture

答案 8 :(得分:2)

也许会考虑“INSTEAD OF”触发器并自行检查?使用列上的非聚集(非唯一)索引来启用查找。

答案 9 :(得分:1)

如前所述,SQL Server在UNIQUE CONSTRAINT方面没有实现ANSI标准。自2007年以来,有一个ticket on Microsoft Connect。正如那里和here所建议的那样,今天最好的选择是使用another answer中所述的过滤索引或计算列,例如:< / p>

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

答案 10 :(得分:1)

您可以创建一个INSTEAD OF触发器来检查特定条件和错误(如果满足)。在较大的表格上创建索引可能代价高昂。

以下是一个例子:

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

答案 11 :(得分:1)

根据我的经验 - 如果您认为一列需要允许 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)
)

这样您就无需担心列是唯一的且可为空的。如果一个人没有借书证,他们就不会在借书证表中有记录。此外,如果有关于借书证的其他属性(可能是到期日期或其他内容),您现在就有了放置这些字段的合乎逻辑的位置。

答案 12 :(得分:-1)

您不能使用UNIQUE约束来执行此操作,但可以在触发器中执行此操作。

    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

答案 13 :(得分:-1)

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

答案 14 :(得分:-1)

这段代码如果你用textBox创建一个注册表单并使用insert和你的textBox为空,你点击提交按钮。

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