I know that any single SQL statement is implicitly run inside a transaction.
But is this enough to guarantee relational integrity for a delete from ... where (... select... )
statement?
How does the isolation level play into this?
Here's my concrete example.
I have two tables: Call
and User
, and a foreign key Call.UserId -> User.Id
.
Several calls can point to the same user.
It's possible that no calls point to a particular user.
Some calls don't have an associated user.
Classic zero-or-one to many relationship: Call [*] -- [0..1] User
.
An user is said to be orphaned if there are no calls pointing to it. New calls are added all the time, so an orphaned user may become not orphaned at some point in the future.
I want to clean up orphaned users. This can be done in a single SQL statement:
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]
))
The question is: Is this safe? (taking into account possible Call
insertions running in parallel)
I mean, if this (excuse my pseudo-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
...is equivalent to the single statement, the operation is NOT safe using SQL Server's default isolation level of READ COMMITTED.
Between the select
and the delete
, another transaction that inserts Calls
may be committed, rendering (some) of the just-selected users non-orphan, and thus failing my transaction with a FK violation. This is easy to test: just add a WAITFOR
between the select
and the delete
, and try inserting new calls while this transaction is waiting. The insert operation will execute immediately and will cause the FK violation in this transaction.