A PostgreSQL UPDATE creates a new row version rather than overwriting the old tuple in place. That MVCC behavior supports concurrent readers, but a routine update can also create work in every index attached to the table. Heap-only tuples, usually called HOT updates, let PostgreSQL avoid much of that index work when the new row version meets a narrow set of conditions.
HOT is not a different SQL operation. It is a storage-level optimization selected by PostgreSQL during an ordinary UPDATE.
An update normally creates another tuple
PostgreSQL stores table rows in heap pages. When a transaction updates a row, the database creates a new tuple version and marks the previous version so visibility rules can determine which transaction sees which version.
Without HOT, indexes need entries that can locate the new tuple version. On a table with several indexes, changing one logical row can therefore create heap activity plus index activity across multiple structures.
That cost is not limited to the immediate write. Old tuple versions and obsolete index entries eventually have to be reclaimed. Repeated updates can increase the amount of storage and maintenance work associated with a row even when the application considers it a single record.
HOT changes the index side of this process.
HOT keeps the version chain on one heap page
An update can use HOT when the new tuple version fits on the same heap page as the old version and the update does not change columns referenced by ordinary indexes. PostgreSQL can then link the tuple versions within that page without creating fresh entries in those indexes.
Consider a table where id and email are indexed but last_seen_at is not:
CREATE TABLE accounts (
id bigint PRIMARY KEY,
email text NOT NULL,
last_seen_at timestamptz
);
CREATE INDEX accounts_email_idx ON accounts (email);An update such as this is a candidate for HOT:
UPDATE accounts
SET last_seen_at = clock_timestamp()
WHERE id = 42;The indexed values stay unchanged. If the page containing the current tuple also has enough room for the new version, PostgreSQL can keep the update in a HOT chain.
Changing email removes that eligibility for an ordinary index on email:
UPDATE accounts
SET email = 'new-address@example.test'
WHERE id = 42;The index key has changed, so the database needs index maintenance that a classic HOT update is designed to avoid.
Current PostgreSQL releases treat summarizing indexes differently from ordinary per-tuple indexes. BRIN is the summarizing index method included in core PostgreSQL, so its presence does not by itself block HOT eligibility in the same manner as a B-tree index on an updated column. A BRIN summary can still require maintenance.
Page space is part of the decision
Avoiding indexed-column changes is necessary but not sufficient. The new tuple version must also fit on the same heap page.
This makes free space a direct part of HOT behavior. A densely packed page may have no room for another version even when only an unindexed column changes. PostgreSQL then has to place the new tuple elsewhere, preventing the same-page HOT chain.
The table fillfactor setting can reserve room for later updates. A lower value leaves more free space on pages during insertion:
CREATE TABLE session_state (
session_id uuid PRIMARY KEY,
payload jsonb NOT NULL,
touched_at timestamptz NOT NULL
) WITH (fillfactor = 80);Reserving space is not automatically beneficial. Lower fillfactor also means fewer initial rows per page, so the table can occupy more pages. The useful setting depends on row size, update frequency, indexed columns, and access patterns.
HOT can still occur with the default fillfactor. Pages naturally acquire reusable space as tuples move through their lifecycle. Fillfactor changes the probability that suitable same-page space is available; it does not enable HOT as a separate feature.
Index design affects update cost
Adding an index changes more than read paths. If a frequently modified column becomes part of an ordinary index, updates to that column no longer qualify for classic HOT on the basis of unchanged indexed attributes.
Expression indexes count as well because their indexed result depends on referenced columns. Partial indexes also have column dependencies in their predicates and indexed expressions.
For example:
CREATE INDEX accounts_lower_email_idx
ON accounts (lower(email));An update to email can affect the indexed expression even though the index does not store the raw column expression as a plain key. PostgreSQL must account for that dependency when deciding whether index entries can remain valid.
This creates a useful connection between schema design and write amplification. An index that serves a real query pattern can be worth its update cost. An index that is rarely used can impose heap-adjacent maintenance costs and reduce HOT opportunities for the columns it references.
The relevant question is not simply how many indexes a table has. It is which columns those indexes depend on and which columns the workload modifies.
HOT chains also support local cleanup
Avoiding new index entries is only one part of HOT. Because index entries can continue to identify the root of a same-page version chain, PostgreSQL can prune obsolete intermediate tuple versions during normal page access when visibility rules permit it.
That local pruning can make line pointers and page space reusable without waiting for every cleanup action to be driven by a later vacuum pass. Vacuum remains part of PostgreSQL maintenance, but HOT reduces some of the index and heap cleanup pressure generated by repeated eligible updates.
The effect is especially relevant for rows that receive many updates to non-indexed state. A chain can advance through newer versions while ordinary index entries continue to lead into the page-level chain rather than accumulating a fresh index tuple for every version.
HOT activity is observable
PostgreSQL exposes table update counters through statistics views. pg_stat_all_tables includes counters for total updates and HOT updates, allowing the ratio to be inspected for a table:
SELECT
schemaname,
relname,
n_tup_upd,
n_tup_hot_upd
FROM pg_stat_all_tables
WHERE relname = 'accounts';A high or low ratio has no universal target. A table that frequently changes indexed keys may legitimately produce few HOT updates. A mostly append-only table may have little update traffic at all.
The counters are more useful when interpreted alongside the schema and workload. If a table performs many updates to non-indexed columns but records few HOT updates, page space is one condition worth examining. If updates routinely touch indexed attributes, the low HOT count follows directly from the index dependencies.
Statistics can also change across PostgreSQL versions, so operational queries should be checked against the documentation for the deployed release rather than copied as permanent monitoring contracts.
HOT is an optimization, not an application contract
Applications should not depend on a particular update becoming HOT. Eligibility is decided from storage conditions and index dependencies that can change as pages fill, schemas evolve, and PostgreSQL versions add storage behavior.
The useful model is narrower. PostgreSQL MVCC creates new row versions; classic HOT can keep eligible versions on the same heap page and avoid fresh entries in ordinary indexes whose referenced columns did not change. Schema choices and available page space influence how often that optimization can apply.
That makes HOT most useful as a lens for examining update-heavy tables. Indexes shape read access, but they also participate in the physical cost of row versioning. Looking at both sides produces a more accurate picture of what an UPDATE asks PostgreSQL to do.