对未提交的数据实施检查约束

时间:2014-08-29 20:44:54

标签: sql sql-server transactions sql-server-2014

注意:我知道这个例子的一个更好的做法是使用唯一索引,但这只是一个更复杂情况的简化示例。

我有一个包含两个值的表。我有一个检查约束,以防止重复这两个值:

Value1    Value2
------    ------
1         1
1         2
1         3
2         1
2         2
2         3

鉴于上述数据,这应该(通常确实)失败:

INSERT INTO Table1 VALUES(1,1)
  

Msg 547,Level 16,State 0,Line 15   INSERT语句与CHECK约束" chkTable1_DuplicateValues"冲突。冲突发生在数据库" MyDB",table" dbo.Table1"中。   声明已经终止。

但是,在竞争条件下 - 两个事务试图同时插入相同数据的情况 - 检查约束失败,并允许数据。

以下是如何重现它:

CREATE TABLE Table1 (Value1 INT, Value2 INT); 
GO

CREATE FUNCTION CheckDuplicateValues(@value1 INT, @value2 INT) RETURNS INT AS 
BEGIN 
    RETURN (
        SELECT COUNT(*) FROM Table1 
        WHERE Value1 = @value1 
          AND Value2 = @value2
    );
END; 
GO

ALTER TABLE Table1 
    ADD CONSTRAINT chkTable1_DuplicateValues 
    CHECK (dbo.CheckDuplicateValues(Value1, Value2) = 1);
GO

然后,在两个单独的窗口(连接)中,在两个窗口中运行以下命令:

BEGIN TRANSACTION;
INSERT INTO Table1 VALUES(1,1);

在两个窗口中,您都会看到:

  

(1行受影响)

我理解为什么会这样 - 基本上,CheckDuplicateValues函数中的查询只读取已提交的数据,加上当前连接上新插入的数据,所以在这两种情况下,它只是从当前连接计算新插入的行。

我不确定解决问题的最佳方法。我应该在我的UDF查询上抛出WITH (NOLOCK)提示:

SELECT COUNT(*) FROM Table1 WITH (NOLOCK)

我也不确定为什么在第二次查询COMMIT TRANSACTION发生后,检查约束不会被强制执行。

有没有更好的方法来解决这个问题?同样,我知道对于这个特定情况,唯一索引是最好的方法,但我的检查约束实际上涉及第二个表,所以我认为检查约束是确保数据完整性的最安全的方法。

1 个答案:

答案 0 :(得分:1)

因此,在您的示例中,只有在事务隔离级别设置为快照或者数据库上的读取提交的快照设置设置为on时,才会遇到您描述的行为。如果未设置这些选项,则第一个插入将起作用,第二个插入将被阻塞,直到您提交第一个插入,此时第二个插入将失败并出现预期的约束违规。

如果由于任何原因需要在数据库上进行快照隔离,则可以在检查约束中修改函数,以通过使用readcommittedlock表提示复制在读提交隔离级别中将要实验的行为,如下所示:

CREATE FUNCTION CheckDuplicateValues(@value1 INT, @value2 INT) RETURNS INT AS 
BEGIN 
    RETURN (
        SELECT COUNT(*) FROM Table1 WITH (READCOMMITTEDLOCK)
        WHERE Value1 = @value1 
          AND Value2 = @value2
    );
END; 
GO