Database Management Systems (DBMS) Module 12 – Concurrency control
Timestamp ordering and optimistic control
The read and write rules of timestamp ordering, what Thomas's write rule adds, how validation works in optimistic control, and version columns in SQL.
What you will learn
- Apply the basic timestamp-ordering read and write rules
- Apply Thomas's write rule and explain which schedules it admits
- Describe validation-based (optimistic) concurrency control and application-level optimistic locking
Before you start
What you’ll learn · Before you start
What you will learn
- Apply the basic timestamp-ordering read and write rules
- Apply Thomas's write rule and explain which schedules it admits
- Describe validation-based (optimistic) concurrency control and application-level optimistic locking
Before you start
On this page
Locking makes a transaction wait when it meets a conflict. Timestamp ordering and optimistic control never make it wait; they abort it instead. Timestamp ordering gives every transaction a timestamp when it starts and keeps the schedule equivalent to the serial order of those timestamps. Each item remembers the largest timestamps that have read and written it, and an operation that arrives too late, after a younger transaction has used the item in a conflicting way, aborts its transaction, which restarts with a new timestamp. Optimistic control lets a transaction run without any checks, keeping its writes in a private copy, and validates at commit that no conflicting transaction got in its way; a transaction that fails validation restarts. Applications use the same idea with a version column on each row.
Two timestamps on every item
Each item Q carries R-TS(Q), the largest timestamp of any transaction that has read it, and W-TS(Q), the timestamp of the transaction that wrote it last. A larger timestamp means a younger transaction. For a transaction T:
| T’s step, and when | Result |
|---|---|
| Read Q after a younger transaction wrote Q: TS(T) is below W-TS(Q) | T aborts |
| Read Q otherwise | The read goes ahead; R-TS(Q) becomes the larger of R-TS(Q) and TS(T) |
| Write Q after a younger transaction read Q: TS(T) is below R-TS(Q) | T aborts |
| Write Q after a younger transaction wrote Q: TS(T) is below W-TS(Q) | T aborts, or under Thomas’s write rule the write is skipped |
| Write Q otherwise | The write goes ahead; W-TS(Q) becomes TS(T) |
The simulator applies the rules to one schedule. R5(A) means the transaction with timestamp 5 reads A:
"""Basic timestamp ordering, step by step. Every transaction gets a timestamp when it starts; every item keeps
R-TS, the largest timestamp that has read it, and W-TS, the timestamp of its last write. An operation that
arrives too late aborts its transaction. Set THOMAS = True to apply Thomas's write rule instead.
Notation: R5(A) means the transaction with timestamp 5 reads A, W7(B) that the one with timestamp 7 writes B.
"""
import re
SCHEDULE = "R5(A) R7(A) W7(A) R5(B) W5(A) R9(B) W7(B) W9(B)"
THOMAS = False
def run(schedule, thomas):
rts, wts, aborted = {}, {}, set()
for token in schedule.split():
op, ts, item = re.fullmatch(r"([RW])(\d+)\((\w+)\)", token).groups()
ts = int(ts)
if ts in aborted:
print(f" {token:6} skipped: T{ts} was aborted")
continue
r, w = rts.get(item, 0), wts.get(item, 0)
if op == "R":
if ts < w:
aborted.add(ts)
verdict = f"abort T{ts}: a younger transaction (T{w}) already wrote {item}"
else:
rts[item] = max(r, ts)
verdict = "read"
elif ts < r:
aborted.add(ts)
verdict = f"abort T{ts}: a younger transaction (T{r}) already read {item}"
elif ts < w and thomas:
verdict = f"ignored: T{w} already wrote a newer value of {item} (Thomas's write rule)"
elif ts < w:
aborted.add(ts)
verdict = f"abort T{ts}: a younger transaction (T{w}) already wrote {item}"
else:
wts[item] = ts
verdict = "written"
state = " ".join(f"{i}: R-TS {rts.get(i, 0)}, W-TS {wts.get(i, 0)}" for i in sorted(set(rts) | set(wts)))
print(f" {token:6} {verdict}")
print(f" {state}")
return aborted
print("basic timestamp ordering" + (" with Thomas's write rule" if THOMAS else ""))
gone = run(SCHEDULE, THOMAS)
print("aborted:", ", ".join(f"T{t}" for t in sorted(gone)) or "none", "(each restarts later with a new timestamp)") Output
basic timestamp ordering
R5(A) read
A: R-TS 5, W-TS 0
R7(A) read
A: R-TS 7, W-TS 0
W7(A) written
A: R-TS 7, W-TS 7
R5(B) read
A: R-TS 7, W-TS 7 B: R-TS 5, W-TS 0
W5(A) abort T5: a younger transaction (T7) already read A
A: R-TS 7, W-TS 7 B: R-TS 5, W-TS 0
R9(B) read
A: R-TS 7, W-TS 7 B: R-TS 9, W-TS 0
W7(B) abort T7: a younger transaction (T9) already read B
A: R-TS 7, W-TS 7 B: R-TS 9, W-TS 0
W9(B) written
A: R-TS 7, W-TS 7 B: R-TS 9, W-TS 9
aborted: T5, T7 (each restarts later with a new timestamp)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timestamp_ordering.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
T5 tried to write A after T7, a younger transaction, had read it: in the timestamp order T5 comes first, so T7 should have read T5’s value, and it is too late for that, so T5 aborts. T7’s write of B fails the same way because T9 had already read B. No transaction ever waited, so no deadlock is possible. The price is that both aborted transactions must restart, and with new, larger timestamps, because with their old ones they would fail the same check again.
Thomas’s write rule changes one case: a write that arrives after a younger transaction’s write is skipped instead of aborting the writer, because in the timestamp order it would be overwritten anyway (Thomas: A Majority Consensus Approach). It keeps some transactions alive that basic timestamp ordering would abort. Strict timestamp ordering makes a transaction wait to read or overwrite a value until its writer has committed or aborted, which keeps schedules recoverable. Multiversion timestamp ordering keeps old versions, so that a late read finds the value it should see instead of aborting.
Optimistic control in three phases
Kung and Robinson split every transaction into three phases (On Optimistic Methods for Concurrency Control):
The three phases of optimistic concurrency control
Text description of the diagram
The figure reads from top to bottom.
- Read phase: the transaction reads from the database and writes only into a private copy.
- Validation: at commit, it checks whether any transaction committed a write to something it read while it was running.
- If there was no conflict, the write phase copies the private writes into the database, and the transaction commits.
- If there was a conflict, the transaction aborts, throws its private copy away and starts again at the read phase.
The read phase does all the work, reading from the database and writing only into a private copy. The validation step at commit checks for conflicts. Backward validation compares a transaction with those that committed while it ran; forward validation compares it with those still running. The write phase copies the private writes into the database. Optimistic control pays off when conflicts are rare, because then almost every transaction passes validation and nobody ever waited for a lock.
Optimistic locking with a version column
Applications use the same idea across requests, where a database transaction cannot stay open. Each row carries a
version number. A client reads the row and its version, works on it, and writes back with an update that names the
version it read and increments it. If somebody changed the row in between, the update matches no row, and the
client must read again. Two app servers sell kettles from one row here; SQLite’s changes() reports how many rows
the last statement changed (SQLite: changes()):
-- Optimistic locking with a version column. App servers A and B both read product 1: stock 10, version 1.
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, stock INTEGER NOT NULL,
version INTEGER NOT NULL);
INSERT INTO products VALUES (1, 'kettle', 10, 1);
-- A sells 3. Its update names the version it read, so it succeeds only if nobody changed the row since.
UPDATE products SET stock = 7, version = version + 1 WHERE id = 1 AND version = 1;
SELECT 'A' AS server, changes() AS rows_updated;
-- B sells 2 from the stock of 10 it read. Version 1 is gone, so its update matches no row.
UPDATE products SET stock = 8, version = version + 1 WHERE id = 1 AND version = 1;
SELECT 'B' AS server, changes() AS rows_updated;
-- Zero rows means a conflict: B reads again (stock 7, version 2) and repeats its update.
UPDATE products SET stock = 5, version = version + 1 WHERE id = 1 AND version = 2;
SELECT 'B, again' AS server, changes() AS rows_updated;
SELECT id, name, stock, version FROM products; Output
┌────────┬──────────────┐ │ server │ rows_updated │ ├────────┼──────────────┤ │ A │ 1 │ └────────┴──────────────┘ ┌────────┬──────────────┐ │ server │ rows_updated │ ├────────┼──────────────┤ │ B │ 0 │ └────────┴──────────────┘ ┌──────────┬──────────────┐ │ server │ rows_updated │ ├──────────┼──────────────┤ │ B, again │ 1 │ └──────────┴──────────────┘ ┌────┬────────┬───────┬─────────┐ │ id │ name │ stock │ version │ ├────┼────────┼───────┼─────────┤ │ 1 │ kettle │ 5 │ 3 │ └────┴────────┴───────┴─────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < version_column.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
B’s first update changed no row, because A had already moved the version from 1 to 2. Without the condition on the
version, B would have written stock 8 over A’s 7, and A’s sale would be lost. HTTP has the same check built in: a
client sends the ETag it read in an If-Match header, and the server answers 412 Precondition Failed instead of
overwriting a newer version (RFC 9110: HTTP Semantics).
| Method, and when it finds a conflict | What the loser does, deadlocks, and fit |
|---|---|
| Two-phase locking: when a lock is requested | The loser waits; deadlocks are possible; suits frequent conflicts |
| Timestamp ordering: at every read and write | The loser restarts with a new timestamp; no deadlocks; suits systems that must never wait |
| Optimistic control: at commit, in validation | The loser redoes the whole transaction; no deadlocks; suits rare conflicts and short transactions |
Exercise
Exercise · Easy · Python
Write the read and write rules of timestamp ordering
Write the two rules of basic timestamp ordering in to_rules.py. Each item is a dictionary with "rts", the largest timestamp of any transaction that has read it, and "wts", the timestamp of the transaction that wrote it last; both start at 0.
read(ts, item): the transaction with timestamptsreads the item. Return"abort"if a younger transaction has already written it; otherwise update"rts"and return"ok".write(ts, item, thomas=False): the transaction writes the item. Return"abort"if a younger transaction has already read it. If a younger transaction has already written it, return"abort", or"ignore"whenthomasis true (Thomas's write rule). Otherwise update"wts"and return"ok".
A larger timestamp means a younger transaction. An ignored or aborted operation changes neither timestamp.
Starter code · to_rules.py
"""The read and write rules of basic timestamp ordering, with Thomas's write rule as an option.
Each item is a dict: "rts" is the largest timestamp that has read it, "wts" the timestamp of its last write.
"""
def read(ts, item):
"""Return "ok" (after updating item["rts"]) or "abort"."""
return "ok"
def write(ts, item, thomas=False):
"""Return "ok" (after updating item["wts"]), "abort", or "ignore" for an obsolete write under Thomas's rule."""
return "ok" The sample tests · test_to_rules.py
import re
from to_rules import read, write
def test_late_read_aborts():
"""A read after a younger transaction's write aborts"""
item = {"rts": 0, "wts": 7}
assert read(5, item) == "abort"
assert item == {"rts": 0, "wts": 7}
def test_read_keeps_the_largest_timestamp():
"""R-TS keeps the largest reader's timestamp"""
item = {"rts": 3, "wts": 2}
assert read(5, item) == "ok" and item["rts"] == 5
assert read(4, item) == "ok" and item["rts"] == 5
def test_write_after_younger_read_aborts():
"""A write after a younger transaction's read aborts, even under Thomas's rule"""
assert write(7, {"rts": 9, "wts": 0}) == "abort"
assert write(7, {"rts": 9, "wts": 0}, thomas=True) == "abort"
def test_obsolete_write():
"""An obsolete write aborts under basic ordering and is ignored under Thomas's rule"""
item = {"rts": 1, "wts": 4}
assert write(2, item) == "abort"
assert write(2, item, thomas=True) == "ignore"
assert item == {"rts": 1, "wts": 4}
def test_whole_schedule():
"""R5(A) R7(A) W7(A) R5(B) W5(A) R9(B) W7(B) W9(B): T5 and T7 abort"""
items = {"A": {"rts": 0, "wts": 0}, "B": {"rts": 0, "wts": 0}}
aborted = set()
for token in "R5(A) R7(A) W7(A) R5(B) W5(A) R9(B) W7(B) W9(B)".split():
op, ts, name = re.fullmatch(r"([RW])(\d+)\((\w)\)", token).groups()
if int(ts) in aborted:
continue
result = read(int(ts), items[name]) if op == "R" else write(int(ts), items[name])
if result == "abort":
aborted.add(int(ts))
assert aborted == {5, 7} A hint
Write each rule as "compare, then act". A read only has to check the write timestamp. A write checks the read timestamp first, because a younger reader has already seen the old value and nothing can fix that; only then does it check the write timestamp, which is the one case where Thomas's rule can skip the write.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Timestamp ordering in depth: Thomas's rule, recoverability and starvation Pro: opens with a Pro, Premium or Ultimate pass
The basic rules guarantee serializability but leave three questions open. This part proves the guarantee, shows the schedules Thomas’s write rule adds, and repairs the two weaknesses of the basic rules: reads of uncommitted values and transactions that restart again and again.
In this part:
- Why the rules give the timestamp order: the proof
- Thomas’s write rule and the schedules it admits
- Reads of uncommitted values, and how to make the protocol recoverable
- A dry run of the three versions on one schedule
- Starvation, and why restarts take a new timestamp
- Multiversion timestamp ordering: reads that never abort
1 runnable example (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Optimistic control in depth: validation tests, contention and lost updates Pro: opens with a Pro, Premium or Ultimate pass
Optimistic control is cheap when conflicts are rare and wasteful when they are not. This part states the exact validation conditions, measures the abort rate as contention grows against an estimate, and reproduces the lost update that a version column prevents, on two real database connections.
In this part:
- Kung and Robinson’s validation conditions
- Abort rate against contention, measured against an estimate
- A lost update reproduced, then caught by a version column
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid
Thomas's write rule lets the database silently skip a transaction's write. Why is that safe, and what kind of schedule does it accept that basic timestamp ordering rejects? Use the smallest example you can.
-
Mid to senior
An inventory service updates stock with optimistic version checks. Normally under 1 % of updates retry, but during a sale on a few products 70 % of attempts fail and latency explodes. Explain the mechanism with numbers, and what you would change for the hot products.
-
Mid to senior
A mobile app edits a user's profile offline and syncs later, and the same profile can be edited on two devices. Design the optimistic concurrency check across the API, and say what the client should do on a conflict.
Key takeaways
- Timestamp ordering keeps the schedule equivalent to the serial order of start timestamps: a read after a younger write aborts, and so does a write after a younger read or write.
- Thomas’s write rule skips a write that a younger write has already made obsolete, instead of aborting.
- Optimistic control runs a transaction against a private copy and validates at commit; it suits rare conflicts.
- A version column, or an ETag with
If-Match, gives applications optimistic locking across requests.
References
- R. H. Thomas: A Majority Consensus Approach to Concurrency Control for Multiple Copy Databases (ACM Transactions on Database Systems)
- H. T. Kung and J. T. Robinson: On Optimistic Methods for Concurrency Control (ACM Transactions on Database Systems)
- P. A. Bernstein, V. Hadzilacos and N. Goodman: Concurrency Control and Recovery in Database Systems (Microsoft Research)
- RFC 9110: HTTP Semantics (If-Match and 412 Precondition Failed) (IETF)
- SQLite: Built-In Scalar SQL Functions (changes) (SQLite)
- Python 3 Documentation: sqlite3, Cursor.rowcount (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress