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 11 – Transactions and schedules

Isolation levels: dirty reads to write skew

Dirty writes, dirty reads, lost updates, non-repeatable reads, phantoms, read skew and write skew, the four SQL levels, and what each database engine allows.

  • Intermediate
  • 35 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 35 minutes free
Free: Seven anomalies, one schedule each · The four standard levels · A lost update on a real database file · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 24 minutes.

What you will learn

  • Define dirty write, dirty read, lost update, non-repeatable read, phantom, read skew and write skew
  • Explain the ANSI isolation levels and why the 1995 critique found them ambiguous
  • Predict what PostgreSQL, MySQL InnoDB and SQLite actually allow at each level

Before you start

An isolation level is a promise about which anomalies may appear when transactions run at the same time. The SQL standard defines four levels, READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ and SERIALIZABLE, by the phenomena each one forbids: dirty reads, non-repeatable reads and phantoms. Only SERIALIZABLE promises that the result matches some order of running the transactions one at a time. The weaker levels let through anomalies that the standard’s definitions do not even name, among them lost updates, read skew and write skew, and engines fill the gaps differently. PostgreSQL starts at Read Committed, MySQL’s InnoDB at Repeatable Read, and SQLite makes every transaction serializable by letting one writer in at a time. This lesson defines each anomaly with the schedule that shows it, and loses an update on a real database file.

Seven anomalies, one schedule each

  • Dirty write. T2 overwrites a value that T1 has not committed yet: w1(x) w2(x) …
  • Dirty read. T2 reads a value that T1 has not committed, which may still be rolled back: w1(x) r2(x) …
  • Lost update. Both read x, both write a value computed from it, and one write silently replaces the other: r1(x) r2(x) w1(x) w2(x).
  • Non-repeatable read. T1 reads x twice and gets two committed values, because T2 changed x in between: r1(x) w2(x) c2 r1(x).
  • Phantom. T1 runs a search twice and gets different rows, because T2 inserted or deleted a match: r1(P) w2(y in P) c2 r1(P).
  • Read skew. T1 reads x before T2 changes x and y, and y after, so it sees a state that never existed: r1(x) w2(x) w2(y) c2 r1(y).
  • Write skew. Both read x and y, each changes a different one, and together they break a rule each one checked: r1(x) r1(y) r2(x) r2(y) w1(x) w2(y).

The first two are about uncommitted data, the subject of the previous lesson. The next three are about reading the same thing twice, or reading and then writing it. The last two involve two items, which is why they are easy to miss: every single row looks fine, and only the combination is wrong.

The four standard levels

  • READ UNCOMMITTED: dirty reads, non-repeatable reads and phantoms are all possible.
  • READ COMMITTED: no dirty reads; non-repeatable reads and phantoms are possible.
  • REPEATABLE READ: no dirty reads and no non-repeatable reads; phantoms are possible.
  • SERIALIZABLE: none of the three, and the result must match some serial order.

These are the minimum each level must forbid; an engine may forbid more. PostgreSQL, for example, lets you ask for READ UNCOMMITTED but runs it as READ COMMITTED, and its REPEATABLE READ does not allow phantoms either. The table also says nothing about lost updates, read skew or write skew, which is the gap the 1995 critique of the standard pointed out.

A lost update on a real database file

Two sessions add money to one wallet: A adds 50 and B adds 20, starting from 100, so the balance should end at 170. The program runs them on a SQLite file three ways:

A wallet, two sessions, three ways to update it Python · lost_update.py
"""A lost update, and two ways to avoid it, on a real SQLite database file.

Two sessions, A and B, each add money to the same wallet: A adds 50 and B adds 20,
starting from 100. Done one after the other, the balance ends at 170.
"""
import os
import sqlite3

DB = "wallet.db"


def connect():
    return sqlite3.connect(DB, autocommit=True, timeout=0)


def balance(con):
    return con.execute("SELECT balance FROM wallet WHERE id = 1").fetchone()[0]


def fresh():
    for name in (DB, DB + "-wal", DB + "-shm", DB + "-journal"):
        if os.path.exists(name):
            os.remove(name)
    con = connect()
    con.execute("PRAGMA journal_mode = WAL")
    con.execute("CREATE TABLE wallet (id INTEGER PRIMARY KEY, balance INTEGER NOT NULL)")
    con.execute("INSERT INTO wallet VALUES (1, 100)")
    return con


print("1. Read, then write the new number: every statement on its own (autocommit)")
fresh()
a, b = connect(), connect()
seen_a, seen_b = balance(a), balance(b)
print(f"   A reads {seen_a}, B reads {seen_b}")
a.execute("UPDATE wallet SET balance = ? WHERE id = 1", (seen_a + 50,))
b.execute("UPDATE wallet SET balance = ? WHERE id = 1", (seen_b + 20,))
print(f"   A writes {seen_a + 50}, B writes {seen_b + 20}; the balance is now {balance(a)}: A's 50 is lost")

print("2. The same, but each session reads and writes inside one transaction")
fresh()
a, b = connect(), connect()
a.execute("BEGIN")
b.execute("BEGIN")
seen_a, seen_b = balance(a), balance(b)
print(f"   A reads {seen_a}, B reads {seen_b}")
a.execute("UPDATE wallet SET balance = ? WHERE id = 1", (seen_a + 50,))
a.execute("COMMIT")
print(f"   A writes {seen_a + 50} and commits")
try:
    b.execute("UPDATE wallet SET balance = ? WHERE id = 1", (seen_b + 20,))
except sqlite3.OperationalError as e:
    print(f"   B's write is refused: {e} ({e.sqlite_errorname})")
    b.execute("ROLLBACK")
    b.execute("BEGIN IMMEDIATE")
    seen_b = balance(b)
    b.execute("UPDATE wallet SET balance = ? WHERE id = 1", (seen_b + 20,))
    b.execute("COMMIT")
    print(f"   B starts again, reads {seen_b}, writes {seen_b + 20}; the balance is now {balance(a)}")

print("3. Let the database do the arithmetic: UPDATE ... SET balance = balance + n")
fresh()
a, b = connect(), connect()
a.execute("UPDATE wallet SET balance = balance + 50 WHERE id = 1")
b.execute("UPDATE wallet SET balance = balance + 20 WHERE id = 1")
print(f"   the balance is now {balance(a)}")

Output

1. Read, then write the new number: every statement on its own (autocommit)
   A reads 100, B reads 100
   A writes 150, B writes 120; the balance is now 120: A's 50 is lost
2. The same, but each session reads and writes inside one transaction
   A reads 100, B reads 100
   A writes 150 and commits
   B's write is refused: database is locked (SQLITE_BUSY_SNAPSHOT)
   B starts again, reads 150, writes 170; the balance is now 170
3. Let the database do the arithmetic: UPDATE ... SET balance = balance + n
   the balance is now 170

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lost_update.py

Timeline: A and B both read a balance of 100; A writes 150, then B writes 120, which overwrites A and loses its 50.Session Aadds 50WalletbalanceSession Badds 20reads 100reads 100writes 150writes 120150 is overwritten:A's 50 is lost

A lost update, step by step

Text description of the diagram

The figure is a timeline with three columns: session A, which adds 50; the wallet balance; and session B, which adds 20. Time runs downwards.

  1. A reads the balance: 100.
  2. B reads the balance: 100.
  3. A writes 150, its own reading plus 50.
  4. B writes 120, its own reading plus 20.
  5. B's write replaces A's, so the balance ends at 120 instead of 170, and A's 50 is lost.

In the first run, each session reads the balance and then writes the number it computed, with every statement in its own transaction. B’s write replaces A’s, and A’s 50 disappears without any error. In the second run, each session reads and writes inside one transaction. SQLite refuses B’s write: B’s view of the database is older than A’s commit, so writing would overwrite a change B never saw. B starts again, reads 150, and the balance reaches 170. In the third run, the database does the arithmetic with balance = balance + n, so there is nothing stale to write.

Other engines treat the second case differently. On PostgreSQL at Read Committed, the second run would not be refused: B’s UPDATE would store 120, because a number computed in the application is all the database sees. The third run is safe there too: the second UPDATE ... SET balance = balance + 20 waits for the first one to commit and then applies to the newest version of the row.

Check yourself

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 Which anomaly does r1(x) r2(x) w1(x) w2(x) c1 c2 show, if each write is computed from its own read?

    Choose one answer.

    Show the answer to question 1

    Answer: A lost update

    Both transactions read the same committed value and each writes a new value computed from it, so T2's write replaces T1's as if T1 had never run. Nobody read uncommitted data, so it is not a dirty read.

  2. Question 2 of 5 What is the weakest standard isolation level that forbids non-repeatable reads?

    Choose one answer.

    Show the answer to question 2

    Answer: REPEATABLE READ

    READ COMMITTED forbids only dirty reads. REPEATABLE READ also forbids non-repeatable reads, and SERIALIZABLE forbids phantoms as well.

  3. Question 3 of 5 In the second run of the wallet example, why does SQLite refuse B's UPDATE after A has committed?

    Choose one answer.

    Show the answer to question 3

    Answer: B's transaction reads an older version of the database than the one it would write over

    B began reading before A committed, so its view of the database is out of date. Letting it write would mean writing over a change it never saw, so SQLite refuses with SQLITE_BUSY_SNAPSHOT, and B has to start again.

  4. Question 4 of 5 On PostgreSQL at Read Committed, A runs UPDATE wallet SET balance = balance + 50 and B runs UPDATE wallet SET balance = balance + 20 at the same time on a balance of 100. Both commit. What is the final balance?

    Type a number.

    Show the answer to question 4

    Answer: 170

    The second UPDATE waits for the first to commit and then applies to the newest version of the row, so both additions count: 100 + 50 + 20 = 170.

  5. Question 5 of 5 Replay the first wallet run with the writes the other way round: both sessions read 100, then B writes its number and A writes its number last. What is the final balance?

    Type a number.

    Show the answer to question 5

    Answer: 150

    A computed 100 + 50 from its stale read and writes last, so B's 20 is the one lost this time. In the other two runs the order does not matter: either the stale writer is refused and starts again, or the database adds to the newest value, so both give 170.

What PostgreSQL, InnoDB and SQLite allow at each level Pro: opens with a Pro, Premium or Ultimate pass

The standard says what each level must forbid; engines decide what they allow. This part replays four interleavings under PostgreSQL’s and InnoDB’s documented rules, explains each outcome from the manuals, and runs SQLite against the same cases.

In this part:

  • Four interleavings under six engine and level pairs, replayed
  • PostgreSQL: one snapshot per statement, or one per transaction
  • InnoDB: reads from a snapshot, writes to the newest version
  • SQLite: one writer at a time, measured on the same cases

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

Why the standard's definitions were called ambiguous in 1995 Pro: opens with a Pro, Premium or Ultimate pass

In 1995 six authors from industry and academia showed that the standard’s three phenomena can be read in two ways, and that the strict reading lets histories that are not serializable through. This part replays their argument on three histories, adds the phenomena they found missing, and places snapshot isolation among the levels.

In this part:

  • Strict and broad readings, separated by three histories
  • The phenomena the standard left out, worked pattern by pattern
  • Snapshot isolation between the levels

1 runnable example (Python) · about 2 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).

Write skew reproduced, and five ways to handle it Pro: opens with a Pro, Premium or Ultimate pass

Write skew is the anomaly that survives snapshot isolation, and it breaks rules that span rows: a combined limit on two accounts, a slot that may be booked once. This part replays both cases under five strategies, compares what each costs, and says which one to pick.

In this part:

  • Two write skews, five strategies, replayed
  • What each strategy costs: the trade-offs
  • A capacity rule, worked: at most two students per hostel room

1 runnable example (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 · Warm-up

    What is the difference between a non-repeatable read and a phantom?

    A non-repeatable read is about a row you have already read: you read it again in the same transaction and get a different committed value, because another transaction updated or deleted it in between. A phantom is about a search: you run the same query again and get a different set of rows, because another transaction inserted or deleted rows that match the condition. Row locks or a fixed snapshot of the rows you read stop non-repeatable reads, but a phantom row did not exist when you read, so stopping phantoms needs something that covers the search itself, such as a snapshot of the whole database, a predicate lock or a lock on an index range.

  2. Fresher to senior

    A wallet service on PostgreSQL at Read Committed reads the balance, adds the top-up in application code and writes the new balance back. During a sale, support finds top-ups missing, and the logs show no errors. Explain the cause, reproduce it, and give two fixes. What changes on MySQL?

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

  3. Mid to senior

    A college allots hostel beds with the rule "at most two students per room", checked by counting a room's allotments before inserting a new one. The service runs at Repeatable Read, and one room ended up with three students. Why, and which fix would you choose?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A nightly report sums every account while transfers keep running. Which isolation level would you choose on PostgreSQL and on MySQL, and why not simply run every transaction at SERIALIZABLE?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The standard defines its four levels by three phenomena: dirty reads, non-repeatable reads and phantoms; only SERIALIZABLE promises a serial result.
  • Lost updates, read skew and write skew are not in the standard’s table, and weaker levels may allow them.
  • A read followed by a write of a number computed in the application can lose an update; let the database do the arithmetic, or lock what you read.
  • Engines differ: PostgreSQL starts at Read Committed, InnoDB at Repeatable Read, and SQLite serializes writers.

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.