6

I have a table called seats, which has a schema like this

id,taken

For each user, I take a random untaken id and assign to that user, here for simplicity say, I make taken =1. The query I am using

   update seats u inner join  (
        SELECT id from seats  
        where taken is null limit 1) s 
   on s.id = u.id set taken = 1;

This query takes a random seat with taken flag null, and for that seat it makes the flag 1. While this query is working fine, is this thread safe?

Consider this scenario, I have two users in parallel. For user1, I select row X, and just before update query is ran user2 checks in, and for that user select query returns the same row as user1. So I will end updating the same row twice.

Is this scenario possible with this query?

2
  • Why do you use a join and not the simple (not standard but allowed in MySQL) update seats set taken = 1 where taken is null <order by whatever> limit 1; Nov 24, 2014 at 12:04
  • @ypercube that makes sense.However, out of curiosity, I would like to know is my query thread-safe as well like yours?
    – Max
    Nov 24, 2014 at 14:26

2 Answers 2

2
+50

ASSESSMENT

Your query is kind of dangerous to the MySQL Query Optimizer.

I have an old post (Problem with MySQL subquery). The question posed involved this query

DELETE FROM test WHERE id =
(SELECT id FROM (SELECT * FROM test) temp ORDER BY RAND() LIMIT 1);

Although the MySQL Query Parser will work with this query and accept it's syntax, the MySQL Documentation implies that some data may disappear during certain optimization phases. In your case, you are accessing just one row to get an id and then update that row. Notwithstanding, the remote possibility of accessing data from a subquery that might not return an id because it can intermittently disappear does not strike me as transaction-safe, let alone thread-safe.

Your query looks similar to the query in that old post

  • Non-SELECT query
  • SELECT subquery
  • Subquery uses LIMIT

This issues goes a limit beyond just being thread-safe or not. Even if it is, you should not trust MySQL's interpretation of grabbing an available seat in the middle of a subquery. You need your query to be transaction-safe.

SUGGESTION #1

My advice to you would be to rewrite this as three queries in a transaction.

Perhaps something like this:

START TRANSACTION;
SELECT id from seats WHERE taken is null limit 1 FOR UPDATE;
SELECT id INTO @available_id from seats WHERE taken is null limit 1;
UPDATE seats SET taken = 1 WHERE id = @available_id;
COMMIT;

or with two queries

START TRANSACTION;
SELECT id INTO @available_id from seats WHERE taken is null limit 1 FOR UPDATE;
UPDATE seats SET taken = 1 WHERE id = @available_id;
COMMIT;

For further info, please read MySQL Documentation on SELECT ... FOR UPDATE.

SUGGESTION #2

I would also make sure that the seats table has an index on the taken column.

If not, add this index

ALTER TABLE seats ADD INDEX (taken);

Do this in Dev/Staging Environment and test your query before doing this in Production.

GIVE IT A TRY !!!

2
  • in your first transaction, why do you have two select queries? specifically this select query "SELECT id from seats WHERE taken is null limit 1 FOR UPDATE;"
    – Max
    Nov 25, 2014 at 6:33
  • At first, I was thinking that the first SELECT triggers the lock and the second SELECT does the actual retrieval. Now that I think about it, the SELECT id INTO @available_id from seats WHERE taken is null limit 1 FOR UPDATE; is most likely sufficient. Nov 25, 2014 at 13:00
0

If you are using InnoDB, You can secure your transaction with SELECT ... FOR UPDATE sentence

http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html

With this, you can perform an exclusive lock on the row while your transaction is not finished. Other sessions will wait until the row lock is released.

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.