Lesson 0006 · Internals & Performance
Plain reads are lock-free (Lesson 0005) — but the moment you write, or read for update, InnoDB takes locks on index records. Here's which locks, and why two transactions can deadlock.
In Lesson 0005 you learned that a plain
SELECT takes no locks — it reads an MVCC snapshot. This lesson is
about the other operations: UPDATE, DELETE, and
locking reads (SELECT … FOR UPDATE / FOR SHARE). These
do take locks, and understanding what they lock explains blocked
queries, why indexing matters for writes too, and the dreaded deadlock.
InnoDB locks are placed on index records — always, even on the clustered
index if you defined no secondary index (recall 0001: the table is the PK index). This
single fact explains everything below: which rows get locked depends on which index the
query walks, so a poorly-indexed WHERE locks far more than you intended.
— MySQL Manual: "Record locks always lock index records, even if a table is defined with no indexes."
| Lock | What it locks | Purpose |
|---|---|---|
| record lock | A single index record. | Stop others updating/deleting that row. |
| gap lock | The gap between index records (or before the first / after the last). | Stop others inserting into the gap. |
| next-key lock | A record lock + the gap before it. | Both at once — the default for scans. |
Remember from 0005 that InnoDB's REPEATABLE READ prevents phantoms even for
locking reads. This is how: by default InnoDB uses next-key
locks for searches and index scans, so it locks not just the matching rows but the
gaps around them — blocking the INSERTs that would create phantom
rows.
— MySQL Manual: "InnoDB uses next-key locks for searches and index scans, which prevents phantom rows."
| S (shared) | Lets the holder read the row; others may also hold S. Taken by SELECT … FOR SHARE. |
| X (exclusive) | Lets the holder update/delete the row; blocks all other locks. Taken by UPDATE, DELETE, SELECT … FOR UPDATE. |
A plain SELECT takes neither — it's the lock-free snapshot read
from 0005. Reach for FOR UPDATE only when you'll write based on what you read (e.g.
check-then-decrement inventory).
A deadlock is a cycle of waiting: each transaction holds a lock the other needs, so neither can proceed. — MySQL Manual: "A deadlock is a situation in which multiple transactions are unable to proceed because each transaction holds a lock that is needed by another one."
InnoDB detects the cycle automatically and rolls back one transaction — the
victim — releasing its locks so the other proceeds. The victim's application gets
error 1213 (ER_LOCK_DEADLOCK).
— MySQL Manual: "InnoDB detects the condition and rolls back one of the transactions (the victim)."
A deadlock is not a bug to eliminate but a condition to handle: catch error
1213 and retry the transaction. Reduce their frequency by (1) keeping
transactions small and short, (2) having every transaction acquire locks in the
same order, and (3) indexing the columns in your
WHERE so you lock few, precise records instead of scanning.
— MySQL Manual: "you must still handle the case where a transaction must be retried"; keep transactions small, "use the same order of operations… create indexes on the columns used."
Because locks sit on the index records a query walks, an UPDATE … WHERE unindexed_col
= ? must scan — and lock — every row it examines, not just the ones that
match. A missing index doesn't just make reads slow (0004); it makes writes lock the whole
table's worth of rows, turning a fast update into a concurrency bottleneck and a deadlock
magnet.
Answer from memory — effortful recall is what builds retention. Feedback is immediate.
A record lock in InnoDB locks:
A gap lock exists specifically to:
A next-key lock is a combination of:
In REPEATABLE READ, next-key locks exist to prevent:
When InnoDB detects a deadlock, it:
A plain (nonlocking) SELECT in InnoDB takes:
An UPDATE … WHERE on an unindexed column tends to lock:
Open two mysql sessions (A and B) on the people table
(PK id). You'll make each lock one row, then reach for the other's.
-- Session A
START TRANSACTION;
UPDATE people SET city = 'A1' WHERE id = 1; -- A holds X lock on row 1
-- Session B
START TRANSACTION;
UPDATE people SET city = 'B2' WHERE id = 2; -- B holds X lock on row 2
-- Session A — now wants row 2 (B has it) → A waits
UPDATE people SET city = 'A2' WHERE id = 2;
-- Session B — now wants row 1 (A has it) → CYCLE
UPDATE people SET city = 'B1' WHERE id = 1;
The instant B issues its last statement, InnoDB detects the cycle and one session gets
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction.
The victim is rolled back; the survivor can COMMIT. That error is your cue to
retry, not to panic.
SHOW ENGINE INNODB STATUS\G
Find the LATEST DETECTED DEADLOCK section. It names both
transactions, the exact SQL each was running, and which locks each held vs waited
for — the whole crime scene. Being able to read this is a senior-level skill; bring me a real
one and we'll walk it together.
— MySQL Manual: "To view the last deadlock… use SHOW ENGINE INNODB STATUS."
MySQL 8.0 Reference Manual — InnoDB Locking (record / gap / next-key / S / X), then Deadlocks in InnoDB for detection, the victim, and avoidance.