【问题标题】:conditional unique constraint条件唯一约束
【发布时间】:2010-10-26 08:21:59
【问题描述】:

我有一种情况,我需要对一组列实施唯一约束,但仅限于一列的一个值。

例如,我有一个像 Table(ID, Name, RecordStatus) 这样的表。

RecordStatus 只能有值 1 或 2(活动或已删除),并且我只想在 RecordStatus = 1 时对 (ID, RecordStatus) 创建唯一约束,因为我不在乎是否有多个已删除记录具有相同的 ID。

除了写触发器,我还能这样做吗?

我使用的是 SQL Server 2005。

【问题讨论】:

  • 这种设计是一种常见的痛苦。您是否考虑过更改设计,以便从表中物理删除名义上“已删除”的记录,并可能将其移至“存档”表?
  • ...因为无法编写 UNIQUE 约束来强制执行简单的键应被视为“代码气味”,IMO。如果您无法更改设计(SQL DDL),因为许多其他表都引用了该表,那么我敢打赌您的 SQL DML 也会因此受到影响,即您必须记住添加 ...AND Table.RecordStatus = 1'对于大多数涉及此表的搜索条件和连接条件,并且在有时不可避免地被省略时会遇到一些细微的错误。

标签: sql sql-server sql-server-2005


【解决方案1】:

看,the filtered index。从文档(强调我的):

过滤索引是一种优化的非聚集索引,特别适合覆盖从明确定义的数据子集中进行选择的查询。 它使用过滤谓词来索引表中的部分行。与全表索引相比,设计良好的过滤索引可以提高查询性能,降低索引维护和存储成本。 p>

下面是一个将唯一索引与过滤谓词相结合的示例:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

RecordStatus1 时,这实质上强制ID 的唯一性。

创建该索引后,违反唯一性将引发错误:

消息 2601,第 14 级,状态 1,第 13 行
无法在具有唯一索引“MyIndex”的对象“dbo.MyTable”中插入重复的键行。重复键值为 (9999)。

注意:过滤索引是在 SQL Server 2008 中引入的。对于早期版本的 SQL Server,请参阅this answer

【讨论】:

  • 请注意,SQL Server 需要ansi_padding 用于过滤索引,因此请确保在创建过滤索引之前通过执行SET ANSI_PADDING ON 启用此选项。
【解决方案2】:

像这样添加一个检查约束。不同之处在于,如果 Status = 1 且 Count > 0,您将返回 false。

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;

【讨论】:

  • 我查看了表级检查约束,但似乎没有任何方法可以将插入或更新的值传递给函数,你知道怎么做吗?
  • 好的,我发布了一个示例脚本,可以帮助您证明我在说什么。我测试了它并且它有效。如果您查看两个注释行,您会看到我收到的消息。请注意,在我的实现中,我只是确保您不能添加具有相同 ID 的第二项,如果已经有一个活动项,则该项是活动项。您可以修改逻辑,如果有一个活动的,您不能添加任何具有相同 ID 的项目。有了这种模式,可能性几乎是无穷无尽的。
  • 我更喜欢触发器中的相同逻辑。 “标量函数中的查询......如果您的 CHECK 约束依赖于查询并且如果不止一行受到任何更新的影响,则可能会产生大问题。发生的情况是在语句完成之前对每一行检查一次约束. 这意味着语句原子性被破坏,函数将以不一致的状态暴露给数据库。结果是不可预测和不准确的。见:blogs.conchango.com/davidportas/archive/2007/02/19/…
  • 那一天只是部分正确。数据库的行为一致且可预测。检查约束将在将行添加到表之后和事务由 dbms 提交之前执行,您可以依靠它。该博客讨论了一个非常独特的问题,您需要针对一组插入执行约束,而不是一次只插入一个。 ashish 要求一次对一个插入进行约束,该约束将准确、可预测且始终如一地发挥作用。如果这听起来很简洁,我很抱歉;我的字符快用完了。
  • 这适用于插入,但似乎不适用于更新。例如。当我没想到时,在其他插入之后添加它可以在这里工作。 INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2);更新 CheckConstraint 设置 Recordstatus=1 其中 name = 'No ProblemsA'
【解决方案3】:

您可以将已删除的记录移动到缺少约束的表中,或者使用两个表的 UNION 视图来保留单个表的外观。

【讨论】:

  • 这实际上是非常聪明的卡尔。这本身不是问题的答案,但它是一个很好的解决方案。如果表有很多行,那也可以加快查找活动记录的速度,因为您可以查看活动记录表。它还会加快约束,因为唯一约束使用索引,而不是我在下面写的检查约束,它必须执行计数。我喜欢它。
【解决方案4】:

你可以用一种非常老套的方式来做到这一点......

在您的表上创建一个模式绑定视图。

创建视图随便 从表中选择 * WHERE RecordStatus = 1

现在使用您想要的字段在视图上创建一个唯一约束。

关于模式绑定视图的一个注意事项是,如果您更改基础表,则必须重新创建视图。因此有很多陷阱。

【讨论】:

  • 这是一个很好的建议,而不是“hacky”。以下是有关此filtered index alternative 的更多信息。
  • 这是个坏主意。问题不是它。
  • 我使用了一次模式绑定视图,并且从未重复过这个错误。与他们一起工作可能是一种皇家痛苦。如果您更改基础表,并不是说您必须重新创建视图 - 您可能必须为所有视图执行此操作,至少在 SQL Server 中。这是你不能在不先删除视图的情况下更改表,如果不先删除对它的引用,你可能无法做到这一点。哦,加上存储可能有问题 - 要么是因为空间,要么是因为它增加了插入和更新的成本。
【解决方案5】:

因为,您将允许重复,唯一约束将不起作用。您可以为 RecordStatus 列创建一个检查约束,并为 INSERT 创建一个存储过程,在插入重复 ID 之前检查现有的活动记录。

【讨论】:

    【解决方案6】:

    如果您不能按照 Bill 的建议使用 NULL 作为 RecordStatus,您可以将他的想法与基于函数的索引结合起来。如果 RecordStatus 不是您要在约束中考虑的值之一(否则 RecordStatus ),则创建一个返回 NULL 的函数,并在其上创建一个索引。

    这样做的好处是您不必在约束中显式检查表中的其他行,这可能会导致性能问题。

    我应该说我根本不了解SQL server,但是我已经在Oracle中成功使用了这种方法。

    【讨论】:

    • 好主意,但在 sql server 中没有基于函数的索引,但感谢您的回答
    【解决方案7】:

    对于那些仍在寻找解决方案的人,我遇到了一个不错的answer,遇到了一个类似的问题,我认为这对许多人来说仍然有用。虽然将已删除的记录移动到另一个表可能是一个更好的解决方案,对于那些不想移动记录的人可以使用链接答案中的想法,如下所示。

    • 当记录可用/活动时设置 deleted=0。
    • 在标记行时设置已删除= 已删除。

    【讨论】:

      猜你喜欢
      • 2013-04-20
      • 2021-07-22
      • 2017-07-07
      • 1970-01-01
      • 2012-07-21
      • 2014-10-20
      • 1970-01-01
      • 2012-05-06
      相关资源
      最近更新 更多