6

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 statement.

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 :)

  • 1) Do you have any triggers? and 2) is the relation Parent/Child enforced by a FK? – Remus Rusanu May 9 '17 at 18:36
  • No triggers. Yes the FK relationship is critical to this problem - it doesn't happen without it. And it doesn't happen if there is a non-unique index on the foreign key column either. – Mark May 9 '17 at 18:38
14

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]
    Hints( READ-COMMITTEDLOCK FORCEDINDEX DETECT-SNAPSHOT-CONFLICT )

Sadly this is not currently exposed in execution plans.

Related articles of mine:

3

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.

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

1

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.

0

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.

  • Thanks. Yes I do get that.... what I don't understand is why on a different transaction I can add a totally different child for a totally different parent and that still invalidates the first transaction's parent which is nothing to do with it (however, if I simply add a non-unique index on the child's foreign key column, then this problem totally goes away!) – Mark May 9 '17 at 18:35
  • 1
    Interesting. You should add that to the original question as it would help come up with a better answer. Do you have any cascading rules for deletes in the foreign key relationship? – Antoine Hernandez May 9 '17 at 18:58
  • Thanks - it doesn't seem to make any difference what the cascade rule is. – Mark May 9 '17 at 19:03
0

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.

  • What leads you to believe that all the other answers, none of which assume the OP is wrong about how they've got their DB configured, are incorrect? Especially given one of those answers was accepted as correct by the OP? – RDFozz Jan 25 '18 at 23:03
  • I did not state that the other answers are not correct. They very well could be correct. My point is that snapshot update conflicts can NOT happen when using Read Committed Snapshot isolation, which is what the questioner stated he is using. He must be using snapshot, but incorrectly stated he is using Read Committed Snapshot. – user3444696 Jan 25 '18 at 23:37
  • I should also point out that if the poster is able to use Snapshot Read Committed then it would be exactly the solution he/she wants, because it would solve the problem without the need for an index, and the poster stated they prefer not to have to create an index. – user3444696 Jan 25 '18 at 23:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.