Why am I getting “Snapshot isolation transaction aborted due to update conflict”?

Posted on

Question :

We have two tables

  1. Parent (Id int identity, Date datetime, Name nvarchar)
  2. Child (Id int identity, ParentId int, Date datetime, Name nvarchar)

The Child having a foreign key relationship to the Parent.

We have enabled database level read committed snapshot isolation.

We only ever insert and delete rows for Parent and Child (no updates)

We have one process (transaction) which deletes old data from Child (and then Parent)

We have multiple other processes (transactions) which insert new data into Parent (and then Child)

The delete process regularly (but not all of the time) gets rolled back, even though the insert process does not insert new Child rows which refer to the Parent rows which the delete wants to delete – it simply creates new Parent rows and one ore more new Child rows which refer to the new Parent

The error when deleting the Parent rows is:

Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table ‘dbo.Child’ directly or
indirectly in database ‘Test’ to update, delete, or insert the row
that has been modified or deleted by another transaction. Retry the
transaction or change the isolation level for the update/delete

I am aware the people suggest having an index on the foreign key column – we’d prefer not to have to do this ideally (for space/performance reasons) – unless this is the only reliably way to get this to work.

Noted this: https://stackoverflow.com/questions/10718668/snapshot-isolation-transaction-aborted-due-to-update-conflict

And pretty good article: https://sqlperformance.com/2014/06/sql-performance/the-snapshot-isolation-level

But neither of these gives me the understanding I would like to have 🙂

Answer :

When deleting from the parent table, SQL Server must check for the existence of any FK child rows that refer to that row. When there is no suitable child index, this check performs a full scan of the child table:

Full child scan

If the scan encounters a row that has been modified since the delete command’s snapshot transaction started, it will fail with an update conflict (by definition). A full scan will obviously touch every row in the table.

With a suitable index, SQL Server can locate and test just the rows in the child table that could match the to-be-deleted parent. When these particular rows have not been modified, no update conflict occurs:

Child seek

Note that foreign key checks under row versioning isolation levels take shared locks (for correctness) as well as detecting update conflicts. For example, the internal hints on the child table accesses above are:

PhyOp_Range TBL: [dbo].[Child]

Sadly this is not currently exposed in execution plans.

Related articles of mine:

I came across this reply by a guy at Microsoft on a thread asking a similar question, and, I thought it was quite insightful:

Without a supporting index on CustomerContactPerson, the statement

DELETE FROM ContactPerson WHERE ID = @ID; Will require a “current”
read of all the rows in CustomerContactPerson to ensure that there
are no CustomerContactPerson rows that refer to the deleted
ContactPerson row. With the index, the DELETE can determine that
there are no related rows in CustomerContactPerson without reading the
rows affected by the other transaction.

Additionally, in a snapshot transaction the pattern for reading data
which you are going to turn around and update is to take an UPDLOCK
when you read. This ensures that you are making your update on the
basis of “current” data, not “consistent” (snapshot) data, and that
when you issue the DML, it the data won’t be locked, and you won’t
unwittingly overwrite another session’s change.

I have received the update from our dev team. it seems that my
understanding on the issue is correct.

Here is their explanation. SNAPSHOT isolation guarantees that you will
see a single, consistent version of the database. When you read the
CustomerContactPerson row in the beginning of the transaction, you
will never be able to read a later version of the row. The DELETE on
ContactPerson would require you to read a version of
CustomerContactPerson row later than your transaction’s snapshot, so
you get an update conflict. It doesn’t matter that you wouldn’t
really update the CustomerContactPerson row, reading it to validate a
FK is treated just the same.

Besides, When the table scan meets the record that is affected by the
other transaction, we can avoid the conflict by locking rows you
intend to update as you read them.

Snapshot isolation, on the other hand, is truly optimistic because
data that is to be modified is not actually locked in advance, but
the data is locked when it is selected for modification. When a data
row meets the update criteria, the snapshot transaction verifies that
the data has not been modified by another transaction after the
snapshot transaction started. If the data has not been modified by
another transaction, the snapshot transaction locks the data, updates
the data, releases the lock, and moves on. If the data has been
modified by another transaction, an update conflict occurs and the
snapshot transaction rolls back.


You must be using Snapshot isolation, not Snapshot read committed. Snapshot update conflicts happen only when using Snapshot isolation and do not happen when using snapshot read committed.

If you are able to use Snapshot Read Committed, then that would be a very easy fix for this problem.

Snapshot Read Committed isolation uses locking (AND obtains row versioning information before each statement) making a snapshot update conflict impossible.

Snapshot update conflicts happen in snapshot isolation (not snapshot read committed) simply because your transaction, when it attempts to commit its changes, is attempting to commit a change to some data whose version has changed since the beginning of the transaction. Given the scenario you outlined, its difficult to understand exactly why you are encountering this problem and perhaps it is related to a table scan vs. what would be an index seek if you had an appropriate index on your FK.

The main point is that you must be using SNAPSHOT, not SNAPSHOT READ COMMITTED isolation as you stated, and you could fix this by using SNAPSHOT READ COMMITTED.

The only way you can get SNAPSHOT is by setting the isolation level to snapshot at the beginning of your transaction. To use SNAPSHOT READ COMMITTED, you must enable it in your database, and then do not set the isolation level in your query or sproc to anything.

The error message provided a general fix and SqlWorldWide suggested one answer to your issue in the comments (“use serializable isolation instead”). The problem is your transaction isolation level. The fix is to change the isolation level for that transaction. Microsoft explains all of this in an article named Lesson 1: Understanding the Available Transaction Isolation Levels

In the article, Microsoft states under a section named Update Conflicts:

There is an additional concurrency problem not yet mentioned because it is specific to the snapshot isolation level. If a specific row (or version of a row) is read in snapshot isolation, SQL Server guarantees that you will get the same row if you issue the query later in the transaction. What happens if the later query is an UPDATE or DELETE statement and the row has changed since it was read the first time? SQL Server can’t use the current version of the row as the base for the update because it would break the promise of the row not changing while the snapshot transaction is active. And it can’t use the row version used by the snapshot transaction as a base because the other transaction that updated or deleted the row would experience a lost update (which is not allowed or supported in SQL Server). Instead, the snapshot transaction is rolled back, and it receives the following error message:

Msg 3960, Level 16, State 4, Line 1
Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot
isolation to access table ‘Test.TestTran’ directly or indirectly in database ‘TestDatabase’ to
update, delete, or insert the row that has been modified or deleted by another transaction.
Retry the transaction or change the isolation level for the update/delete statement.

This is similar to the error you are receiving.

I’m speculating but I think this is what’s happening. When you delete a parent row, the engine needs to enforce whatever ON DELETE rule is defined in the foreign key — regardless of whether you know you have by then deleted all child rows, the engine has no way of knowing that. Since, as you say, you don’t have an index on the foreign key column in the child table (for performance reasons), the engine resorts to a clustered index scan (I’m assuming you do have a PK in the child table) and as soon as it stumbles upon the first stale row it aborts the transaction, because it cannot know the foreign key value inserted outside the snapshot it is looking at.

If you had an index on the foreign key column of the child table, the server would be able to selectively access only the potentially affected rows, that is, no rows (since you have deleted them by then), thus avoiding the snapshot conflict and the clustered index scan.

Leave a Reply

Your email address will not be published. Required fields are marked *