Home / Software y Cloud / What do you see when you read a row in PostgreSQL? Several versions at once

What do you see when you read a row in PostgreSQL? Several versions at once

Control de concurrencia multiversión en PostgreSQL

Picture a spreadsheet open to a hundred people at once: everyone reads, one writes, and nobody loses their work. At the scale of millions of transactions per second in a real database, that is called multi-version concurrency control, or MVCC. It is the mechanism that makes simultaneous reading trivial without writers blocking each other.

The problem: writing and reading should not fight

In a classic relational database, every UPDATE must leave data in a consistent state. The simplest way is to lock the row: while one process modifies it, the others wait. Under heavy concurrency that lock becomes a bottleneck, and a single hot row can drag down the whole application’s performance.

PostgreSQL took a different path. Instead of locking, it creates a new version of the row and leaves the old ones untouched. Each transaction sees a coherent snapshot of the database, as if it were looking at history at a specific point in time.

The core: xmin, xmax and the visibility map

Each row version carries two hidden metadata fields. xmin stores the transaction ID that created it; xmax stores the one that invalidated it (or 0 if it is still current). To decide whether a version is visible to a particular reader, PostgreSQL compares those IDs with the reader transaction’s own ID using a structure called the visibility map, which marks which disk pages contain only versions visible to all transactions.

Thanks to that map, most reads do not even need to check each row’s metadata: the system already knows every version on that page is valid and returns it as is.

The price: dead tuples and VACUUM

This mechanism has a cost: old row versions are not deleted instantly. They remain on disk as dead tuples until no active transaction could still need them. If that next step never happened, the table would grow without limit.

The job of collecting them falls to VACUUM (or the autovacuum process, which runs it on its own). Its work: mark dead tuple space as reusable, update the visibility map and, when required, compact the table with VACUUM FULL. That is why a PostgreSQL with no autovacuum ends up bloated: the database does not lose data, but it reads pages full of garbage.

Isolation as a consequence, not a luxury

With MVCC, transactions get isolated from each other almost for free: each works on its own snapshot. The default level, READ COMMITTED, refreshes the snapshot on every statement; REPEATABLE READ freezes it for the whole transaction. Both ensure a reader never sees half-written data, without ever having been locked by a writer.

It is the same philosophy behind other large engines, under different names: InnoDB in MySQL uses MVCC, and so do columnar engines and next-generation databases. The idea —do not lock, version— has become the de facto standard of modern engines.

What to take away

That a database “does not step on itself” is not magic: it is a price paid in disk and CPU. Next time you see a bloat warning or tables growing for no reason, think of MVCC: thousands of copies of your data waiting, neatly ordered, until no one needs them, while your application keeps reading without waiting for anyone.