skip to content

A transaction that updates a row and then re-reads it sees its own uncommitted change, even though no other transaction can. Beyond transaction-id stamps and the in-flight list, what extra bookkeeping does the engine need so that a single statement does not repeatedly process rows it has just written itself?

level: seniorimportance: nice to knowfreq 28%

answer

  1. self exception: my own txid counts as visible
  2. txid too coarse — same id for all my statements
  3. command/statement sequence number per version
  4. strictly-earlier command is visible, current command is not
  5. Halloween problem: updated row re-qualifies and loops

basics

~20 s

Own writes are visible because the visibility test treats the reader's own transaction id as visible. To keep one statement from re-processing its own output, engines also stamp each version with a statement/command sequence number and hide versions created by the current or later commands of the same transaction.

solid answer

~60 s

Visibility has a special case for self: a version whose creator is *my own* transaction id counts as visible to me, and one whose expirer is my own id counts as deleted for me — that is why `UPDATE` followed by `SELECT` inside one transaction shows the new value while everyone else still sees the old one. Transaction ids alone are too coarse, though. Within one transaction every statement carries the same id, so a statement that writes rows it is also scanning could see its own freshly written versions and process them again — the classic **Halloween problem**, where an update like "raise every salary below a threshold by 10%" loops because updated rows keep re-qualifying. Engines therefore add a **command/statement sequence number** stamped alongside the transaction id. The rule becomes: my own version is visible only if the command that created it is *strictly earlier* than the currently executing command. A statement sees everything its transaction did before it started and nothing it is doing now — so each row is processed once. Prior statements' effects remain fully visible, which is what makes multi-statement transactions behave intuitively.

code

sql · 10 lines
sql
-- rows updated by this statement must not be re-seen by the same statement
UPDATE employees
SET salary = salary * 1.1
WHERE salary < 50000;

-- earlier statements of the same transaction ARE visible:
BEGIN;
INSERT INTO employees(id, salary) VALUES (9, 40000);
SELECT salary FROM employees WHERE id = 9;  -- returns 40000 to me only
COMMIT;

go deeper

for a junior

Know the plain fact: my transaction sees its own uncommitted changes; nobody else does until commit.

for a middle

Explain the self exception in the visibility rule and that statements within a transaction see the effects of earlier statements.

for a senior

Name the command sequence number and the strictly-earlier rule, and explain the Halloween problem it prevents with a concrete looping-update example.

for a principal

Extend to observable consequences — trigger and data-modifying-CTE visibility, cursor stability, and the guarantee that a statement operates on a fixed row set — and why this bookkeeping never needs to outlive the transaction.

## Why self-visibility is a special case The general visibility rule rejects versions created by transactions that were in flight at snapshot time. Your own transaction is, by definition, in flight — so a literal application of the rule would make a transaction unable to see its own writes. Every engine therefore carves out an explicit exception: if a version's creator id equals my own transaction id, treat creation as visible; if the expirer id equals my own id, treat the row as deleted for me. This is what makes the ordinary programming model work — insert a row, then query it back; update a row, then read the new value — while other transactions continue to see the pre-change state until commit. ## Where transaction ids alone break down The transaction id is uniform across the whole transaction, so it cannot distinguish "written by an earlier statement of mine" from "written by the statement currently running". That distinction matters, because a statement can read and write the same rows. The canonical failure is the **Halloween problem**, named after the day it was found. Consider `UPDATE employees SET salary = salary * 1.1 WHERE salary < 50000` executed via an index on `salary`. The statement updates a row from 45,000 to 49,500 and produces a new version. If the scan then encounters that new version and self-visibility says "created by me, therefore visible", the row still satisfies `salary < 50000` and gets raised again — and again — until it crosses the threshold or the statement never terminates. The rows read must be the rows as of the statement's start, not as the statement is mutating them. The same hazard appears in an `INSERT ... SELECT` where source and target are the same table, and in delete-and-scan patterns. ## The command sequence number The fix is a finer-grained stamp: alongside the transaction id, each version records the **command (statement) sequence number** within the transaction that produced it. PostgreSQL calls this the command id (`cmin`/`cmax`); other engines implement the same idea through statement-scoped read views or scan-position rules. The self-visibility clause becomes: > a version created by my own transaction is visible to me only if its command number is **strictly less than** the command number of the statement currently executing. Consequences: - Statement 3 sees everything statements 1 and 2 did — multi-statement transactions read intuitively. - Statement 3 does **not** see rows statement 3 itself is writing, so each qualifying row is processed exactly once and the Halloween problem cannot arise. - Deletes are symmetric: a row expired by an earlier command of mine is gone for me; a row my current command just expired is still considered present by that same command's scan, which is precisely what keeps the scan stable. ## Related consequences worth knowing - **Trigger and constraint timing.** Code running inside a statement — row triggers, check expressions, functions in the statement — executes at the same command number and so inherits the same partial view. This is why a `BEFORE`/row-level trigger that queries the table it is modifying can see a surprising, statement-frozen view, and why an `AFTER` statement-level trigger sees the completed effects. - **Data-modifying CTEs.** In engines where a single statement contains several modifying subqueries, all of them share one command number, so none sees another's output within that statement. The result is a genuine source of confusion and a good interview follow-up. - **Cursors.** A cursor opened at one command number keeps that view, which is why rows fetched through it can be stale relative to writes the same transaction performed afterwards, unless the cursor was declared to be re-evaluated. - **Storage cost.** The command number only matters while the transaction is open — no other transaction ever needs it — so engines can reuse the same header space for other purposes once the transaction ends, rather than carrying it forever. ## What a strong answer sounds like Start with the plain fact — own writes are visible because the rule has a self exception. Then show you know the exception alone is insufficient, name the Halloween problem with a concrete looping-update example, and describe the command sequence number and the strictly-earlier rule that resolves it. Mentioning triggers or data-modifying CTEs as places where the statement-level view is observable turns a recited answer into a demonstrated one.

  • What is the Halloween problem, and how does command-level visibility prevent it?
    It is a statement re-processing rows it has itself just modified, so those rows keep re-qualifying for the statement's predicate and are updated repeatedly — a percentage raise applied through an index on the updated column is the classic case. Command-level visibility makes versions created by the currently executing command invisible to that same command's scan, so each qualifying row is seen and updated exactly once.
  • Inside one transaction, statement 1 inserts a row and statement 2 selects it. Statement 3 is a single statement that both inserts and selects from the same table. What does each see?
    Statement 2 sees the row from statement 1, because that version's command number is strictly earlier than statement 2's. Within statement 3, the select portion does not see rows the same statement inserts, because they share one command number and only strictly-earlier commands are visible. That is why data-modifying subqueries in one statement do not observe each other's output.
  • Do other transactions ever need the command sequence number?
    No — for another transaction the only question is whether my transaction committed, which is decided by my transaction id and commit status; my internal statement ordering is irrelevant to it. That is why engines can treat the command number as transient bookkeeping valid only while the transaction is open and reuse the header space afterwards.

saying these in an interview costs you the question

  • Saying a transaction cannot see its own uncommitted changes until commit.
  • Claiming other transactions can see uncommitted changes at any isolation level in a multi-version engine.
  • Asserting the transaction id alone is enough to order visibility within a transaction.
  • Believing a single statement's write side and read side observe each other's output.

context