Question presented to candidate: "In PostgreSQL specifically, walk me through READ COMMITTED, REPEATABLE READ and SERIALIZABLE. For each one, which anomalies can still happen, what does the database do when two transactions collide, and what does my application have to handle?"
What a strong answer should cover:
- PostgreSQL implements three distinct levels; READ UNCOMMITTED behaves as READ COMMITTED, so dirty reads never happen
- READ COMMITTED (default): a new snapshot per statement; non-repeatable reads, phantoms, lost updates in read-modify-write code and write skew are possible; a blocked
UPDATEwaits, then re-checks itsWHEREon the newest row version - REPEATABLE READ: one snapshot taken at the first statement (not at
BEGIN); no non-repeatable reads and, stricter than the standard, no phantoms; a concurrent update of the same row aborts with SQLSTATE 40001; write skew still possible - SERIALIZABLE: Serializable Snapshot Isolation (SSI, since 9.1) tracks read/write dependencies with SIRead predicate locks and aborts one transaction of a dangerous pattern with 40001, so write skew is prevented; reads never block
- Application duties: retry the whole transaction on 40001, keep serializable transactions short, use
READ ONLY DEFERRABLEfor long reports, or use explicit locks (FOR UPDATE) at READ COMMITTED instead
Clarifying questions expected:
- Which invariant are we protecting — one row (counter, balance) or a rule across several rows (on-call roster, no overlapping bookings)?
- Can the application retry a transaction safely (no external side effects inside it)?
Code / implementation expected: Optional — a two-session timeline for write skew, and the fix with SERIALIZABLE plus retry or with FOR UPDATE.
Schema used by the examples: