36

I'm interested in whether a SELECT FOR UPDATE query will lock a non-existent row.

Example

Table FooBar with two columns, foo and bar, foo has a unique index.

  • Issue query SELECT bar FROM FooBar WHERE foo = ? FOR UPDATE
  • If the first query returns zero rows, issue a query
    INSERT INTO FooBar (foo, bar) values (?, ?)

Now is it possible that the INSERT would cause an index violation or does the SELECT FOR UPDATE prevent that?

Interested in behavior on SQLServer (2005/8), Oracle and MySQL.

informatik01
  • 16,038
  • 10
  • 74
  • 104
Mike Q
  • 22,839
  • 20
  • 87
  • 129

5 Answers5

20

MySQL

SELECT ... FOR UPDATE with UPDATE

Using transactions with InnoDB (auto-commit turned off), a SELECT ... FOR UPDATE allows one session to temporarily lock down a particular record (or records) so that no other session can update it. Then, within the same transaction, the session can actually perform an UPDATE on the same record and commit or roll back the transaction. This would allow you to lock down the record so no other session could update it while perhaps you do some other business logic.

This is accomplished with locking. InnoDB utilizes indexes for locking records, so locking an existing record seems easy--simply lock the index for that record.

SELECT ... FOR UPDATE with INSERT

However, to use SELECT ... FOR UPDATE with INSERT, how do you lock an index for a record that doesn't exist yet? If you are using the default isolation level of REPEATABLE READ, InnoDB will also utilize gap locks. As long as you know the id (or even range of ids) to lock, then InnoDB can lock the gap so no other record can be inserted in that gap until we're done with it.

If your id column were an auto-increment column, then SELECT ... FOR UPDATE with INSERT INTO would be problematic because you wouldn't know what the new id was until you inserted it. However, since you know the id that you wish to insert, SELECT ... FOR UPDATE with INSERT will work.

CAVEAT

On the default isolation level, SELECT ... FOR UPDATE on a non-existent record does not block other transactions. So, if two transactions both do a SELECT ... FOR UPDATE on the same non-existent index record, they'll both get the lock, and neither transaction will be able to update the record. In fact, if they try, a deadlock will be detected.

Therefore, if you don't want to deal with a deadlock, you might just do the following:

INSERT INTO ...

Start a transaction, and perform the INSERT. Do your business logic, and either commit or rollback the transaction. As soon as you do the INSERT on the non-existent record index on the first transaction, all other transactions will block if they attempt to INSERT a record with the same unique index. If the second transaction attempts to insert a record with the same index after the first transaction commits the insert, then it will get a "duplicate key" error. Handle accordingly.

SELECT ... LOCK IN SHARE MODE

If you select with LOCK IN SHARE MODE before the INSERT, if a previous transaction has inserted that record but hasn't committed yet, the SELECT ... LOCK IN SHARE MODE will block until the previous transaction has completed.

So to reduce the chance of duplicate key errors, especially if you hold the locks for awhile while performing business logic before committing them or rolling them back:

  1. SELECT bar FROM FooBar WHERE foo = ? LOCK FOR UPDATE
  2. If no records returned, then
  3. INSERT INTO FooBar (foo, bar) VALUES (?, ?)
Marcus Adams
  • 53,009
  • 9
  • 91
  • 143
  • Generally true, but SELECT ... FOR UPDATE with INSERT nevertheless is useless IMHO, at least in MySQL. Please look [here](http://stackoverflow.com/a/31184293/3239842) for an explanation. – Binarus Jul 02 '15 at 12:35
  • 4
    About the caveat: If I do `SELECT ... FOR UPDATE` on a non-existent record, it most definitely _does_ lock! In fact, it seems to lock the entire index (or the entire table - not sure). I read that this depends on what index (if any) is used to find the row. Tested with MySQL 5.7.25. – Timo Jan 24 '19 at 13:20
  • Another fix for the caveat you mentioned is to set isolation level to `READ COMMITTED`, which stops MySQL from setting gap locks. This works for my use case but for other people, they might want to look into this isolation level and see if this is the best solution. – Mygod Nov 26 '20 at 18:28
10

In Oracle, the SELECT ... FOR UPDATE has no effect on a non-existent row (the statement simply raises a No Data Found exception). The INSERT statement will prevent a duplicates of unique/primary key values. Any other transactions attempting to insert the same key values will block until the first transaction commits (at which time the blocked transaction will get a duplicate key error) or rolls back (at which time the blocked transaction continues).

DCookie
  • 42,630
  • 11
  • 83
  • 92
  • I would restate the part about "the statement simply raises a non data found exception"; a SELECT...FOR UPDATE could return rows and still have no effect on a non-existent row. ;) – Patrick Marchand Aug 30 '10 at 18:32
  • Not in the case the OP stated, where the select was on foo=explicit_value, foo being uniquely indexed. – DCookie Aug 30 '10 at 19:16
  • It is possible for SELECT statement to return 0 rows. It only raises NO_DATA_FOUND if it is invoked with INTO. – jva Aug 30 '10 at 19:46
  • Point taken. I was looking at the pseudocode and translating to a procedural context. – DCookie Aug 30 '10 at 22:32
  • What transaction isolation is Oracle using? Is this under snapshot isolation? – Gili Jan 01 '13 at 19:54
  • @gili, Oracle only supports: READ COMMITTED and SERIALIZABLE. You can't do a true dirty read - uncommitted transactions are not visible in other sessions. An Oracle read query will never block or be blocked by another query, even if the other query is an update. – DCookie Oct 26 '21 at 15:37
  • @DCookie What happens if `SERIALIZABLE` transactions execute conflicting commands followed by other commands that do not conflict? For example, `UPDATE value=2 WHERE value=1` and `UPDATE value=3 WHERE value=1` followed by some queries that keep the transactions open longer before eventually committing? If the transactions do not block, does that mean that `SERIALIZABLE` transactions can fail to commit? – Gili Oct 27 '21 at 17:15
  • @Gili, this is getting too much for comments. Have a look at this blog by Tom Kyte: https://blogs.oracle.com/oraclemagazine/post/on-transaction-isolation-levels – DCookie Oct 29 '21 at 15:31
2

On Oracle:

Session 1

create table t (id number);
alter table t add constraint pk primary key(id);

SELECT *
FROM t
WHERE id = 1
FOR UPDATE;
-- 0 rows returned
-- this creates row level lock on table, preventing others from locking table in exclusive mode

Session 2

SELECT *
FROM t 
FOR UPDATE;
-- 0 rows returned
-- there are no problems with locking here

rollback; -- releases lock


INSERT INTO t
VALUES (1);
-- 1 row inserted without problems
jva
  • 2,797
  • 1
  • 26
  • 41
1

I wrote a detailed analysis of this thing on SQL Server: Developing Modifications that Survive Concurrency

Anyway, you need to use SERIALIZABLE isolation level, and you really need to stress test.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
A-K
  • 16,804
  • 8
  • 54
  • 74
0

SQL Server only has the FOR UPDATE as part of a cursor. And, it only applies to UPDATE statements that are associated with the current row in the cursor.

So, the FOR UPDATE has no relationship with INSERT. Therefore, I think your answer is that it's not applicable in SQL Server.

Now, it may be possible to simulate the FOR UPDATE behavior with transactions and locking strategies. But, that may be more than what you're looking for.

bobs
  • 21,844
  • 12
  • 67
  • 78
  • Historically, there may have been a difference in behavior (when updates locked the whole table) - but not in the last decade (and quite possibly, not since FOR UPDATE was introduced). – JulesLt Aug 30 '10 at 16:35
  • I think the OP would be referring to `WITH (UPDLOCK)` then… – binki Sep 09 '20 at 15:17