From 89a76ceedd251e74742452f1b6fa57653c7219b9 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 17 Dec 2025 16:51:05 -0500 Subject: [PATCH v36 05/16] Use GlobalVisState in vacuum to determine page level visibility During vacuum's first and third phases, we examine tuples' visibility to determine if we can set the page all-visible in the visibility map. Previously, this check compared tuple xmins against a single XID chosen at the start of vacuum (OldestXmin). We now use GlobalVisState, which also enables future work to set the VM during on-access pruning, since ordinary queries have access to GlobalVisState but not OldestXmin. This also benefits vacuum: in some cases, GlobalVisState may advance during a vacuum, allowing more pages to become considered all-visible. And, in the future, we could easily add a heuristic to update GlobalVisState more frequently during vacuums of large tables. OldestXmin is still used for freezing and as a backstop to ensure we don't freeze a dead tuple that wasn't yet prunable according to GlobalVisState in the rare occurrences where GlobalVisState moves backwards. Because comparing a transaction ID against GlobalVisState is more expensive than comparing against a single XID, we defer this check until after scanning all tuples on the page. Therefore, we perform the GlobalVisState check only once per page. This is safe because visibility_cutoff_xid records the newest live xmin on the page; if it is globally visible, then the entire page is all-visible. Using GlobalVisState means on-access pruning can also maintain visibility_cutoff_xid. This approach will result in examining more tuple xmins than before; however, the additional cost should not be significant. And doing so will enable us to set the visibility map on access in the future. Author: Melanie Plageman Reviewed-by: Andres Freund Reviewed-by: Chao Li Discussion: https://postgr.es/m/flat/bqc4kh5midfn44gnjiqez3bjqv4zogydguvdn446riw45jcf3y%404ez66il7ebvk#c755ef151507aba58471ffaca607e493 --- src/backend/access/heap/heapam_visibility.c | 22 +++++++++ src/backend/access/heap/pruneheap.c | 37 +++++++-------- src/backend/access/heap/vacuumlazy.c | 51 +++++++++++++-------- src/include/access/heapam.h | 2 + 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 75ae268d753..aee88947393 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -1060,6 +1060,28 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, return res; } +/* + * Wrapper around GlobalVisTestIsRemovableXid() for use when examining live + * tuples. Returns true if the given XID may be considered running by at least + * one snapshot. + * + * This function alone is insufficient to determine tuple visibility; callers + * must also consider the XID's commit status. Its purpose is purely semantic: + * when applied to live tuples, GlobalVisTestIsRemovableXid() is checking + * whether the inserting transaction is still considered running, not whether + * the tuple is removable. Live tuples are, by definition, not removable, but + * the snapshot criteria for “transaction still running” are identical to + * those used for removal XIDs. + * + * See the comment above GlobalVisTestIsRemovable[Full]Xid() for details on the + * required preconditions for calling this function. + */ +bool +GlobalVisTestXidMaybeRunning(GlobalVisState *state, TransactionId xid) +{ + return !GlobalVisTestIsRemovableXid(state, xid); +} + /* * Work horse for HeapTupleSatisfiesVacuum and similar routines. * diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 2cd684873c0..f7e9fd51ac9 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -1035,6 +1035,17 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, */ prune_freeze_plan(&prstate, off_loc); + /* + * After processing all the live tuples on the page, if the newest xmin + * amongst them may be considered running by any snapshot, the page cannot + * be all-visible. + */ + if (prstate.set_all_visible && + TransactionIdIsNormal(prstate.visibility_cutoff_xid) && + GlobalVisTestXidMaybeRunning(prstate.vistest, + prstate.visibility_cutoff_xid)) + prstate.set_all_visible = prstate.set_all_frozen = false; + /* * If checksums are enabled, calling heap_prune_satisfies_vacuum() while * checking tuple visibility information in prune_freeze_plan() may have @@ -1702,29 +1713,15 @@ heap_prune_record_unchanged_lp_normal(PruneState *prstate, OffsetNumber offnum) } /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? A FrozenTransactionId - * is seen as committed to everyone. Otherwise, we check if - * there is a snapshot that considers this xid to still be - * running, and if so, we don't consider the page all-visible. + * The inserter definitely committed. But we don't know if it + * is old enough that everyone sees it as committed. Later, + * after processing all the tuples on the page, we'll check if + * there is any snapshot that still considers the newest xid + * on the page to be running. If so, we don't consider the + * page all-visible. */ xmin = HeapTupleHeaderGetXmin(htup); - /* - * For now always use prstate->cutoffs for this test, because - * we only update 'set_all_visible' and 'set_all_frozen' when - * freezing is requested. We could use - * GlobalVisTestIsRemovableXid instead, if a non-freezing - * caller wanted to set the VM bit. - */ - Assert(prstate->cutoffs); - if (!TransactionIdPrecedes(xmin, prstate->cutoffs->OldestXmin)) - { - prstate->set_all_visible = false; - prstate->set_all_frozen = false; - break; - } - /* Track newest xmin on page. */ if (TransactionIdFollows(xmin, prstate->visibility_cutoff_xid) && TransactionIdIsNormal(xmin)) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 957322648ca..ad85e1e1738 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -461,13 +461,13 @@ static void dead_items_cleanup(LVRelState *vacrel); #ifdef USE_ASSERT_CHECKING static bool heap_page_is_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, bool *all_frozen, TransactionId *visibility_cutoff_xid, OffsetNumber *logging_offnum); #endif static bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -2054,13 +2054,10 @@ lazy_scan_prune(LVRelState *vacrel, Assert(presult.lpdead_items == 0); Assert(heap_page_is_all_visible(vacrel->rel, buf, - vacrel->cutoffs.OldestXmin, &debug_all_frozen, + vacrel->vistest, &debug_all_frozen, &debug_cutoff, &vacrel->offnum)); Assert(presult.set_all_frozen == debug_all_frozen); - - Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == presult.vm_conflict_horizon); } #endif @@ -2816,7 +2813,7 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, * done outside the critical section. */ if (heap_page_would_be_all_visible(vacrel->rel, buffer, - vacrel->cutoffs.OldestXmin, + vacrel->vistest, deadoffsets, num_offsets, &all_frozen, &visibility_cutoff_xid, &vacrel->offnum)) @@ -3577,14 +3574,14 @@ dead_items_cleanup(LVRelState *vacrel) */ static bool heap_page_is_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, bool *all_frozen, TransactionId *visibility_cutoff_xid, OffsetNumber *logging_offnum) { return heap_page_would_be_all_visible(rel, buf, - OldestXmin, + vistest, NULL, 0, all_frozen, visibility_cutoff_xid, @@ -3605,7 +3602,7 @@ heap_page_is_all_visible(Relation rel, Buffer buf, * Returns true if the page is all-visible other than the provided * deadoffsets and false otherwise. * - * OldestXmin is used to determine visibility. + * vistest is used to determine visibility. * * Output parameters: * @@ -3624,7 +3621,7 @@ heap_page_is_all_visible(Relation rel, Buffer buf, */ static bool heap_page_would_be_all_visible(Relation rel, Buffer buf, - TransactionId OldestXmin, + GlobalVisState *vistest, OffsetNumber *deadoffsets, int ndeadoffsets, bool *all_frozen, @@ -3705,7 +3702,7 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, { TransactionId xmin; - /* Check comments in lazy_scan_prune. */ + /* Check heap_prune_record_unchanged_lp_normal comments */ if (!HeapTupleHeaderXminCommitted(tuple.t_data)) { all_visible = false; @@ -3714,16 +3711,17 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, } /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? + * The inserter definitely committed. But we don't know if + * it is old enough that everyone sees it as committed. + * Don't check that now. + * + * If we scan all tuples without finding one that prevents + * the page from being all-visible, we then check whether + * any snapshot still considers the newest XID on the page + * to be running. In that case, the page is not considered + * all-visible. */ xmin = HeapTupleHeaderGetXmin(tuple.t_data); - if (!TransactionIdPrecedes(xmin, OldestXmin)) - { - all_visible = false; - *all_frozen = false; - break; - } /* Track newest xmin on page. */ if (TransactionIdFollows(xmin, *visibility_cutoff_xid) && @@ -3752,6 +3750,19 @@ heap_page_would_be_all_visible(Relation rel, Buffer buf, } } /* scan along page */ + /* + * After processing all the live tuples on the page, if the newest xmin + * among them may still be considered running by any snapshot, the page + * cannot be all-visible. + */ + if (all_visible && + TransactionIdIsNormal(*visibility_cutoff_xid) && + GlobalVisTestXidMaybeRunning(vistest, *visibility_cutoff_xid)) + { + all_visible = false; + *all_frozen = false; + } + /* Clear the offset information once we have processed the given page. */ *logging_offnum = InvalidOffsetNumber; diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 568358a060a..849ed82bcf2 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -475,6 +475,8 @@ extern TM_Result HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, Buffer buffer); extern HTSV_Result HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, Buffer buffer); + +extern bool GlobalVisTestXidMaybeRunning(GlobalVisState *state, TransactionId xid); extern HTSV_Result HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *dead_after); extern void HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer, -- 2.43.0