Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: Two timestamps on every item · Optimistic control in three phases · Optimistic locking with a version column · Exercise. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

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

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, with both timestamps after every step Python · timestamp_ordering.py
"""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

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):

Optimistic control: a read phase into a private copy, then validation, then either the write phase or an abort and restart.Read phasereads the database,writes a private copyValidationdid anyone commita write to whatit read?Write phasecopies its writesinto the databaseAbortdrops the copyand restartsnoyesagain

The three phases of optimistic concurrency control

Text description of the diagram

The figure reads from top to bottom.

  1. Read phase: the transaction reads from the database and writes only into a private copy.
  2. Validation: at commit, it checks whether any transaction committed a write to something it read while it was running.
  3. If there was no conflict, the write phase copies the private writes into the database, and the transaction commits.
  4. 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()):

Two app servers, one row and a version column SQL · version_column.sql
-- 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

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 timestamp ts reads 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" when thomas is 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.

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.

  1. Question 1 of 5 Item A has R-TS 9 and W-TS 4. The transaction with timestamp 7 writes A. What happens under basic timestamp ordering?

    Choose one answer.

    Show the answer to question 1

    Answer: T7 aborts, because a younger transaction, T9, has already read A

    In the timestamp order T7 comes before T9, so T9 should have read T7's value. T9 has already read the older value, and nothing can repair that, so T7 aborts and restarts with a new timestamp.

  2. Question 2 of 5 Item B has R-TS 3 and W-TS 8. The transaction with timestamp 6 writes B under Thomas's write rule. What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The write is skipped; both timestamps stay as they are

    No younger transaction has read B (R-TS is 3), but a younger one, T8, has written it. In the timestamp order, T8's write would overwrite T6's anyway, so Thomas's rule skips T6's write instead of aborting T6.

  3. Question 3 of 5 Why can basic timestamp ordering never deadlock?

    Choose one answer.

    Show the answer to question 3

    Answer: No transaction ever waits; an operation either goes ahead or aborts its transaction

    A deadlock needs transactions that wait for each other. The basic rules never make anything wait, so the wait-for graph is always empty. The cost is moved into restarts.

  4. Question 4 of 5 In the version-column example, why did B's first UPDATE change no row?

    Choose one answer.

    Show the answer to question 4

    Answer: Its WHERE clause asked for version 1, and A's update had already moved the row to version 2

    The update names the version its writer read. A changed the row first and incremented the version, so B's condition matched nothing. Zero rows changed is the signal to read the row again and recompute.

  5. Question 5 of 5 Which of these find a conflict only when the transaction or the update tries to commit its change? Choose every one.

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • Optimistic control with validation
    • An update with a version column in its WHERE clause

    Optimistic control checks for conflicts in its validation step at commit, and a version-column update checks when the write is attempted. Timestamp ordering checks every read and write as it happens, and two-phase locking checks every lock request.

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.

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.

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

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.