In PostgreSQL Read Committed, an ordinary table read uses a snapshot established for that statement. Another transaction can commit while the read is running, yet the table scan continues using its original visibility boundary. The next statement can see the new commit. This explains how one transaction can obtain individually consistent answers that differ between statements, without either statement observing an uncommitted intermediate update.

Name the relevant boundary

Read Committed is PostgreSQL’s default isolation level, but roles, databases, or sessions can change the configured default. An unqualified BEGIN therefore does not prove which level an application uses. The experiment below chooses Read Committed explicitly. Repeatable Read instead reuses a transaction snapshot established by the first non-transaction-control statement; simply beginning a transaction is not equivalent to taking that snapshot.

A snapshot governs visibility of other transactions’ changes. Previous writes made by the reading transaction itself remain visible to its later statements. Snapshot consistency consequently does not mean every value is frozen regardless of its origin, nor does it promise that data returned earlier will still be current when an application later acts on it.

When it breaks

Two Read Committed queries can observe different committed populations. Computing a count, waiting, and then fetching the corresponding rows can therefore produce a mismatch if other transactions change the data between statements. Combine the required observation into one suitable query, or choose an appropriate transaction-level reading contract when several statements must share a view.

Do not extend the ordinary-read rule to every write or locking read. At Read Committed, an update can wait for a concurrent updater and then re-evaluate its condition against the updated row. SELECT FOR UPDATE also has locking behavior. A stable earlier read does not reserve that row or establish a cross-row business invariant. Isolation, atomic updates, and explicit coordination answer different concurrency questions.

Check it quickly

Use two fresh psql -X -v ON_ERROR_STOP=1 sessions, A and B, with the same role in a disposable PostgreSQL 18 database. Keep normal autocommit outside explicit blocks. The schema and advisory-lock key are reserved for this experiment. The advisory lock is only a synchronization gate; it does not protect the table row being changed.

  1. In B, create the initial value and hold the session-level gate.
CREATE SCHEMA statement_snapshot_demo;
CREATE TABLE statement_snapshot_demo.item (id integer PRIMARY KEY,value integer);
INSERT INTO statement_snapshot_demo.item VALUES (1,100);
SELECT pg_advisory_lock(8610,1);
  1. In A, start a Read Committed statement that waits on that gate after its statement snapshot has been established. Leave it running while operating B.
SET application_name='xbcatm_snapshot_reader';
SET statement_timeout='60s';
BEGIN ISOLATION LEVEL READ COMMITTED;
WITH gate AS MATERIALIZED (SELECT pg_advisory_xact_lock(8610,1))
SELECT value FROM statement_snapshot_demo.item CROSS JOIN gate WHERE id=1;
  1. In B, inspect A. Continue only after the result shows Lock and advisory, proving that the statement is actually in progress rather than relying on a timed guess.
SELECT wait_event_type,wait_event FROM pg_stat_activity
WHERE application_name='xbcatm_snapshot_reader' AND state='active';
  1. Still in B, commit the new value through autocommit, then release the gate. Return to A to collect its result.
UPDATE statement_snapshot_demo.item SET value=200 WHERE id=1;
SELECT pg_advisory_unlock(8610,1);
  1. In A, issue a new read and finish its transaction. Finally, remove the fixture in B.
SELECT value FROM statement_snapshot_demo.item WHERE id=1;
COMMIT;
DROP SCHEMA statement_snapshot_demo CASCADE;

PostgreSQL 18.6 showed A waiting on the advisory gate. B updated the stored value to 200 before releasing it; the unlock returned true. Nevertheless, A’s already-running statement returned 100. Its next statement, inside the same explicit transaction, returned 200. The gate made the ordering observable without depending on how quickly either operator copied a command.

A holds its transaction-level advisory lock until commit, so finish A before cleanup. Its sixty-second statement timeout bounds a forgotten gate. If that timeout fires first, roll back A, release B’s gate, and remove the fixture before starting again. A canceled statement provides no successful snapshot observation.

Use the result for the right decision

The experiment demonstrates the table scan’s statement boundary, not permission to reuse stale application decisions. A Repeatable Read transaction can keep earlier results stable, but merely reading again inside that same snapshot does not discover a newly committed change. Writing transactions at stronger isolation levels may receive serialization failures and must recompute the complete operation when retrying.

For a report, decide whether one consistent statement or several statements sharing a snapshot match the intended output. For a state transition, decide what must remain true when the write commits and choose the corresponding concurrency protocol. Consistent reading and correct concurrent decisions are connected, but one does not automatically provide the other.

Sources