Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a transaction that only updates a single table always isolated?

According to the UPDATE documentation, an UPDATE always acquires an exclusive lock on the whole table. However, I am wondering if the exclusive lock is acquired before the rows to be updated are determined or only just before the actual update.

My concrete problem is that I have a nested SELECT in my UPDATE like this:

UPDATE Tasks
SET Status = 'Active'
WHERE Id = (SELECT TOP 1 Id 
            FROM Tasks
            WHERE Type = 1
                AND (SELECT COUNT(*) 
                     FROM Tasks 
                     WHERE Status = 'Active') = 0
            ORDER BY Id)

Now I am wondering whether it is really guaranteed that there is exactly one task with Status = 'Active' afterwards if in parallel the same statement may be executed with another Type:

UPDATE Tasks
SET Status = 'Active'
WHERE Id = (SELECT TOP 1 Id 
            FROM Tasks
            WHERE Type = 2           -- <== The only difference
                AND (SELECT COUNT(*) 
                     FROM Tasks 
                     WHERE Status = 'Active') = 0
            ORDER BY Id)

If for both statements the rows to change would be determined before the lock is acquired, I could end up with two active tasks which I must prevent.

If this is the case, how can I prevent it? Can I prevent it without setting the transaction level to SERIALIZABLE or messing with lock hints?

From the answer to Is a single SQL Server statement atomic and consistent? I learned that the problem arises when the nested SELECT accesses another table. However, I'm not sure if I have to care about this issue if only the updated table is concerned.

like image 644
lex82 Avatar asked Jan 08 '16 14:01

lex82


People also ask

What is isolated transaction in DBMS?

Isolation − A transaction is isolated from other transactions. i.e. A transaction is not affected by another transaction. Although multiple transactions execute concurrently it must appear as if the transaction are running serially (one after the other).

How one transaction is isolated from another in SQL?

SQL Server provides 5 Isolation levels to implement with SQL Transaction to maintain data concurrency in the database. Isolation level is nothing but locking the row while performing some task, so that other transaction can not access or will wait for the current transaction to finish its job.

How does transaction isolation work?

Transactions specify an isolation level that defines how one transaction is isolated from other transactions. Isolation is the separation of resource or data modifications made by different transactions. Isolation levels are described for which concurrency side effects are allowed, such as dirty reads or phantom reads.

What is not a transaction isolation level in SQL Server?

Read committed transaction isolation level does not protect transactions from phantom reads, non-repeatable reads and lost updates problems. Read uncommitted does not protect transactions from phantom reads, non-repeatable read, lost updates and dirty reads problems.


1 Answers

If you want exactly one task with static = active, then set up the table to ensure this is true. Use a filtered unique index:

create unique index unq_tasks_status_filter_active on tasks(status)
    where status = 'Active';

A second concurrent update might fail, but you will be ensured of uniqueness. Your application code can process such failed updates, and re-try.

Relying on the actual execution plans of the updates might be dangerous. That is why it is safer to have the database do such validations. Underlying implementation details could vary, depending on the environment and version of SQL Server. For instance, what works in a single threaded, single processor environment may not work in a parallel environment. What works with one isolation level may not work with another.

EDIT:

And, I cannot resist. For efficiency purposes, consider writing the query as:

UPDATE Tasks
    SET Status = 'Active'
    WHERE NOT EXISTS (SELECT 1
                      FROM Tasks
                      WHERE Status = 'Active'
                     ) AND
          Id = (SELECT TOP 1 Id 
                FROM Tasks
                WHERE Type = 2           -- <== The only difference
                ORDER BY Id
               );

Then place indexes on Tasks(Status) and Tasks(Type, Id). In fact, with the right query, you might find that the query is so fast (despite the update on the index) that your worry about current updates is greatly mitigated. This would not solve a race condition, but it might at least make it rare.

And if you are capturing errors, then with the unique filtered index, you could just do:

UPDATE Tasks
    SET Status = 'Active'
    WHERE Id = (SELECT TOP 1 Id 
                FROM Tasks
                WHERE Type = 2           -- <== The only difference
                ORDER BY Id
               );

This will return an error if a row already is active.

Note: all these queries and concepts can be applied to "one active per group". This answer is addressing the question that you asked. If you have a "one active per group" problem, then consider asking another question.

like image 186
Gordon Linoff Avatar answered Oct 20 '22 00:10

Gordon Linoff