Skip to content
dbexplore

Glossary: Vacuum and freezing

Visibility map, all-visible and all-frozen

Also called: all-visible, all-frozen, VM fork.

Definition, revised in place. Last updated .

The visibility map is a small file beside every table holding two bits for each of its heap pages. The all-visible bit says every row version on that page is visible to every current transaction, so nothing on it is dead and nothing needs checking. The all-frozen bit says the page additionally contains no unfrozen transaction id. Vacuum sets both, and a bit is cleared the moment anything writes to the page it describes. Nothing else in PostgreSQL is this cheap to consult.

What the two bits let the server skip

All-visible is a permission to not look at a page. It is consulted twice, by two unrelated parts of the server, for two different reasons. Vacuum reads it to skip pages it has already cleaned, which is why a routine pass over a mostly-static table finishes in a fraction of the time the table’s size suggests. The planner reads it through the executor so that an index scan can return a row without going to the heap to check whether that row is visible, which is the entire mechanism behind an index-only scan.

All-frozen is a permission to not look at a page even during an anti-wraparound pass. Without it, the pass that advances the table’s frozen id has to read every page in the table no matter how old and untouched; with it, the pass reads only the pages that have changed since the last freeze. On a large append-mostly table this is the difference between a freeze that takes minutes and one that takes hours.

The property that surprises people is how easily the bits go away. One update to one row clears the all-visible bit for the whole page containing it, and the page stays uncleared until vacuum next visits. A table taking a light, uniformly scattered write load can therefore have a nearly empty map while looking almost static from the outside.

Counting the pages a pass can skip

The pg_visibility extension reads the map directly, one row per page.

CREATE EXTENSION pg_visibility;
CREATE TABLE ship_leg (id int PRIMARY KEY, label text);
INSERT INTO ship_leg SELECT g, repeat('x', 40) FROM generate_series(1, 20000) g;
VACUUM (FREEZE, ANALYZE) ship_leg;
SELECT count(*) AS pages,
       count(*) FILTER (WHERE all_visible) AS all_visible_pages,
       count(*) FILTER (WHERE all_frozen)  AS all_frozen_pages
FROM pg_visibility_map('ship_leg');
CREATE EXTENSION
CREATE TABLE
INSERT 0 20000
VACUUM
 pages | all_visible_pages | all_frozen_pages 
-------+-------------------+------------------
   187 |               187 |              187
(1 row)

Every page is marked both ways, because the table was written once and then frozen, which is the best case and also the shape of a partition that has stopped receiving rows. Run the same count after a scattered UPDATE and the visible total falls much faster than the number of rows touched would suggest, since the unit is the page and not the row.

pg_class.relallvisible is the cheaper approximation of the same thing and needs no extension, but it is a planner estimate refreshed by vacuum rather than a live count, so it can be confidently wrong about a table that has been written to since.

Keeping the map full

A map that keeps emptying is a vacuum frequency question, and the treatment that covers thresholds, scale factors and the per-table overrides worth setting is the guide on autovacuum and table bloat. If the reason you care is a query plan that stopped being index-only, start instead from the index-only scan, where the heap fetch counter is the thing to read.

Put every Postgres you run on autopilot.

We onboard teams in small batches. Tell us about your fleet and we will reach out when a seat opens. One email, no drip campaign.