A reporting transaction starts at 10:00 and reads an account's status as ACTIVE. At 10:01 an administrator changes that status to SUSPENDED and commits. At 10:05 the report reads the account again and still sees ACTIVE. New sessions see SUSPENDED.
The interview question. Explain how a database can return those different results without making the administrator wait for the report. Show what is stored, how a snapshot chooses a row version, and why a long-running reader can create an operational problem. Use PostgreSQL REPEATABLE READ for the reporting transaction, with ordinary row updates and no conflicting DDL.
Think first
A PostgreSQL REPEATABLE READ report starts at 09:00 and sees a subscription as ACTIVE. Another transaction changes it to CANCELLED and commits. At 09:12 the report still sees ACTIVE while a new session sees CANCELLED. The updated table is growing quickly and vacuum cannot reclaim many old tuples. Explain the mechanism, the operational cost and one safe mitigation.
Take a few minutes to form an approach, then compare it with the worked explanation.
Reveal the worked explanation
One logical row can have more than one physical history
An update does not have to overwrite the bytes that every reader is currently examining. PostgreSQL uses multiversion concurrency control, or MVCC. For a normal update it creates a new tuple version and leaves the old version available while transactions might still need to see it.
Think of the account as having two physical versions:
Version v7: status ACTIVE visible before admin commit
Version v8: status SUSPENDED created by admin transaction
The report started with a stable snapshot. Its questions about the account are answered using the versions visible to that snapshot, not simply whichever version was written last. After the administrator commits, the report still has an older view. A newly started transaction can observe the new committed status.
A repeatable-read snapshot continues to see an older row version while a writer commits a newer one, and vacuum waits until the old snapshot no longer needs the older version
PostgreSQL uses transaction visibility metadata and snapshot rules to decide which tuple is visible. The decision also accounts for inserts or updates made by the reader's own transaction. It is not implemented by putting a separate full database copy on disk for every client. PostgreSQL MVCC
Why did the second SELECT still see ACTIVE?
Because we specified REPEATABLE READ. PostgreSQL fixes the transaction's snapshot at its first non-control statement, so subsequent ordinary reads use a consistent view of commits that preceded that snapshot. That can be valuable for a report that must not mix different moments of the database inside one computation.
The default READ COMMITTED behaves differently. Each ordinary SELECT gets a snapshot as of that statement's start. Two SELECT statements in one transaction can therefore return different committed values. Saying "MVCC means all reads in a transaction see the same version" would be false without naming the isolation level. PostgreSQL transaction isolation
The advantage is that ordinary readers usually do not block ordinary row updates, and those updates do not force all earlier readers to abandon their snapshots. But do not turn that into "MVCC removes locks". Two writers competing for the same row still coordinate. SELECT FOR UPDATE deliberately acquires locks. Schema operations, unique constraints, and other database operations may also block. MVCC changes how many reads work, not whether coordination ever exists.
The old version cannot disappear immediately
Imagine the writer commits SUSPENDED and the database promptly removes v7. The reporting transaction still needs v7 to give the answer promised by its snapshot. Deleting it too early would make the stable snapshot impossible to serve.
That means update-heavy tables can contain dead or obsolete tuples that newer snapshots no longer need but that an older snapshot may still require. PostgreSQL's normal VACUUM reclaims row versions only when they are safe to remove. It usually makes space reusable inside the table rather than returning the entire freed space directly to the operating system. PostgreSQL routine vacuuming
Consider a dashboard service that begins a transaction, runs one query, and then waits fifteen minutes for a downstream HTTP call before committing. If that transaction retains a snapshot that needs old versions, garbage collection cannot reclaim some of the update history while it remains open. A series of such sessions can contribute to table and index bloat and more maintenance work.
Not every idle transaction pins exactly the same horizon. The isolation mode, actual snapshot state, replication slots, and other retention obligations matter. The useful rule is to observe the oldest relevant transaction or snapshot horizon rather than declaring every long-lived connection equally harmful.
What would you measure?
Start with the age of active transactions and their backend states. PostgreSQL exposes session and transaction information through pg_stat_activity, including transactions left idle in transaction. Look at table-level dead-tuple estimates and vacuum history, then check whether maintenance is keeping up.
If vacuum reports substantial dead versions that are not yet removable, find which snapshots or replication obligations are holding the removal boundary back. Do not run VACUUM FULL as a routine first response. It rewrites data and requires a much stronger lock than ordinary vacuum. That cure could interrupt the workload you were trying to save.
For the reporting service, the simplest fix might be to execute its database queries in a short, explicit read transaction and finish that transaction before doing slow network work. If the report needs a stable snapshot over a large scan, use an intentional deadline and enough resources to finish it. If it only needs eventual reporting totals, a separately maintained projection might be cheaper, with a stated freshness policy.
There is a business side to the example too. A security-sensitive action should not decide whether to allow access based on an indefinitely old reporting snapshot. Snapshot correctness means correct relative to the chosen view, not guaranteed freshness for every business decision.
The important interview insight is the trade. MVCC replaces some read/write waiting with retained history and visibility work. Once you understand how each reader chooses a version, the next failure becomes possible. Two transactions may each read a perfectly consistent snapshot and still commit a combined result that breaks a business rule.
Go deeper
What changes in different storage engines?
PostgreSQL ordinarily keeps different row versions as tuples in its heap, while other engines may use an undo chain or another versioned representation. The shared idea is snapshot visibility, not one universal byte layout.
A version might be visible because its creator committed before the snapshot and the deleting or replacing transaction had not become visible for that snapshot. The precise logic involves transaction ID state and visibility metadata, not a wall-clock comparison of 10:00 with 10:01.
Some snapshots are retained for reasons other than a user's reporting transaction, including logical replication slots and prepared transactions. Vacuum and wraparound risk are operational issues, not just disk optimization. Querying old transaction horizons and monitoring autovacuum is part of an MVCC-based database's maintenance discipline.
This lesson concerns which row versions a snapshot can still see and how long that history must remain. The use of visibility metadata in an index read path is a separate engineering decision; it does not remove the retention obligation of a long-lived snapshot.
Check your reasoning
Explain how snapshots choose tuple versions rather than simply the newest physical bytes.
Distinguish REPEATABLE READ stable snapshots from READ COMMITTED per-statement visibility.
Explain why older row versions must remain while an old snapshot could need them.
Investigate old transaction horizons, vacuum progress, and possible other holders of retention state.
Suggest short intentional read transactions and reject routine VACUUM FULL as a first response.
Common mistake: Claiming MVCC is lock-free, that all isolation levels keep the same snapshot, or that VACUUM can always delete every superseded tuple immediately.
Read the answer explanation
The later SELECT can see the newly committed value because it receives a newer statement snapshot. — READ COMMITTED uses a new ordinary statement snapshot. REPEATABLE READ instead preserves a transaction snapshot.
Apply it elsewhere
A background data export holds a database snapshot for 45 minutes while web requests update customer data. What happens to cleanup, and how could the exporter change its architecture?
Compare your approach
Check whether the export truly needs one fixed snapshot and shorten or bound that snapshot. Monitor oldest retained visibility horizons, vacuum, and any replication slots.
One more constraint
A long transaction finishes, but dead tuples are still not removable. What else could retain the oldest visibility horizon?
Quick revision
PostgreSQL creates row versions rather than immediately overwriting every reader's view. Under REPEATABLE READ, a transaction sees a stable snapshot while a later writer commits a newer version that new transactions may see. Under READ COMMITTED, each ordinary statement can get a newer snapshot. Ordinary readers and writers need not block each other for this reason, but writer conflicts and explicit locks remain. Old row versions cannot be reclaimed while relevant snapshots still need them, so monitor long-running transactions, vacuum progress, and retention horizons.