AdvancedReferenceDatabases

PostgreSQL 19 maintenance extensions - pg_visibility, pg_surgery, pg_prewarm, pgrowlocks

The contrib modules that inspect and repair what PostgreSQL 19's maintenance changes touch - pg_visibility for the visibility map that scans now write, pgrowlocks for per-row lock detail under pg_stat_lock, pg_surgery for the corruption cases that block vacuum, and pg_prewarm for cache warmup. None changed in 19; all are more relevant because of it.

Applies to
PostgreSQL15-19None of these four modules changed in PostgreSQL 19 - their function signatures are identical on supported releases. They are documented here because 19's maintenance work (scans marking pages all-visible, pg_stat_lock, the freeze-driven vacuum scores) is what you use them to inspect. 19 itself was still in beta at the last review (Beta 4, 24 September 2026).
matureUpdated Reviewed
advancedAssumes production experience with the basics.

PostgreSQL 19 maintenance extensions

A check first, because it changes how to read this page: none of these four modules changed in PostgreSQL 19. pg_visibility, pg_surgery, pg_prewarm and pgrowlocks ship the same functions they did in 15 through 18. They belong in this section anyway because 19 changed the behaviour they inspect: ordinary table scans can now set visibility-map bits (previously only VACUUM and COPY ... FREEZE could), pg_stat_lock aggregates lock contention but cannot show which rows are locked, and the new freeze-driven vacuum scores raise the cost of the corruption cases pg_surgery exists for. These are the tools for “is the on-disk state what the database thinks it is”.

All four install the same way:

CREATE EXTENSION pg_visibility;   -- or pg_surgery, pg_prewarm, pgrowlocks

pg_prewarm’s autoprewarm worker additionally needs shared_preload_libraries = 'pg_prewarm' and a restart.

pg_visibility - is the visibility map telling the truth

The visibility map (VM) holds two bits per heap page: all-visible (every tuple on the page is visible to every transaction - index-only scans may skip the heap) and all-frozen (every tuple is frozen - vacuum need not touch the page until it is modified again). A third bit, PD_ALL_VISIBLE, lives in the page header and should agree with the VM’s all-visible bit; crash recovery, races during inspection, or corruption can make them disagree.

Figure 1. How a scan earns an index-only scan
Figure 1. How a scan earns an index-only scanA table scan reads every tuple on a heap page, finds them all committed, and in PostgreSQL 19 may set the page's all-visible bit in the visibility map. A later index-only scan reads the bit and skips the heap. pg_visibility inspects the bits, and a corrupt map is rebuilt by truncating it and running VACUUM.table scanreads every tupleheap pageall seen committedvisibility mapall-visible, all-frozenindex-only scanskips the heappg_visibilitychecks bits per pageVACUUM rebuildafter truncate VM12345
New in 19: a plain scan can set all-visible (step 2) - before, only VACUUM and COPY FREEZE did. pg_visibility is how you verify the bits; the repair path is pg_truncate_visibility_map plus VACUUM.
Text equivalent
  1. A table scan reads every tuple on the page and sees all committed.
  2. In 19 the scan may set the page’s all-visible bit in the visibility map; vacuum still sets all-frozen.
  3. An index-only scan checks the bit and answers from the index, skipping the heap fetch.
  4. The bit the index-only scan trusts is the one pg_visibility audits - pg_check_visible and pg_check_frozen compare the map against the pages themselves.
  5. If the map is corrupt, pg_truncate_visibility_map clears it and the next VACUUM rebuilds it.
Function Returns Cost
pg_visibility_map(rel) / pg_visibility_map(rel, blkno) all_visible, all_frozen per page (or one page) Reads only the VM - cheap
pg_visibility(rel) / pg_visibility(rel, blkno) Both VM bits plus the page’s PD_ALL_VISIBLE Reads data blocks - expensive on large tables
pg_visibility_map_summary(rel) Count of all-visible and all-frozen pages Cheap; the quick health check
pg_check_frozen(rel) TIDs of non-frozen tuples on all-frozen pages - empty means clean Reads the relation
pg_check_visible(rel) TIDs of non-visible tuples on all-visible pages - empty means clean Reads the relation
pg_truncate_visibility_map(rel) Clears the VM; the next VACUUM rebuilds it The map reads as all-zeroes until rebuilt - index-only scans degrade to heap fetches in the meantime

Most functions run as superuser or pg_stat_scan_tables; pg_truncate_visibility_map is superuser only. The practical loop for the 19 change: after a large reporting scan, pg_visibility_map_summary shows whether all-visible coverage rose - that is the difference between an index-only scan that stays index-only and one that silently fetches every heap page.

pgrowlocks - which rows are locked, not which lock type

pg_stat_lock (new in 19) answers “which lock type serialises my writers”. pgrowlocks answers the orthogonal question: “which rows in this table are locked right now, and by whom”:

SELECT * FROM pgrowlocks('accounts');
-- locked_row | locker | multi |  xids   |      modes       |  pids
-- (0,1)      |    609 | f     | {609}   | {"For Share"}    | {3161}
-- (0,3)      |    607 | f     | {607}   | {"For Update"}   | {3107}

It takes AccessShareLock and reads the table row by row, so on a large table it is slow, it blocks behind any ACCESS EXCLUSIVE lock, and its output is not a consistent snapshot - locks can appear or release mid-scan. It shows lock holders (For Key Share through Update, including multixact IDs), not waiters; for the waiter side join pg_locks to pg_blocking_pids. Reach for it when pg_stat_lock says row-level lock waits are hurting and you need to see which rows the fight is over.

pg_surgery - the last resort for corruption that blocks vacuum

Two functions, both deliberately unsafe:

Function Does The case it exists for
heap_force_kill(rel, tid[]) Marks line pointers dead without reading the tuples A tuple whose transaction status cannot be read (for example, a missing pg_xact file) makes the row un-selectable
heap_force_freeze(rel, tid[]) Marks tuples frozen without reading them A tuple with xmin before relfrozenxid aborts VACUUM with “found xmin … from before relfrozenxid”

The second case matters more in 19 than before: a table that cannot be vacuumed accumulates the highest possible freeze score, and once vacuum_failsafe_age is passed the vacuum that runs is the aggressive, non-cancellable kind. Freezing the corrupt tuple lets vacuum proceed - at the price the documentation states plainly: these functions can make a table inconsistent with its indexes, violate UNIQUE or FOREIGN KEY constraints, or make tuples visible that crash the server when read. Use them to get a damaged table into a state where pg_dump or REPACK can run, then fix the data properly - they are a bridge to a rebuild, not a repair.

pg_prewarm - cache warmup, unchanged

pg_prewarm loads relation data into cache before it is asked for:

  • pg_prewarm(rel, mode, fork, first_block, last_block) - three modes: buffer (into the PostgreSQL buffer cache), read (into the OS cache, synchronous, works everywhere), prefetch (asynchronous OS prefetch where supported).
  • Autoprewarm: with pg_prewarm in shared_preload_libraries, a background worker dumps the shared buffer contents to autoprewarm.blocks every pg_prewarm.autoprewarm_interval seconds (default 300, 0 means dump only at shutdown), and after a restart two workers reload those blocks. autoprewarm_start_worker() and autoprewarm_dump_now() run it on demand.

The honest caveat is in the documentation: prewarmed blocks get no protection from eviction, and prewarming more than fits will evict other data - which is why it is mostly a startup tool, warming the cache while the cache is empty anyway. In 19 it pairs naturally with EXPLAIN (ANALYZE, IO) and the elastic I/O worker pool: a prewarmed buffer pool is what makes the I/O numbers on the async I/O page reproducible in benchmarks.

What changed, and in which release

Module PG 15-18 PG 19
pg_visibility Unchanged Unchanged - more relevant: scans now write the bits it verifies
pg_surgery Unchanged Unchanged - matters more: an unvacuumable table tops the new freeze score
pg_prewarm Unchanged Unchanged
pgrowlocks Unchanged Unchanged - row-level counterpart to the new pg_stat_lock

Where it breaks

  • pg_visibility on a huge table - the functions that consult PD_ALL_VISIBLE read every data block; use the _map variants or the summary first.
  • pgrowlocks mid-incident - it scans the table under AccessShareLock; on the contended table itself that can be the slowest possible way to look. pg_locks + pg_blocking_pids first.
  • pg_surgery used as a fix - it makes broken pages consistent enough to work with, not correct; anything frozen or killed this way needs auditing afterwards.
  • Prewarming past the cache size - blocks read early are evicted by blocks read later, so prewarming a relation larger than shared_buffers mostly warms the tail of it.

What this is not for

  • Routine monitoring - these are point-in-time diagnostics; the always-on surface is the statistics views on the monitoring page.
  • Fixing bloat - pg_visibility tells you the map’s state, not the table’s; bloat inspection is pgstattuple (which in 19 got faster streaming reads), and the fix is REPACK.
  • Page-level forensics - for reading tuple headers and page internals directly, that is pageinspect, not these modules.

PostgreSQL release support

5 supported
ReleaseLatestReleasedEnd of lifeStatus
1818.6Sept 2025Nov 2030Supported
1717.11Sept 2024Nov 2029Supported
1616.15Sept 2023Nov 2028Supported
1515.19Oct 2022Nov 2027Supported
1414.24Sept 2021Nov 2026Supported
1313.23Sept 2020Nov 2025End of life
1212.22Oct 2019Nov 2024End of life
1111.22Oct 2018Nov 2023End of life

Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.

What to watch

Specific things that would change what this page concludes.

  • Nothing to watch for these modules in 19 - they are unchanged; the watch item is whether any GA release note adds module changes late in the cycle

Where PostgreSQL sits

How this component relates to the rest of the stack. Items without a link are not covered here yet.

Depends on

Must be present for this to run

Deployed with

Commonly run alongside

Alternatives

Solves a similar problem differently