单个 DELETE 语句(使用 WHERE( ...SELECT...) )引用安全吗?

Is a single DELETE statement (with WHERE( ...SELECT...)) referentially safe?

我知道任何单个 SQL 语句 is implicitly run inside a transaction.
但这足以保证 delete from ... where (... select... ) 语句的关系完整性吗?

隔离级别如何发挥作用?

这是我的具体例子。

我有两个表:CallUser,以及一个外键 Call.UserId -> User.Id

多个调用可以指向同一个用户。
可能没有呼叫指向特定用户。
有些呼叫没有关联的用户。
经典 零或一对多 关系:Call [*] -- [0..1] User.

如果没有调用指向某个用户,则称该用户是 孤儿。 新呼叫一直在添加,因此孤立用户在将来的某个时候可能不会成为孤立用户。

我想清理孤立用户。这可以在单个 SQL 语句中完成:

delete [dbo].[User] 
    FROM [dbo].[User] AS [U]
    WHERE ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Call] AS [C]
        WHERE [U].[Id] = [C].[UserId]
    ))

问题是:这样安全吗?(考虑到可能的 Call 并行插入 运行)

我的意思是,如果这样(请原谅我的伪SQL):

BEGIN TRANSACTION

@orphanIds = SELECT U.Id
    FROM [dbo].[User] AS [U]
    WHERE ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Call] AS [C]
        WHERE [U].[Id] = [C].[UserId]
    ))

DELETE FROM [dbo].[User] 
    WHERE Id in (@orphanIds)

COMMIT

...等同于单个语句,使用 SQL 服务器的默认隔离级别 READ COMMITTED 操作不安全。

selectdelete 之间,可能会提交另一个插入 Calls 的事务,使(某些)刚刚选择的用户成为非孤立用户,从而失败我的交易违反了 FK。这很容易测试:只需在 selectdelete 之间添加一个 WAITFOR,然后尝试在此事务等待时插入新调用。插入操作将立即执行,并会导致此事务中的 FK 违规。

你是对的,它不安全,所以我认为在这种情况下你可以在调用上使用排他锁 table 直到事务结束。

您的担心是有道理的。 DELETE 语句在其完整执行期间不会锁定 Call table。如 MSDN 所述:

By default, a DELETE statement always acquires an exclusive (X) lock on the table it modifies, and holds that lock until the transaction completes. With an exclusive (X) lock, no other transactions can modify data

但是,调用 table 不是被修改的那个。内部 SELECT 语句将发出共享锁,但该锁不会持续到整个语句结束,如 MSDN:

所述

Shared (S) locks on a resource are released as soon as the data has been read

因此,虽然并发 INSERT Into Call 语句必须等待 SELECT 完成,但它会在之后立即完成,并且可以与 DELETE 操作同时执行.

This question 列出了 SO 用户遇到的几个类似案例。

您可以申请 HOLDLOCK locking hint:

HOLDLOCK - Hold a shared lock until completion of the transaction instead of releasing the lock as soon as the required table, row, or data page is no longer required. HOLDLOCK is equivalent to SERIALIZABLE.

你会这样写你的陈述:

DELETE FROM User
WHERE  Id NOT IN (
    SELECT UserId FROM Call WITH (HOLDLOCK)
)

不过要注意潜在的死锁。如果在并发进程中执行这样的事情,就会发生这种情况:

INSERT INTO Call (Id, UserId)
SELECT 123, Id
FROM   User WITH (HOLDLOCK)
WHERE  Name = 'Johnson'

避免此类死锁的一种方法是确保 table 在所有情况下都以相同顺序锁定。

请注意,使用隔离级别 REPEATABLE READ 不会提供必要的保护,如 MSDN 所述:

This prevents other transactions from modifying any rows that have been read by the current transaction. Other transactions can insert new rows that match the search conditions of statements issued by the current transaction.