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

Posted

技术标签:

【中文标题】如何创建一个也允许空值的唯一约束?【英文标题】:How do I create a unique constraint that also allows nulls? 【发布时间】:2010-10-20 12:57:39 【问题描述】:

我想对要使用 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 值,同时仍检查实际数据的唯一性?

【问题讨论】:

为标准兼容性投票的连接问题:connect.microsoft.com/SQLServer/Feedback/Details/299229 How to create a unique index on a NULL column?的可能重复 UNIQUE 约束并允许 NULL。 ?这是常识。这是不可能的 @flik,最好不要参考“常识”。那不是有效的论点。特别是考虑到null 不是价值而是价值的缺失。根据 SQL 标准,null 不被视为等于 null。那么为什么多个null 应该是违反唯一性的呢? 【参考方案1】:

您正在寻找的确实是 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 来强制执行约束。

【讨论】:

这可能是最好的方法。不确定是否有任何性能影响?有人吗? 我正在尝试在 SQL Server 2008 Express 版本中执行此操作,但我收到如下错误: CREATE UNIQUE NONCLUSTERED INDEX UC_MailingId ON [SLS-CP].dbo.MasterFileEntry(MailingId) WHERE MailingId IS NOT NULL 结果:Msg 156、Level 15、State 1、Line 3 关键字“WHERE”附近的语法不正确。如果我删除 where 子句,DDL 运行良好,但当然不会做我需要它做的事情。有什么想法吗? 除非我弄错了,否则您不能像关闭唯一约束一样从唯一索引创建外键。 (至少 SSMS 在我尝试时向我抱怨过。)如果能够将一个始终唯一(非空时)的可为空列作为外键关系的来源,那就太好了。 确实是一个很好的答案。太糟糕了,它被接受为答案的人隐藏了。这个解决方案几乎没有引起我的注意,但它现在在我的实施中就像奇迹一样。 SQL 2005 及以下版本的另一种替代方法是计算列,即“Nullbuster”技巧。 ***.com/a/191729/132461 它使您免于将数据库与另一个视图混为一谈,您只需使用另一列 - 如果 ColumnA 是您希望成为 ANSI 可为空的 UNIQUE 的唯一列,则通常命名为 ColumnA-Nullbuster。在 ColumnA-Nullbuster 上放置一个 UNIQUE 索引(或表达业务意图的约束),它将在 ColumnA 上强制执行唯一性【参考方案2】:

SQL Server 2008 +

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

SQL Server 2008 之前的版本

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

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

【讨论】:

这将追溯地向现有行添加值,如果是这样,这是我需要做的,谢谢? 您需要运行 UPDATE 语句将现有值设置为 NEWID(),其中现有字段为空 如果您使用的是 SQL Server 2008 或更高版本,请参阅下面的答案,获得超过 100 个赞。您可以将 WHERE 子句添加到您的唯一约束。 ADO.NET DataTables 也遇到了这个问题。因此,即使我可以使用这种方法在支持字段中允许空值,DataTable 也不会让我首先将空值存储在唯一列中。如果有人知道解决方案,请发布here 请确保您向下滚动并阅读获得 600 个赞的答案。它不再只是超过 100 个。【参考方案3】:

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”的情况,这肯定存在问题(主要是由于试图确定某事物何时是 真正的值 或 缺失数据的代理值;也可能存在非 NULL 代理值的数量像疯了似地相乘的问题。

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

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

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

更新

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

如果 IssueID 1 有票证 123,UNIQUE 约束将在值(123,NULL)上。如果 IssueID 2 没有票证,它将为 (NULL, 2)。有些想法会表明这个约束不能对表中的任何行重复,并且仍然允许多个 NULL。

【讨论】:

Server 2005 的良好解决方法。但我想指出,ANSI 唯一索引的可能好处之一已丢失:跳过列值为空的记录的能力。例如,如果您的表有数百万条记录,但只有一小部分具有非空值,则真正的 ANSI 唯一索引将非常小,而采用这种解决方法的索引将占用大量空间。 @GuillermoPrandi 这些都是有效的想法。在我看来,如果一个表有数百万行并且其中大多数在特定列中有一个 NULL,那么该表本身可能没有尽可能地标准化。也许在该列中确实具有值的行应该移动到另一个表(与原始表具有一对零或一的关系)。然后,删除原始表中的列。现在可以将有效的唯一索引放在第二个表上。将两个表与LEFT JOIN 组合在一起的物化视图可以重新构造原始表。【参考方案4】:

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

([YourColumnName] IS NOT NULL)

这适用于 MSSQL 2012

【讨论】:

这里描述了如何在Microsoft SQL Server Management Studio下制作过滤索引并且可以完美运行:msdn.microsoft.com/en-us/library/cc280372.aspx【参考方案5】:

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

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

每个非空更新和插入都失败并出现以下错误:

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

我在MSDN找到了这个

当您在计算列或索引视图上创建或更改索引时,SET ARITHABORT 必须为 ON。如果 SET ARITHABORT 为 OFF,则在计算列或索引视图上具有索引的表上的 CREATE、UPDATE、INSERT 和 DELETE 语句将失败。

所以为了让它正常工作,我这样做了

右键【数据库】-->属性-->选项-->其他 Options-->Misscellaneous-->Arithmetic Abort Enabled-->true

我相信可以使用

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

但我没有测试过这个

【讨论】:

【参考方案6】:

也可以在设计器中完成

右击索引>属性得到这个窗口

【讨论】:

如果你能接触到设计师,这是一个很好的选择 虽然,正如我刚刚发现的,一旦你的表中有数据,你就不能再使用设计器了。它似乎忽略了过滤器,并且任何尝试的表更新都会遇到消息“不允许重复键”【参考方案7】:

创建一个只选择非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

【讨论】:

所以我需要更改我的 dal 以插入视图吗? 您可以创建一个触发器 INSTEAD OF INSERT。【参考方案8】:

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

您可以像这样创建视图:

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)

【讨论】:

【参考方案9】:

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

【讨论】:

【参考方案10】:

根据我的经验 - 如果您认为列需要允许 NULL,但对于它们存在的值也需要是唯一的,那么您可能对数据进行了错误的建模。这通常表明您在同一个表中创建一个单独的子实体作为不同的实体。将此实体放在第二个表中可能更有意义。

在提供的示例中,我会将 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)
)

这样您就不必担心列既是唯一的又可以为空。如果一个人没有借书证,他们就不会在借书证表中记录。此外,如果有关于图书证的其他属性(可能是到期日期等),您现在可以合理地放置这些字段。

【讨论】:

严重不同意你的第一句话。在澳大利亚,每个员工都有一个叫做税号的东西,这当然是独一无二的。根据法律,您无需将其提供给您的员工。这意味着该列可能为空,但应该是唯一的。在这种情况下,额外的表格可能会被视为过度设计。【参考方案11】:

如前所述,对于UNIQUE CONSTRAINT,SQL Server 没有实现 ANSI 标准。自 2007 年以来就有一个 ticket on Microsoft Connect。正如那里和 here 所建议的那样,截至目前,最好的选择是使用 another answer 中所述的过滤索引或计算列,例如:

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

【讨论】:

【参考方案12】:

您可以创建 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

【讨论】:

【参考方案13】:

您不能使用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

【讨论】:

【参考方案14】:
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];

【讨论】:

【参考方案15】:

如果您使用 textBox 制作注册表单并使用 insert 并且您的 textBox 为空并且您单击提交按钮,则此代码。

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

【讨论】:

以上是关于如何创建一个也允许空值的唯一约束?的主要内容,如果未能解决你的问题,请参考以下文章

SQL中Unique约束有啥用啊?

如何使用 Hibernate 注释创建允许 NULL 值的唯一约束?

mysql约束

5-05约束的类型

在其中一列中具有多个空值的复合唯一键约束

MySQL 是不是忽略唯一约束的空值?