Technical Article

MVCC, Autovacuum, and HOT Updates

When you update a row in PostgreSQL, the application sees only the updated value. Underneath the hood, however, the storage engine has just scheduled deferred maintenance: Multi-Version Concurrency Control (MVCC) does not overwrite data in place; it writes a new version of the tuple while preserving the older version on disk.

This design enables concurrent transactions to read isolated, consistent snapshots without acquiring read locks against active writers. The trade-off is that write-heavy and update-heavy workloads continuously generate dead tuples, produce Write-Ahead Logging (WAL) volume, and exert steady pressure on the autovacuum daemon.

The Hidden Cost of Table and Index Bloat

The performance penalty of updates rarely surfaces immediately during the write operation itself. Instead, it accumulates gradually across the storage layer:

  • Table pages become fragmented with dead tuple gaps.
  • B-tree indexes expand as new index pointers are appended.
  • Buffer pool cache efficiency degrades as memory fills with unvacuumed pages.
  • Read queries must scan significantly more disk pages to retrieve identical result sets.

This bloat cycle compounds when long-running transactions remain open. PostgreSQL cannot vacuum dead tuples if their transaction horizon might still be visible to an active transaction's snapshot.

Heap-Only Tuples (HOT) Updates

This is where Heap-Only Tuples (HOT) updates become critical. A HOT update occurs when two conditions are met:

  1. The update does not modify any column referenced by a table index.
  2. The page containing the original tuple has sufficient free space (fillfactor) to hold the new tuple version.

When these conditions hold, PostgreSQL links the new tuple directly inside the existing heap page via an internal pointer chain. Because no index references need updating, the database bypasses writing new entries to every secondary index on the table.

While HOT updates do not completely eliminate MVCC overhead, they drastically mitigate write amplification—especially on wide tables with numerous secondary indexes where a single standard update would otherwise require rewriting pointers across multiple index B-trees.

Investigating Write Performance Beyond Latency

Evaluating database write performance requires looking far beyond simple UPDATE execution latency. Key operational indicators include:

  • Rate of dead tuple generation vs. autovacuum throughput.
  • Maximum age of open transactions and snapshot pinning.
  • Physical table and index growth rates over time.
  • Ratio of HOT updates to total update operations.

In PostgreSQL, every write mutation incurs an obligation for future cleanup. When the mutation rate outpaces autovacuum cleanup capacity, the accumulated technical debt inevitably manifests as degraded cache efficiency, elevated I/O, and higher query latencies.