117

This is not a full/correct MySQL query only pseudo-code:

Select *
 from Notifications as n
 where n.date > (CurrentDate-10 days)
 limit by 1
 FOR UPDATE

http://dev.mysql.com/doc/refman/5.0/en/select.html states: If you use FOR UPDATE with a storage engine that uses page or row locks, rows examined by the query are write-locked until the end of the current transaction

Is here only the one record returned locked by MySQL or all records it has to scan to find the single record?

halfer
  • 19,824
  • 17
  • 99
  • 186
Markus
  • 4,062
  • 4
  • 39
  • 42
  • 1
    Locking `all Records it has to SCAN TO FIND the SINGLE RECORD` would be so terribly stupid, that I really REALLY doubt MySQL works like this. Think about algorithm in the MySQL search engine - when it sees some row and *knows* it's not the row you need, why on Earth would it spend extra time to set the lock?! I suggest you to un-accept answer, so that other MySQL people could comment on this – Alexander Malakhov May 20 '11 at 05:36
  • Also, being Oracle DB developer I assure you that Oracle locks only rows which satisfy `WHERE` condition. Hence it's technically possible and I don't think MySQL *that* *much* inferior – Alexander Malakhov May 20 '11 at 05:39
  • 1
    While it looks like my answer ended up being correct, I suggest that you select the other answer instead as it is correct and has actually *tested* it, while mine just refers to the documentation which, as Alexander points out, could be read in more than one way. – El Yobo May 20 '11 at 12:45

7 Answers7

146

Why don't we just try it?

Set up the database

CREATE DATABASE so1;
USE so1;
CREATE TABLE notification (`id` BIGINT(20), `date` DATE, `text` TEXT) ENGINE=InnoDB;
INSERT INTO notification(id, `date`, `text`) values (1, '2011-05-01', 'Notification 1');
INSERT INTO notification(id, `date`, `text`) values (2, '2011-05-02', 'Notification 2');
INSERT INTO notification(id, `date`, `text`) values (3, '2011-05-03', 'Notification 3');
INSERT INTO notification(id, `date`, `text`) values (4, '2011-05-04', 'Notification 4');
INSERT INTO notification(id, `date`, `text`) values (5, '2011-05-05', 'Notification 5');

Now, start two database connections

Connection 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

Connection 2

BEGIN;

If MySQL locks all rows, the following statement would block. If it only locks the rows it returns, it shouldn't block.

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

And indeed it does block.

Interestingly, we also cannot add records that would be read, i.e.

INSERT INTO notification(id, `date`, `text`) values (6, '2011-05-06', 'Notification 6');

blocks as well!

I can't be sure at this point whether MySQL just goes ahead and locks the entire table when a certain percentage of rows are locked, or where it's actually really intelligent in making sure the result of the SELECT ... FOR UPDATE query can never be changed by another transaction (with an INSERT, UPDATE, or DELETE) while the lock is being held.

Frans
  • 3,670
  • 1
  • 31
  • 29
  • 9
    +1 for proof by example. This behavior looks terrifying. Have you tried to lock just 1 row? Why don't you select by `id` column, I don't trust date literals :). What's the version of MySQL/InnoDB ? – Alexander Malakhov May 20 '11 at 07:50
  • 65
    +1. the whole table is not blocked if a lock is taken on a unique column. i tried `CREATE TABLE notification (`id` BIGINT(20) NOT NULL AUTO_INCREMENT, `date` DATE, `text` TEXT, PRIMARY KEY (`id`)) ENGINE=InnoDB;` which works. But it does not work if unique/primary key is not specified ie. `SELECT * FROM notification WHERE `id` = '1' FOR UPDATE;` on the original schema – deepak Jul 12 '12 at 09:23
  • Good answer and went right to the heart of something I'm seeing. That is I get the locking semantics I want/expect when the `WHERE` clause is id=x but not when it's name='foo'. But I'd love to see this explained officially in MySQL docs and I don't feel like El Yobo's link is quite that... – Chris Markle Feb 14 '13 at 01:33
  • 3
    This example was very instructive for me. Attempting to select individual records for update by their ID resulted in blocking, until I made the ID field a primary key. Then, everything worked fine. The important lesson is to only use FOR UPDATE if you are using a unique key, otherwise your entire table may as well be locked since it'll scan the entire table/index to find all matches, therefore locking everything. – fabspro Dec 05 '13 at 11:51
  • 9
    @fabspro You don't need to use a unique key. Any key will work, whether it's unique or not. – thekingoftruth Jun 27 '14 at 07:05
  • @thekingoftruth True. I was probably using an unindexed field (a bad idea anyway). – fabspro Jul 01 '14 at 11:10
  • 1
    @Frans - is it locking because the search is based on non-primary columns - IMO it locks only on "For index records the search encounters" as given in the documentation – surpavan Dec 30 '16 at 19:11
  • 6
    My own tests show that using `for update` with where filters on non-indexed columns results in whole-table locking, while with where filters on indexed columns results in the desired behaviour of filtered row locking. So primary key is not necessary, any key is enough. – CMCDragonkai Apr 12 '17 at 01:52
  • 2
    However upon further testing, with an index, if you run `for update` on a where filter that results in an empty set, this does not block another query for the same empty set. Whereas without an index, running `for update` will block the query for the same empty set. – CMCDragonkai Apr 12 '17 at 05:47
  • It also depends on the indexes such as primary key and unique indexes and same for data types e.g. it locks less records if you select range for update based on unique int index or primary key. But it could be more if you select range for update based on text fields. – M Rostami Mar 20 '20 at 13:20
  • 1
    You don't have index on `date` column - how you expect to avoid lock on row for `2011-05-02`? – i486 Apr 27 '20 at 09:12
43

The thread is pretty old, just to share my two cents regarding the tests above performed by @Frans

Connection 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

Connection 2

BEGIN;

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

The concurrent transaction 2 will be blocked for sure, but the reason is NOT that the transaction 1 is holding the lock on the whole table. The following explains what has happened behind the scene:

First of all, the default isolation level of the InnoDB storage engine is Repeatable Read. In this case,

1- When the column used in where condition is not indexed (as the case above):

The engine is obliged to perform a full table scan to filter out the records not matching the criteria. EVERY ROW that have been scanned are locked in the first place. MySQL may release the locks on those records not matching the where clause later on. It is an optimization for the performance, however, such behavior violates the 2PL constraint.

When transaction 2 starts, as explained, it needs to acquire the X lock for each row retrieved although there exists only a single record (id = 2) matching the where clause. Eventually the transaction 2 will be waiting for the X lock of the first row (id = 1) until the transaction 1 commits or rollbacks.

2- When the column used in where condition is a primary index

Only the index entry satisfying the criteria is locked. That's why in the comments someone says that some tests are not blocked.

3 - When the column used in where condition is an index but not unique

This case is more complicated. 1) The index entry is locked. 2) One X lock is attached to the corresponding primary index. 3) Two gap locks are attached to the non-existing entries right before and after the record matching the search criteria.

isapir
  • 21,295
  • 13
  • 115
  • 116
flyingice
  • 591
  • 4
  • 6
  • What about the case when when the column condition is not a primary index, but occurs only once in value without the UNIQUE KEY in the table. Will it have gap locks? – arqam May 18 '22 at 08:45
38

I know this question is pretty old, but I've wanted to share the results of some relevant testing I've done with indexed columns which has yielded some pretty strange results.

Table structure:

CREATE TABLE `t1` (                       
  `id` int(11) NOT NULL AUTO_INCREMENT,                 
  `notid` int(11) DEFAULT NULL,                         
  PRIMARY KEY (`id`)                                    
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

12 rows inserted with INSERT INTO t1 (notid) VALUES (1), (2),..., (12). On connection 1:

BEGIN;    
SELECT * FROM t1 WHERE id=5 FOR UPDATE;

On connection 2, the following statements are blocked:

SELECT * FROM t1 WHERE id!=5 FOR UPDATE;
SELECT * FROM t1 WHERE id<5 FOR UPDATE;
SELECT * FROM t1 WHERE notid!=5 FOR UPDATE;
SELECT * FROM t1 WHERE notid<5 FOR UPDATE;
SELECT * FROM t1 WHERE id<=4 FOR UPDATE;

The strangest part is that SELECT * FROM t1 WHERE id>5 FOR UPDATE; is not blocked, nor are any of

...
SELECT * FROM t1 WHERE id=3 FOR UPDATE;
SELECT * FROM t1 WHERE id=4 FOR UPDATE;
SELECT * FROM t1 WHERE id=6 FOR UPDATE;
SELECT * FROM t1 WHERE id=7 FOR UPDATE;
...

I'd also like to point out that it seems the entire table is locked when the WHERE condition in the query from connection 1 matches a non-indexed row. For example, when connection 1 executes SELECT * FROM t1 WHERE notid=5 FOR UPDATE, all select queries with FOR UPDATE and UPDATE queries from connection 2 are blocked.

-EDIT-

This is a rather specific situation, but it was the only I could find that exhibits this behaviour:

Connection 1:

BEGIN;
SELECT *, @x:=@x+id AS counter FROM t1 CROSS JOIN (SELECT @x:=0) b HAVING counter>5 LIMIT 1 FOR UPDATE;
+----+-------+-------+---------+
| id | notid | @x:=0 | counter |
+----+-------+-------+---------+
|  3 |     3 |     0 |       9 |
+----+-------+-------+---------+
1 row in set (0.00 sec)

From connection 2:

SELECT * FROM t1 WHERE id=2 FOR UPDATE; is blocked;

SELECT * FROM t1 WHERE id=4 FOR UPDATE; is not blocked.

concat
  • 3,107
  • 16
  • 30
  • this is interesting. I want to know why those blocks happen too. – deathemperor Sep 08 '17 at 10:20
  • 1
    it's because of gap locks which is based on indexes. – M Rostami Mar 20 '20 at 12:18
  • 1
    It can be due to very low number of rows in the table. The DB could have chosen to bypass the index and directly do full table scan to get the row. with id=5. This explains why rows with id < 5 were blocked but rows with id > 5 were not blocked. – Abdul Rauf Dec 27 '21 at 16:55
13

Following links from the documentation page you posted gives more information about locking. In this page

A SELECT ... FOR UPDATE reads the latest available data, setting exclusive locks on each row it reads. Thus, it sets the same locks a searched SQL UPDATE would set on the rows.

This seems pretty clear that it is all rows that it has to scan.

El Yobo
  • 14,823
  • 5
  • 60
  • 78
  • Not sure if I understood you correctly, but are you saying, that if I'm selecting last row and columns in `WHERE` aren't indexed, it will lock the whole table ? This is obviously wrong. At least Oracle locks only `select`ed rows – Alexander Malakhov May 20 '11 at 04:15
  • Oracle is much better than MySQL :) I don't know through experimentation, only through reading the documentation, but that *is* what it seems to say. It does sounds rather stupid though. – El Yobo May 20 '11 at 04:22
  • 1
    It sounds **enormously** stupid. Given popularity of MySQL, I really doubt it's the way it works. And statement `setting exclusive locks on each row it reads` can be interpreted other way. Though I agree it's not 100% clear – Alexander Malakhov May 20 '11 at 05:29
  • I don't know; MySQL does some pretty incredibly stupid things :-/ I generally just BEGIN, do whatever I need to do and COMMIT, but have no idea what it's doing internally when I do that. – El Yobo May 20 '11 at 05:57
  • 3
    @ Alexander Malakhov - That does seem to be the way it works, I just tested it and found it locks the whole table unless you index the columns in your where clause. The documentation should really be improved here, as it's extremely confusing. "Each row it reads" would make me think that the rows it "reads" would be based on the conditions in my WHERE clause. So if my WHERE clause constrained the result to 1 returned row, that is the row I would expect to be locked. But it seems "each row it reads" means each row "scanned by the database". – dcp Jan 08 '14 at 21:31
12

From mysql official doc:

A locking read, an UPDATE, or a DELETE generally set record locks on every index record that is scanned in the processing of the SQL statement. It does not matter whether there are WHERE conditions in the statement that would exclude the row.

For the case discussed in Frans' answer, all rows are locked because there's a table scan during sql processing:

If you have no indexes suitable for your statement and MySQL must scan the entire table to process the statement, every row of the table becomes locked, which in turn blocks all inserts by other users to the table. It is important to create good indexes so that your queries do not unnecessarily scan many rows.

Check the latest doc here: https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html

yxc
  • 229
  • 2
  • 5
2

As others have mentioned, SELECT... FOR UPDATE locks all rows encountered in the default isolation level. Try setting the isolation for the session which runs this query to READ COMMITTED, for example precede the query with: set session transaction isolation level read committed;

tcoker
  • 21
  • 2
-6

It locks all the rows selected by query.

r.bhardwaj
  • 1,603
  • 6
  • 28
  • 54