Quelle pruneheap.c
Sprache: C
/*-------------------------------------------------------------------------
*
* pruneheap . c
* heap page pruning and HOT - chain management code
*
* Portions Copyright ( c ) 1996 - 2025 , PostgreSQL Global Development Group
* Portions Copyright ( c ) 1994 , Regents of the University of California
*
*
* IDENTIFICATION
* src / backend / access / heap / pruneheap . c
*
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
#include "postgres.h"
#include "access/heapam.h"
#include "access/heapam_xlog.h"
#include "access/htup_details.h"
#include "access/multixact.h"
#include "access/transam.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "commands/vacuum.h"
#include "executor/instrument.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
/* Working data for heap_page_prune_and_freeze() and subroutines */
typedef struct
{
/*-------------------------------------------------------
* Arguments passed to heap_page_prune_and_freeze ( )
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
/* tuple visibility test, initialized for the relation */
GlobalVisState *vistest;
/* whether or not dead items can be set LP_UNUSED during pruning */
bool mark_unused_now;
/* whether to attempt freezing tuples */
bool freeze;
struct VacuumCutoffs *cutoffs;
/*-------------------------------------------------------
* Fields describing what to do to the page
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
TransactionId new_prune_xid; /* new prune hint value */
TransactionId latest_xid_removed;
int nredirected; /* numbers of entries in arrays below */
int ndead;
int nunused;
int nfrozen;
/* arrays that accumulate indexes of items to be changed */
OffsetNumber redirected[MaxHeapTuplesPerPage * 2 ];
OffsetNumber nowdead[MaxHeapTuplesPerPage];
OffsetNumber nowunused[MaxHeapTuplesPerPage];
HeapTupleFreeze frozen[MaxHeapTuplesPerPage];
/*-------------------------------------------------------
* Working state for HOT chain processing
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
/*
* ' root_items ' contains offsets of all LP_REDIRECT line pointers and
* normal non - HOT tuples . They can be stand - alone items or the first item
* in a HOT chain . ' heaponly_items ' contains heap - only tuples which can
* only be removed as part of a HOT chain .
*/
int nroot_items;
OffsetNumber root_items[MaxHeapTuplesPerPage];
int nheaponly_items;
OffsetNumber heaponly_items[MaxHeapTuplesPerPage];
/*
* processed [ offnum ] is true if item at offnum has been processed .
*
* This needs to be MaxHeapTuplesPerPage + 1 long as FirstOffsetNumber is
* 1 . Otherwise every access would need to subtract 1 .
*/
bool processed[MaxHeapTuplesPerPage + 1 ];
/*
* Tuple visibility is only computed once for each tuple , for correctness
* and efficiency reasons ; see comment in heap_page_prune_and_freeze ( ) for
* details . This is of type int8 [ ] , instead of HTSV_Result [ ] , so we can
* use - 1 to indicate no visibility has been computed , e . g . for LP_DEAD
* items .
*
* This needs to be MaxHeapTuplesPerPage + 1 long as FirstOffsetNumber is
* 1 . Otherwise every access would need to subtract 1 .
*/
int8 htsv[MaxHeapTuplesPerPage + 1 ];
/*
* Freezing - related state .
*/
HeapPageFreeze pagefrz;
/*-------------------------------------------------------
* Information about what was done
*
* These fields are not used by pruning itself for the most part , but are
* used to collect information about what was pruned and what state the
* page is in after pruning , for the benefit of the caller . They are
* copied to the caller ' s PruneFreezeResult at the end .
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
int ndeleted; /* Number of tuples deleted from the page */
/* Number of live and recently dead tuples, after pruning */
int live_tuples;
int recently_dead_tuples;
/* Whether or not the page makes rel truncation unsafe */
bool hastup;
/*
* LP_DEAD items on the page after pruning . Includes existing LP_DEAD
* items
*/
int lpdead_items; /* number of items in the array */
OffsetNumber *deadoffsets; /* points directly to presult->deadoffsets */
/*
* all_visible and all_frozen indicate if the all - visible and all - frozen
* bits in the visibility map can be set for this page after pruning .
*
* visibility_cutoff_xid is the newest xmin of live tuples on the page .
* The caller can use it as the conflict horizon , when setting the VM
* bits . It is only valid if we froze some tuples , and all_frozen is
* true .
*
* NOTE : all_visible and all_frozen don ' t include LP_DEAD items . That ' s
* convenient for heap_page_prune_and_freeze ( ) , to use them to decide
* whether to freeze the page or not . The all_visible and all_frozen
* values returned to the caller are adjusted to include LP_DEAD items at
* the end .
*
* all_frozen should only be considered valid if all_visible is also set ;
* we don ' t bother to clear the all_frozen flag every time we clear the
* all_visible flag .
*/
bool all_visible;
bool all_frozen;
TransactionId visibility_cutoff_xid;
} PruneState;
/* Local functions */
static HTSV_Result heap_prune_satisfies_vacuum(PruneState *prstate,
HeapTuple tup,
Buffer buffer);
static inline HTSV_Result htsv_get_valid_status(int status);
static void heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff,
OffsetNumber rootoffnum, PruneState *prstate);
static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid);
static void heap_prune_record_redirect(PruneState *prstate,
OffsetNumber offnum, OffsetNumber rdoffnum,
bool was_normal);
static void heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum,
bool was_normal);
static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum,
bool was_normal);
static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum, bool was_normal);
static void heap_prune_record_unchanged_lp_unused(Page page, PruneState *prstate, OffsetNumber offnum);
static void heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumber offnum);
static void heap_prune_record_unchanged_lp_dead(Page page, PruneState *prstate, OffsetNumber offnum);
static void heap_prune_record_unchanged_lp_redirect(PruneState *prstate, OffsetNumber offnum);
static void page_verify_redirects(Page page);
/*
* Optionally prune and repair fragmentation in the specified page .
*
* This is an opportunistic function . It will perform housekeeping
* only if the page heuristically looks like a candidate for pruning and we
* can acquire buffer cleanup lock without blocking .
*
* Note : this is called quite often . It ' s important that it fall out quickly
* if there ' s not any use in pruning .
*
* Caller must have pin on the buffer , and must * not * have a lock on it .
*/
void
heap_page_prune_opt(Relation relation, Buffer buffer)
{
Page page = BufferGetPage(buffer);
TransactionId prune_xid;
GlobalVisState *vistest;
Size minfree;
/*
* We can ' t write WAL in recovery mode , so there ' s no point trying to
* clean the page . The primary will likely issue a cleaning WAL record
* soon anyway , so this is no particular loss .
*/
if (RecoveryInProgress())
return ;
/*
* First check whether there ' s any chance there ' s something to prune ,
* determining the appropriate horizon is a waste if there ' s no prune_xid
* ( i . e . no updates / deletes left potentially dead tuples around ) .
*/
prune_xid = ((PageHeader) page)->pd_prune_xid;
if (!TransactionIdIsValid(prune_xid))
return ;
/*
* Check whether prune_xid indicates that there may be dead rows that can
* be cleaned up .
*/
vistest = GlobalVisTestFor(relation);
if (!GlobalVisTestIsRemovableXid(vistest, prune_xid))
return ;
/*
* We prune when a previous UPDATE failed to find enough space on the page
* for a new tuple version , or when free space falls below the relation ' s
* fill - factor target ( but not less than 10 % ) .
*
* Checking free space here is questionable since we aren ' t holding any
* lock on the buffer ; in the worst case we could get a bogus answer . It ' s
* unlikely to be * seriously * wrong , though , since reading either pd_lower
* or pd_upper is probably atomic . Avoiding taking a lock seems more
* important than sometimes getting a wrong answer in what is after all
* just a heuristic estimate .
*/
minfree = RelationGetTargetPageFreeSpace(relation,
HEAP_DEFAULT_FILLFACTOR);
minfree = Max(minfree, BLCKSZ / 10 );
if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree)
{
/* OK, try to get exclusive buffer lock */
if (!ConditionalLockBufferForCleanup(buffer))
return ;
/*
* Now that we have buffer lock , get accurate information about the
* page ' s free space , and recheck the heuristic about whether to
* prune .
*/
if (PageIsFull(page) || PageGetHeapFreeSpace(page) < minfree)
{
OffsetNumber dummy_off_loc;
PruneFreezeResult presult;
/*
* For now , pass mark_unused_now as false regardless of whether or
* not the relation has indexes , since we cannot safely determine
* that during on - access pruning with the current implementation .
*/
heap_page_prune_and_freeze(relation, buffer, vistest, 0 ,
NULL, &presult, PRUNE_ON_ACCESS, &dummy_off_loc, NULL, NULL);
/*
* Report the number of tuples reclaimed to pgstats . This is
* presult . ndeleted minus the number of newly - LP_DEAD - set items .
*
* We derive the number of dead tuples like this to avoid totally
* forgetting about items that were set to LP_DEAD , since they
* still need to be cleaned up by VACUUM . We only want to count
* heap - only tuples that just became LP_UNUSED in our report ,
* which don ' t .
*
* VACUUM doesn ' t have to compensate in the same way when it
* tracks ndeleted , since it will set the same LP_DEAD items to
* LP_UNUSED separately .
*/
if (presult.ndeleted > presult.nnewlpdead)
pgstat_update_heap_dead_tuples(relation,
presult.ndeleted - presult.nnewlpdead);
}
/* And release buffer lock */
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/*
* We avoid reuse of any free space created on the page by unrelated
* UPDATEs / INSERTs by opting to not update the FSM at this point . The
* free space should be reused by UPDATEs to * this * page .
*/
}
}
/*
* Prune and repair fragmentation and potentially freeze tuples on the
* specified page .
*
* Caller must have pin and buffer cleanup lock on the page . Note that we
* don ' t update the FSM information for page on caller ' s behalf . Caller might
* also need to account for a reduction in the length of the line pointer
* array following array truncation by us .
*
* If the HEAP_PRUNE_FREEZE option is set , we will also freeze tuples if it ' s
* required in order to advance relfrozenxid / relminmxid , or if it ' s
* considered advantageous for overall system performance to do so now . The
* ' cutoffs ' , ' presult ' , ' new_relfrozen_xid ' and ' new_relmin_mxid ' arguments
* are required when freezing . When HEAP_PRUNE_FREEZE option is set , we also
* set presult - > all_visible and presult - > all_frozen on exit , to indicate if
* the VM bits can be set . They are always set to false when the
* HEAP_PRUNE_FREEZE option is not set , because at the moment only callers
* that also freeze need that information .
*
* vistest is used to distinguish whether tuples are DEAD or RECENTLY_DEAD
* ( see heap_prune_satisfies_vacuum ) .
*
* options :
* MARK_UNUSED_NOW indicates that dead items can be set LP_UNUSED during
* pruning .
*
* FREEZE indicates that we will also freeze tuples , and will return
* ' all_visible ' , ' all_frozen ' flags to the caller .
*
* cutoffs contains the freeze cutoffs , established by VACUUM at the beginning
* of vacuuming the relation . Required if HEAP_PRUNE_FREEZE option is set .
* cutoffs - > OldestXmin is also used to determine if dead tuples are
* HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD .
*
* presult contains output parameters needed by callers , such as the number of
* tuples removed and the offsets of dead items on the page after pruning .
* heap_page_prune_and_freeze ( ) is responsible for initializing it . Required
* by all callers .
*
* reason indicates why the pruning is performed . It is included in the WAL
* record for debugging and analysis purposes , but otherwise has no effect .
*
* off_loc is the offset location required by the caller to use in error
* callback .
*
* new_relfrozen_xid and new_relmin_mxid must provided by the caller if the
* HEAP_PRUNE_FREEZE option is set . On entry , they contain the oldest XID and
* multi - XID seen on the relation so far . They will be updated with oldest
* values present on the page after pruning . After processing the whole
* relation , VACUUM can use these values as the new relfrozenxid / relminmxid
* for the relation .
*/
void
heap_page_prune_and_freeze(Relation relation, Buffer buffer,
GlobalVisState *vistest,
int options,
struct VacuumCutoffs *cutoffs,
PruneFreezeResult *presult,
PruneReason reason,
OffsetNumber *off_loc,
TransactionId *new_relfrozen_xid,
MultiXactId *new_relmin_mxid)
{
Page page = BufferGetPage(buffer);
BlockNumber blockno = BufferGetBlockNumber(buffer);
OffsetNumber offnum,
maxoff;
PruneState prstate;
HeapTupleData tup;
bool do_freeze;
bool do_prune;
bool do_hint;
bool hint_bit_fpi;
int64 fpi_before = pgWalUsage.wal_fpi;
/* Copy parameters to prstate */
prstate.vistest = vistest;
prstate.mark_unused_now = (options & HEAP_PAGE_PRUNE_MARK_UNUSED_NOW) != 0 ;
prstate.freeze = (options & HEAP_PAGE_PRUNE_FREEZE) != 0 ;
prstate.cutoffs = cutoffs;
/*
* Our strategy is to scan the page and make lists of items to change ,
* then apply the changes within a critical section . This keeps as much
* logic as possible out of the critical section , and also ensures that
* WAL replay will work the same as the normal case .
*
* First , initialize the new pd_prune_xid value to zero ( indicating no
* prunable tuples ) . If we find any tuples which may soon become
* prunable , we will save the lowest relevant XID in new_prune_xid . Also
* initialize the rest of our working state .
*/
prstate.new_prune_xid = InvalidTransactionId;
prstate.latest_xid_removed = InvalidTransactionId;
prstate.nredirected = prstate.ndead = prstate.nunused = prstate.nfrozen = 0 ;
prstate.nroot_items = 0 ;
prstate.nheaponly_items = 0 ;
/* initialize page freezing working state */
prstate.pagefrz.freeze_required = false ;
if (prstate.freeze)
{
Assert(new_relfrozen_xid && new_relmin_mxid);
prstate.pagefrz.FreezePageRelfrozenXid = *new_relfrozen_xid;
prstate.pagefrz.NoFreezePageRelfrozenXid = *new_relfrozen_xid;
prstate.pagefrz.FreezePageRelminMxid = *new_relmin_mxid;
prstate.pagefrz.NoFreezePageRelminMxid = *new_relmin_mxid;
}
else
{
Assert(new_relfrozen_xid == NULL && new_relmin_mxid == NULL);
prstate.pagefrz.FreezePageRelminMxid = InvalidMultiXactId;
prstate.pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId;
prstate.pagefrz.FreezePageRelfrozenXid = InvalidTransactionId;
prstate.pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId;
}
prstate.ndeleted = 0 ;
prstate.live_tuples = 0 ;
prstate.recently_dead_tuples = 0 ;
prstate.hastup = false ;
prstate.lpdead_items = 0 ;
prstate.deadoffsets = presult->deadoffsets;
/*
* Caller may update the VM after we ' re done . We can keep track of
* whether the page will be all - visible and all - frozen after pruning and
* freezing to help the caller to do that .
*
* Currently , only VACUUM sets the VM bits . To save the effort , only do
* the bookkeeping if the caller needs it . Currently , that ' s tied to
* HEAP_PAGE_PRUNE_FREEZE , but it could be a separate flag if you wanted
* to update the VM bits without also freezing or freeze without also
* setting the VM bits .
*
* In addition to telling the caller whether it can set the VM bit , we
* also use ' all_visible ' and ' all_frozen ' for our own decision - making . If
* the whole page would become frozen , we consider opportunistically
* freezing tuples . We will not be able to freeze the whole page if there
* are tuples present that are not visible to everyone or if there are
* dead tuples which are not yet removable . However , dead tuples which
* will be removed by the end of vacuuming should not preclude us from
* opportunistically freezing . Because of that , we do not clear
* all_visible when we see LP_DEAD items . We fix that at the end of the
* function , when we return the value to the caller , so that the caller
* doesn ' t set the VM bit incorrectly .
*/
if (prstate.freeze)
{
prstate.all_visible = true ;
prstate.all_frozen = true ;
}
else
{
/*
* Initializing to false allows skipping the work to update them in
* heap_prune_record_unchanged_lp_normal ( ) .
*/
prstate.all_visible = false ;
prstate.all_frozen = false ;
}
/*
* The visibility cutoff xid is the newest xmin of live tuples on the
* page . In the common case , this will be set as the conflict horizon the
* caller can use for updating the VM . If , at the end of freezing and
* pruning , the page is all - frozen , there is no possibility that any
* running transaction on the standby does not see tuples on the page as
* all - visible , so the conflict horizon remains InvalidTransactionId .
*/
prstate.visibility_cutoff_xid = InvalidTransactionId;
maxoff = PageGetMaxOffsetNumber(page);
tup.t_tableOid = RelationGetRelid(relation);
/*
* Determine HTSV for all tuples , and queue them up for processing as HOT
* chain roots or as heap - only items .
*
* Determining HTSV only once for each tuple is required for correctness ,
* to deal with cases where running HTSV twice could result in different
* results . For example , RECENTLY_DEAD can turn to DEAD if another
* checked item causes GlobalVisTestIsRemovableFullXid ( ) to update the
* horizon , or INSERT_IN_PROGRESS can change to DEAD if the inserting
* transaction aborts .
*
* It ' s also good for performance . Most commonly tuples within a page are
* stored at decreasing offsets ( while the items are stored at increasing
* offsets ) . When processing all tuples on a page this leads to reading
* memory at decreasing offsets within a page , with a variable stride .
* That ' s hard for CPU prefetchers to deal with . Processing the items in
* reverse order ( and thus the tuples in increasing order ) increases
* prefetching efficiency significantly / decreases the number of cache
* misses .
*/
for (offnum = maxoff;
offnum >= FirstOffsetNumber;
offnum = OffsetNumberPrev(offnum))
{
ItemId itemid = PageGetItemId(page, offnum);
HeapTupleHeader htup;
/*
* Set the offset number so that we can display it along with any
* error that occurred while processing this tuple .
*/
*off_loc = offnum;
prstate.processed[offnum] = false ;
prstate.htsv[offnum] = -1 ;
/* Nothing to do if slot doesn't contain a tuple */
if (!ItemIdIsUsed(itemid))
{
heap_prune_record_unchanged_lp_unused(page, &prstate, offnum);
continue ;
}
if (ItemIdIsDead(itemid))
{
/*
* If the caller set mark_unused_now true , we can set dead line
* pointers LP_UNUSED now .
*/
if (unlikely(prstate.mark_unused_now))
heap_prune_record_unused(&prstate, offnum, false );
else
heap_prune_record_unchanged_lp_dead(page, &prstate, offnum);
continue ;
}
if (ItemIdIsRedirected(itemid))
{
/* This is the start of a HOT chain */
prstate.root_items[prstate.nroot_items++] = offnum;
continue ;
}
Assert(ItemIdIsNormal(itemid));
/*
* Get the tuple ' s visibility status and queue it up for processing .
*/
htup = (HeapTupleHeader) PageGetItem(page, itemid);
tup.t_data = htup;
tup.t_len = ItemIdGetLength(itemid);
ItemPointerSet(&tup.t_self, blockno, offnum);
prstate.htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup,
buffer);
if (!HeapTupleHeaderIsHeapOnly(htup))
prstate.root_items[prstate.nroot_items++] = offnum;
else
prstate.heaponly_items[prstate.nheaponly_items++] = offnum;
}
/*
* If checksums are enabled , heap_prune_satisfies_vacuum ( ) may have caused
* an FPI to be emitted .
*/
hint_bit_fpi = fpi_before != pgWalUsage.wal_fpi;
/*
* Process HOT chains .
*
* We added the items to the array starting from ' maxoff ' , so by
* processing the array in reverse order , we process the items in
* ascending offset number order . The order doesn ' t matter for
* correctness , but some quick micro - benchmarking suggests that this is
* faster . ( Earlier PostgreSQL versions , which scanned all the items on
* the page instead of using the root_items array , also did it in
* ascending offset number order . )
*/
for (int i = prstate.nroot_items - 1 ; i >= 0 ; i--)
{
offnum = prstate.root_items[i];
/* Ignore items already processed as part of an earlier chain */
if (prstate.processed[offnum])
continue ;
/* see preceding loop */
*off_loc = offnum;
/* Process this item or chain of items */
heap_prune_chain(page, blockno, maxoff, offnum, &prstate);
}
/*
* Process any heap - only tuples that were not already processed as part of
* a HOT chain .
*/
for (int i = prstate.nheaponly_items - 1 ; i >= 0 ; i--)
{
offnum = prstate.heaponly_items[i];
if (prstate.processed[offnum])
continue ;
/* see preceding loop */
*off_loc = offnum;
/*
* If the tuple is DEAD and doesn ' t chain to anything else , mark it
* unused . ( If it does chain , we can only remove it as part of
* pruning its chain . )
*
* We need this primarily to handle aborted HOT updates , that is ,
* XMIN_INVALID heap - only tuples . Those might not be linked to by any
* chain , since the parent tuple might be re - updated before any
* pruning occurs . So we have to be able to reap them separately from
* chain - pruning . ( Note that HeapTupleHeaderIsHotUpdated will never
* return true for an XMIN_INVALID tuple , so this code will work even
* when there were sequential updates within the aborted transaction . )
*/
if (prstate.htsv[offnum] == HEAPTUPLE_DEAD)
{
ItemId itemid = PageGetItemId(page, offnum);
HeapTupleHeader htup = (HeapTupleHeader) PageGetItem(page, itemid);
if (likely(!HeapTupleHeaderIsHotUpdated(htup)))
{
HeapTupleHeaderAdvanceConflictHorizon(htup,
&prstate.latest_xid_removed);
heap_prune_record_unused(&prstate, offnum, true );
}
else
{
/*
* This tuple should ' ve been processed and removed as part of
* a HOT chain , so something ' s wrong . To preserve evidence ,
* we don ' t dare to remove it . We cannot leave behind a DEAD
* tuple either , because that will cause VACUUM to error out .
* Throwing an error with a distinct error message seems like
* the least bad option .
*/
elog(ERROR, "dead heap-only tuple (%u, %d) is not linked to from any HOT chain" ,
blockno, offnum);
}
}
else
heap_prune_record_unchanged_lp_normal(page, &prstate, offnum);
}
/* We should now have processed every tuple exactly once */
#ifdef USE_ASSERT_CHECKING
for (offnum = FirstOffsetNumber;
offnum <= maxoff;
offnum = OffsetNumberNext(offnum))
{
*off_loc = offnum;
Assert(prstate.processed[offnum]);
}
#endif
/* Clear the offset information once we have processed the given page. */
*off_loc = InvalidOffsetNumber;
do_prune = prstate.nredirected > 0 ||
prstate.ndead > 0 ||
prstate.nunused > 0 ;
/*
* Even if we don ' t prune anything , if we found a new value for the
* pd_prune_xid field or the page was marked full , we will update the hint
* bit .
*/
do_hint = ((PageHeader) page)->pd_prune_xid != prstate.new_prune_xid ||
PageIsFull(page);
/*
* Decide if we want to go ahead with freezing according to the freeze
* plans we prepared , or not .
*/
do_freeze = false ;
if (prstate.freeze)
{
if (prstate.pagefrz.freeze_required)
{
/*
* heap_prepare_freeze_tuple indicated that at least one XID / MXID
* from before FreezeLimit / MultiXactCutoff is present . Must
* freeze to advance relfrozenxid / relminmxid .
*/
do_freeze = true ;
}
else
{
/*
* Opportunistically freeze the page if we are generating an FPI
* anyway and if doing so means that we can set the page
* all - frozen afterwards ( might not happen until VACUUM ' s final
* heap pass ) .
*
* XXX : Previously , we knew if pruning emitted an FPI by checking
* pgWalUsage . wal_fpi before and after pruning . Once the freeze
* and prune records were combined , this heuristic couldn ' t be
* used anymore . The opportunistic freeze heuristic must be
* improved ; however , for now , try to approximate the old logic .
*/
if (prstate.all_visible && prstate.all_frozen && prstate.nfrozen > 0 )
{
/*
* Freezing would make the page all - frozen . Have already
* emitted an FPI or will do so anyway ?
*/
if (RelationNeedsWAL(relation))
{
if (hint_bit_fpi)
do_freeze = true ;
else if (do_prune)
{
if (XLogCheckBufferNeedsBackup(buffer))
do_freeze = true ;
}
else if (do_hint)
{
if (XLogHintBitIsNeeded() && XLogCheckBufferNeedsBackup(buffer))
do_freeze = true ;
}
}
}
}
}
if (do_freeze)
{
/*
* Validate the tuples we will be freezing before entering the
* critical section .
*/
heap_pre_freeze_checks(buffer, prstate.frozen, prstate.nfrozen);
}
else if (prstate.nfrozen > 0 )
{
/*
* The page contained some tuples that were not already frozen , and we
* chose not to freeze them now . The page won ' t be all - frozen then .
*/
Assert(!prstate.pagefrz.freeze_required);
prstate.all_frozen = false ;
prstate.nfrozen = 0 ; /* avoid miscounts in instrumentation */
}
else
{
/*
* We have no freeze plans to execute . The page might already be
* all - frozen ( perhaps only following pruning ) , though . Such pages
* can be marked all - frozen in the VM by our caller , even though none
* of its tuples were newly frozen here .
*/
}
/* Any error while applying the changes is critical */
START_CRIT_SECTION();
if (do_hint)
{
/*
* Update the page ' s pd_prune_xid field to either zero , or the lowest
* XID of any soon - prunable tuple .
*/
((PageHeader) page)->pd_prune_xid = prstate.new_prune_xid;
/*
* Also clear the " page is full " flag , since there ' s no point in
* repeating the prune / defrag process until something else happens to
* the page .
*/
PageClearFull(page);
/*
* If that ' s all we had to do to the page , this is a non - WAL - logged
* hint . If we are going to freeze or prune the page , we will mark
* the buffer dirty below .
*/
if (!do_freeze && !do_prune)
MarkBufferDirtyHint(buffer, true );
}
if (do_prune || do_freeze)
{
/* Apply the planned item changes and repair page fragmentation. */
if (do_prune)
{
heap_page_prune_execute(buffer, false ,
prstate.redirected, prstate.nredirected,
prstate.nowdead, prstate.ndead,
prstate.nowunused, prstate.nunused);
}
if (do_freeze)
heap_freeze_prepared_tuples(buffer, prstate.frozen, prstate.nfrozen);
MarkBufferDirty(buffer);
/*
* Emit a WAL XLOG_HEAP2_PRUNE_FREEZE record showing what we did
*/
if (RelationNeedsWAL(relation))
{
/*
* The snapshotConflictHorizon for the whole record should be the
* most conservative of all the horizons calculated for any of the
* possible modifications . If this record will prune tuples , any
* transactions on the standby older than the youngest xmax of the
* most recently removed tuple this record will prune will
* conflict . If this record will freeze tuples , any transactions
* on the standby with xids older than the youngest tuple this
* record will freeze will conflict .
*/
TransactionId frz_conflict_horizon = InvalidTransactionId;
TransactionId conflict_xid;
/*
* We can use the visibility_cutoff_xid as our cutoff for
* conflicts when the whole page is eligible to become all - frozen
* in the VM once we ' re done with it . Otherwise we generate a
* conservative cutoff by stepping back from OldestXmin .
*/
if (do_freeze)
{
if (prstate.all_visible && prstate.all_frozen)
frz_conflict_horizon = prstate.visibility_cutoff_xid;
else
{
/* Avoids false conflicts when hot_standby_feedback in use */
frz_conflict_horizon = prstate.cutoffs->OldestXmin;
TransactionIdRetreat(frz_conflict_horizon);
}
}
if (TransactionIdFollows(frz_conflict_horizon, prstate.latest_xid_removed))
conflict_xid = frz_conflict_horizon;
else
conflict_xid = prstate.latest_xid_removed;
log_heap_prune_and_freeze(relation, buffer,
conflict_xid,
true , reason,
prstate.frozen, prstate.nfrozen,
prstate.redirected, prstate.nredirected,
prstate.nowdead, prstate.ndead,
prstate.nowunused, prstate.nunused);
}
}
END_CRIT_SECTION();
/* Copy information back for caller */
presult->ndeleted = prstate.ndeleted;
presult->nnewlpdead = prstate.ndead;
presult->nfrozen = prstate.nfrozen;
presult->live_tuples = prstate.live_tuples;
presult->recently_dead_tuples = prstate.recently_dead_tuples;
/*
* It was convenient to ignore LP_DEAD items in all_visible earlier on to
* make the choice of whether or not to freeze the page unaffected by the
* short - term presence of LP_DEAD items . These LP_DEAD items were
* effectively assumed to be LP_UNUSED items in the making . It doesn ' t
* matter which vacuum heap pass ( initial pass or final pass ) ends up
* setting the page all - frozen , as long as the ongoing VACUUM does it .
*
* Now that freezing has been finalized , unset all_visible if there are
* any LP_DEAD items on the page . It needs to reflect the present state
* of the page , as expected by our caller .
*/
if (prstate.all_visible && prstate.lpdead_items == 0 )
{
presult->all_visible = prstate.all_visible;
presult->all_frozen = prstate.all_frozen;
}
else
{
presult->all_visible = false ;
presult->all_frozen = false ;
}
presult->hastup = prstate.hastup;
/*
* For callers planning to update the visibility map , the conflict horizon
* for that record must be the newest xmin on the page . However , if the
* page is completely frozen , there can be no conflict and the
* vm_conflict_horizon should remain InvalidTransactionId . This includes
* the case that we just froze all the tuples ; the prune - freeze record
* included the conflict XID already so the caller doesn ' t need it .
*/
if (presult->all_frozen)
presult->vm_conflict_horizon = InvalidTransactionId;
else
presult->vm_conflict_horizon = prstate.visibility_cutoff_xid;
presult->lpdead_items = prstate.lpdead_items;
/* the presult->deadoffsets array was already filled in */
if (prstate.freeze)
{
if (presult->nfrozen > 0 )
{
*new_relfrozen_xid = prstate.pagefrz.FreezePageRelfrozenXid;
*new_relmin_mxid = prstate.pagefrz.FreezePageRelminMxid;
}
else
{
*new_relfrozen_xid = prstate.pagefrz.NoFreezePageRelfrozenXid;
*new_relmin_mxid = prstate.pagefrz.NoFreezePageRelminMxid;
}
}
}
/*
* Perform visibility checks for heap pruning .
*/
static HTSV_Result
heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer)
{
HTSV_Result res;
TransactionId dead_after;
res = HeapTupleSatisfiesVacuumHorizon(tup, buffer, &dead_after);
if (res != HEAPTUPLE_RECENTLY_DEAD)
return res;
/*
* For VACUUM , we must be sure to prune tuples with xmax older than
* OldestXmin - - a visibility cutoff determined at the beginning of
* vacuuming the relation . OldestXmin is used for freezing determination
* and we cannot freeze dead tuples ' xmaxes .
*/
if (prstate->cutoffs &&
TransactionIdIsValid(prstate->cutoffs->OldestXmin) &&
NormalTransactionIdPrecedes(dead_after, prstate->cutoffs->OldestXmin))
return HEAPTUPLE_DEAD;
/*
* Determine whether or not the tuple is considered dead when compared
* with the provided GlobalVisState . On - access pruning does not provide
* VacuumCutoffs . And for vacuum , even if the tuple ' s xmax is not older
* than OldestXmin , GlobalVisTestIsRemovableXid ( ) could find the row dead
* if the GlobalVisState has been updated since the beginning of vacuuming
* the relation .
*/
if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after))
return HEAPTUPLE_DEAD;
return res;
}
/*
* Pruning calculates tuple visibility once and saves the results in an array
* of int8 . See PruneState . htsv for details . This helper function is meant
* to guard against examining visibility status array members which have not
* yet been computed .
*/
static inline HTSV_Result
htsv_get_valid_status(int status)
{
Assert(status >= HEAPTUPLE_DEAD &&
status <= HEAPTUPLE_DELETE_IN_PROGRESS);
return (HTSV_Result) status;
}
/*
* Prune specified line pointer or a HOT chain originating at line pointer .
*
* Tuple visibility information is provided in prstate - > htsv .
*
* If the item is an index - referenced tuple ( i . e . not a heap - only tuple ) ,
* the HOT chain is pruned by removing all DEAD tuples at the start of the HOT
* chain . We also prune any RECENTLY_DEAD tuples preceding a DEAD tuple .
* This is OK because a RECENTLY_DEAD tuple preceding a DEAD tuple is really
* DEAD , our visibility test is just too coarse to detect it .
*
* Pruning must never leave behind a DEAD tuple that still has tuple storage .
* VACUUM isn ' t prepared to deal with that case .
*
* The root line pointer is redirected to the tuple immediately after the
* latest DEAD tuple . If all tuples in the chain are DEAD , the root line
* pointer is marked LP_DEAD . ( This includes the case of a DEAD simple
* tuple , which we treat as a chain of length 1 . )
*
* We don ' t actually change the page here . We just add entries to the arrays in
* prstate showing the changes to be made . Items to be redirected are added
* to the redirected [ ] array ( two entries per redirection ) ; items to be set to
* LP_DEAD state are added to nowdead [ ] ; and items to be set to LP_UNUSED
* state are added to nowunused [ ] . We perform bookkeeping of live tuples ,
* visibility etc . based on what the page will look like after the changes
* applied . All that bookkeeping is performed in the heap_prune_record_ * ( )
* subroutines . The division of labor is that heap_prune_chain ( ) decides the
* fate of each tuple , ie . whether it ' s going to be removed , redirected or
* left unchanged , and the heap_prune_record_ * ( ) subroutines update PruneState
* based on that outcome .
*/
static void
heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff,
OffsetNumber rootoffnum, PruneState *prstate)
{
TransactionId priorXmax = InvalidTransactionId;
ItemId rootlp;
OffsetNumber offnum;
OffsetNumber chainitems[MaxHeapTuplesPerPage];
/*
* After traversing the HOT chain , ndeadchain is the index in chainitems
* of the first live successor after the last dead item .
*/
int ndeadchain = 0 ,
nchain = 0 ;
rootlp = PageGetItemId(page, rootoffnum);
/* Start from the root tuple */
offnum = rootoffnum;
/* while not end of the chain */
for (;;)
{
HeapTupleHeader htup;
ItemId lp;
/* Sanity check (pure paranoia) */
if (offnum < FirstOffsetNumber)
break ;
/*
* An offset past the end of page ' s line pointer array is possible
* when the array was truncated ( original item must have been unused )
*/
if (offnum > maxoff)
break ;
/* If item is already processed, stop --- it must not be same chain */
if (prstate->processed[offnum])
break ;
lp = PageGetItemId(page, offnum);
/*
* Unused item obviously isn ' t part of the chain . Likewise , a dead
* line pointer can ' t be part of the chain . Both of those cases were
* already marked as processed .
*/
Assert(ItemIdIsUsed(lp));
Assert(!ItemIdIsDead(lp));
/*
* If we are looking at the redirected root line pointer , jump to the
* first normal tuple in the chain . If we find a redirect somewhere
* else , stop - - - it must not be same chain .
*/
if (ItemIdIsRedirected(lp))
{
if (nchain > 0 )
break ; /* not at start of chain */
chainitems[nchain++] = offnum;
offnum = ItemIdGetRedirect(rootlp);
continue ;
}
Assert(ItemIdIsNormal(lp));
htup = (HeapTupleHeader) PageGetItem(page, lp);
/*
* Check the tuple XMIN against prior XMAX , if any
*/
if (TransactionIdIsValid(priorXmax) &&
!TransactionIdEquals(HeapTupleHeaderGetXmin(htup), priorXmax))
break ;
/*
* OK , this tuple is indeed a member of the chain .
*/
chainitems[nchain++] = offnum;
switch (htsv_get_valid_status(prstate->htsv[offnum]))
{
case HEAPTUPLE_DEAD:
/* Remember the last DEAD tuple seen */
ndeadchain = nchain;
HeapTupleHeaderAdvanceConflictHorizon(htup,
&prstate->latest_xid_removed);
/* Advance to next chain member */
break ;
case HEAPTUPLE_RECENTLY_DEAD:
/*
* We don ' t need to advance the conflict horizon for
* RECENTLY_DEAD tuples , even if we are removing them . This
* is because we only remove RECENTLY_DEAD tuples if they
* precede a DEAD tuple , and the DEAD tuple must have been
* inserted by a newer transaction than the RECENTLY_DEAD
* tuple by virtue of being later in the chain . We will have
* advanced the conflict horizon for the DEAD tuple .
*/
/*
* Advance past RECENTLY_DEAD tuples just in case there ' s a
* DEAD one after them . We have to make sure that we don ' t
* miss any DEAD tuples , since DEAD tuples that still have
* tuple storage after pruning will confuse VACUUM .
*/
break ;
case HEAPTUPLE_DELETE_IN_PROGRESS:
case HEAPTUPLE_LIVE:
case HEAPTUPLE_INSERT_IN_PROGRESS:
goto process_chain;
default :
elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result" );
goto process_chain;
}
/*
* If the tuple is not HOT - updated , then we are at the end of this
* HOT - update chain .
*/
if (!HeapTupleHeaderIsHotUpdated(htup))
goto process_chain;
/* HOT implies it can't have moved to different partition */
Assert(!HeapTupleHeaderIndicatesMovedPartitions(htup));
/*
* Advance to next chain member .
*/
Assert(ItemPointerGetBlockNumber(&htup->t_ctid) == blockno);
offnum = ItemPointerGetOffsetNumber(&htup->t_ctid);
priorXmax = HeapTupleHeaderGetUpdateXid(htup);
}
if (ItemIdIsRedirected(rootlp) && nchain < 2 )
{
/*
* We found a redirect item that doesn ' t point to a valid follow - on
* item . This can happen if the loop in heap_page_prune_and_freeze ( )
* caused us to visit the dead successor of a redirect item before
* visiting the redirect item . We can clean up by setting the
* redirect item to LP_DEAD state or LP_UNUSED if the caller
* indicated .
*/
heap_prune_record_dead_or_unused(prstate, rootoffnum, false );
return ;
}
process_chain:
if (ndeadchain == 0 )
{
/*
* No DEAD tuple was found , so the chain is entirely composed of
* normal , unchanged tuples . Leave it alone .
*/
int i = 0 ;
if (ItemIdIsRedirected(rootlp))
{
heap_prune_record_unchanged_lp_redirect(prstate, rootoffnum);
i++;
}
for (; i < nchain; i++)
heap_prune_record_unchanged_lp_normal(page, prstate, chainitems[i]);
}
else if (ndeadchain == nchain)
{
/*
* The entire chain is dead . Mark the root line pointer LP_DEAD , and
* fully remove the other tuples in the chain .
*/
heap_prune_record_dead_or_unused(prstate, rootoffnum, ItemIdIsNormal(rootlp));
for (int i = 1 ; i < nchain; i++)
heap_prune_record_unused(prstate, chainitems[i], true );
}
else
{
/*
* We found a DEAD tuple in the chain . Redirect the root line pointer
* to the first non - DEAD tuple , and mark as unused each intermediate
* item that we are able to remove from the chain .
*/
heap_prune_record_redirect(prstate, rootoffnum, chainitems[ndeadchain],
ItemIdIsNormal(rootlp));
for (int i = 1 ; i < ndeadchain; i++)
heap_prune_record_unused(prstate, chainitems[i], true );
/* the rest of tuples in the chain are normal, unchanged tuples */
for (int i = ndeadchain; i < nchain; i++)
heap_prune_record_unchanged_lp_normal(page, prstate, chainitems[i]);
}
}
/* Record lowest soon-prunable XID */
static void
heap_prune_record_prunable(PruneState *prstate, TransactionId xid)
{
/*
* This should exactly match the PageSetPrunable macro . We can ' t store
* directly into the page header yet , so we update working state .
*/
Assert(TransactionIdIsNormal(xid));
if (!TransactionIdIsValid(prstate->new_prune_xid) ||
TransactionIdPrecedes(xid, prstate->new_prune_xid))
prstate->new_prune_xid = xid;
}
/* Record line pointer to be redirected */
static void
heap_prune_record_redirect(PruneState *prstate,
OffsetNumber offnum, OffsetNumber rdoffnum,
bool was_normal)
{
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
/*
* Do not mark the redirect target here . It needs to be counted
* separately as an unchanged tuple .
*/
Assert(prstate->nredirected < MaxHeapTuplesPerPage);
prstate->redirected[prstate->nredirected * 2 ] = offnum;
prstate->redirected[prstate->nredirected * 2 + 1 ] = rdoffnum;
prstate->nredirected++;
/*
* If the root entry had been a normal tuple , we are deleting it , so count
* it in the result . But changing a redirect ( even to DEAD state ) doesn ' t
* count .
*/
if (was_normal)
prstate->ndeleted++;
prstate->hastup = true ;
}
/* Record line pointer to be marked dead */
static void
heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum,
bool was_normal)
{
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
Assert(prstate->ndead < MaxHeapTuplesPerPage);
prstate->nowdead[prstate->ndead] = offnum;
prstate->ndead++;
/*
* Deliberately delay unsetting all_visible until later during pruning .
* Removable dead tuples shouldn ' t preclude freezing the page .
*/
/* Record the dead offset for vacuum */
prstate->deadoffsets[prstate->lpdead_items++] = offnum;
/*
* If the root entry had been a normal tuple , we are deleting it , so count
* it in the result . But changing a redirect ( even to DEAD state ) doesn ' t
* count .
*/
if (was_normal)
prstate->ndeleted++;
}
/*
* Depending on whether or not the caller set mark_unused_now to true , record that a
* line pointer should be marked LP_DEAD or LP_UNUSED . There are other cases in
* which we will mark line pointers LP_UNUSED , but we will not mark line
* pointers LP_DEAD if mark_unused_now is true .
*/
static void
heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum,
bool was_normal)
{
/*
* If the caller set mark_unused_now to true , we can remove dead tuples
* during pruning instead of marking their line pointers dead . Set this
* tuple ' s line pointer LP_UNUSED . We hint that this option is less
* likely .
*/
if (unlikely(prstate->mark_unused_now))
heap_prune_record_unused(prstate, offnum, was_normal);
else
heap_prune_record_dead(prstate, offnum, was_normal);
}
/* Record line pointer to be marked unused */
static void
heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum, bool was_normal)
{
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
Assert(prstate->nunused < MaxHeapTuplesPerPage);
prstate->nowunused[prstate->nunused] = offnum;
prstate->nunused++;
/*
* If the root entry had been a normal tuple , we are deleting it , so count
* it in the result . But changing a redirect ( even to DEAD state ) doesn ' t
* count .
*/
if (was_normal)
prstate->ndeleted++;
}
/*
* Record an unused line pointer that is left unchanged .
*/
static void
heap_prune_record_unchanged_lp_unused(Page page, PruneState *prstate, OffsetNumber offnum)
{
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
}
/*
* Record line pointer that is left unchanged . We consider freezing it , and
* update bookkeeping of tuple counts and page visibility .
*/
static void
heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumber offnum)
{
HeapTupleHeader htup;
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
prstate->hastup = true ; /* the page is not empty */
/*
* The criteria for counting a tuple as live in this block need to match
* what analyze . c ' s acquire_sample_rows ( ) does , otherwise VACUUM and
* ANALYZE may produce wildly different reltuples values , e . g . when there
* are many recently - dead tuples .
*
* The logic here is a bit simpler than acquire_sample_rows ( ) , as VACUUM
* can ' t run inside a transaction block , which makes some cases impossible
* ( e . g . in - progress insert from the same transaction ) .
*
* HEAPTUPLE_DEAD are handled by the other heap_prune_record_ * ( )
* subroutines . They don ' t count dead items like acquire_sample_rows ( )
* does , because we assume that all dead items will become LP_UNUSED
* before VACUUM finishes . This difference is only superficial . VACUUM
* effectively agrees with ANALYZE about DEAD items , in the end . VACUUM
* won ' t remember LP_DEAD items , but only because they ' re not supposed to
* be left behind when it is done . ( Cases where we bypass index vacuuming
* will violate this optimistic assumption , but the overall impact of that
* should be negligible . )
*/
htup = (HeapTupleHeader) PageGetItem(page, PageGetItemId(page, offnum));
switch (prstate->htsv[offnum])
{
case HEAPTUPLE_LIVE:
/*
* Count it as live . Not only is this natural , but it ' s also what
* acquire_sample_rows ( ) does .
*/
prstate->live_tuples++;
/*
* Is the tuple definitely visible to all transactions ?
*
* NB : Like with per - tuple hint bits , we can ' t set the
* PD_ALL_VISIBLE flag if the inserter committed asynchronously .
* See SetHintBits for more info . Check that the tuple is hinted
* xmin - committed because of that .
*/
if (prstate->all_visible)
{
TransactionId xmin;
if (!HeapTupleHeaderXminCommitted(htup))
{
prstate->all_visible = false ;
break ;
}
/*
* 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 .
*/
xmin = HeapTupleHeaderGetXmin(htup);
/*
* For now always use prstate - > cutoffs for this test , because
* we only update ' all_visible ' 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->all_visible = false ;
break ;
}
/* Track newest xmin on page. */
if (TransactionIdFollows(xmin, prstate->visibility_cutoff_xid) &&
TransactionIdIsNormal(xmin))
prstate->visibility_cutoff_xid = xmin;
}
break ;
case HEAPTUPLE_RECENTLY_DEAD:
prstate->recently_dead_tuples++;
prstate->all_visible = false ;
/*
* This tuple will soon become DEAD . Update the hint field so
* that the page is reconsidered for pruning in future .
*/
heap_prune_record_prunable(prstate,
HeapTupleHeaderGetUpdateXid(htup));
break ;
case HEAPTUPLE_INSERT_IN_PROGRESS:
/*
* We do not count these rows as live , because we expect the
* inserting transaction to update the counters at commit , and we
* assume that will happen only after we report our results . This
* assumption is a bit shaky , but it is what acquire_sample_rows ( )
* does , so be consistent .
*/
prstate->all_visible = false ;
/*
* If we wanted to optimize for aborts , we might consider marking
* the page prunable when we see INSERT_IN_PROGRESS . But we
* don ' t . See related decisions about when to mark the page
* prunable in heapam . c .
*/
break ;
case HEAPTUPLE_DELETE_IN_PROGRESS:
/*
* This an expected case during concurrent vacuum . Count such
* rows as live . As above , we assume the deleting transaction
* will commit and update the counters after we report .
*/
prstate->live_tuples++;
prstate->all_visible = false ;
/*
* This tuple may soon become DEAD . Update the hint field so that
* the page is reconsidered for pruning in future .
*/
heap_prune_record_prunable(prstate,
HeapTupleHeaderGetUpdateXid(htup));
break ;
default :
/*
* DEAD tuples should ' ve been passed to heap_prune_record_dead ( )
* or heap_prune_record_unused ( ) instead .
*/
elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result %d" ,
prstate->htsv[offnum]);
break ;
}
/* Consider freezing any normal tuples which will not be removed */
if (prstate->freeze)
{
bool totally_frozen;
if ((heap_prepare_freeze_tuple(htup,
prstate->cutoffs,
&prstate->pagefrz,
&prstate->frozen[prstate->nfrozen],
&totally_frozen)))
{
/* Save prepared freeze plan for later */
prstate->frozen[prstate->nfrozen++].offset = offnum;
}
/*
* If any tuple isn ' t either totally frozen already or eligible to
* become totally frozen ( according to its freeze plan ) , then the page
* definitely cannot be set all - frozen in the visibility map later on .
*/
if (!totally_frozen)
prstate->all_frozen = false ;
}
}
/*
* Record line pointer that was already LP_DEAD and is left unchanged .
*/
static void
heap_prune_record_unchanged_lp_dead(Page page, PruneState *prstate, OffsetNumber offnum)
{
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
/*
* Deliberately don ' t set hastup for LP_DEAD items . We make the soft
* assumption that any LP_DEAD items encountered here will become
* LP_UNUSED later on , before count_nondeletable_pages is reached . If we
* don ' t make this assumption then rel truncation will only happen every
* other VACUUM , at most . Besides , VACUUM must treat
* hastup / nonempty_pages as provisional no matter how LP_DEAD items are
* handled ( handled here , or handled later on ) .
*
* Similarly , don ' t unset all_visible until later , at the end of
* heap_page_prune_and_freeze ( ) . This will allow us to attempt to freeze
* the page after pruning . As long as we unset it before updating the
* visibility map , this will be correct .
*/
/* Record the dead offset for vacuum */
prstate->deadoffsets[prstate->lpdead_items++] = offnum;
}
/*
* Record LP_REDIRECT that is left unchanged .
*/
static void
heap_prune_record_unchanged_lp_redirect(PruneState *prstate, OffsetNumber offnum)
{
/*
* A redirect line pointer doesn ' t count as a live tuple .
*
* If we leave a redirect line pointer in place , there will be another
* tuple on the page that it points to . We will do the bookkeeping for
* that separately . So we have nothing to do here , except remember that
* we processed this item .
*/
Assert(!prstate->processed[offnum]);
prstate->processed[offnum] = true ;
}
/*
* Perform the actual page changes needed by heap_page_prune_and_freeze ( ) .
*
* If ' lp_truncate_only ' is set , we are merely marking LP_DEAD line pointers
* as unused , not redirecting or removing anything else . The
* PageRepairFragmentation ( ) call is skipped in that case .
*
* If ' lp_truncate_only ' is not set , the caller must hold a cleanup lock on
* the buffer . If it is set , an ordinary exclusive lock suffices .
*/
void
heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused)
{
Page page = (Page) BufferGetPage(buffer);
OffsetNumber *offnum;
HeapTupleHeader htup PG_USED_FOR_ASSERTS_ONLY;
/* Shouldn't be called unless there's something to do */
Assert(nredirected > 0 || ndead > 0 || nunused > 0 );
/* If 'lp_truncate_only', we can only remove already-dead line pointers */
Assert(!lp_truncate_only || (nredirected == 0 && ndead == 0 ));
/* Update all redirected line pointers */
offnum = redirected;
for (int i = 0 ; i < nredirected; i++)
{
OffsetNumber fromoff = *offnum++;
OffsetNumber tooff = *offnum++;
ItemId fromlp = PageGetItemId(page, fromoff);
ItemId tolp PG_USED_FOR_ASSERTS_ONLY;
#ifdef USE_ASSERT_CHECKING
/*
* Any existing item that we set as an LP_REDIRECT ( any ' from ' item )
* must be the first item from a HOT chain . If the item has tuple
* storage then it can ' t be a heap - only tuple . Otherwise we are just
* maintaining an existing LP_REDIRECT from an existing HOT chain that
* has been pruned at least once before now .
*/
if (!ItemIdIsRedirected(fromlp))
{
Assert(ItemIdHasStorage(fromlp) && ItemIdIsNormal(fromlp));
htup = (HeapTupleHeader) PageGetItem(page, fromlp);
Assert(!HeapTupleHeaderIsHeapOnly(htup));
}
else
{
/* We shouldn't need to redundantly set the redirect */
Assert(ItemIdGetRedirect(fromlp) != tooff);
}
/*
* The item that we ' re about to set as an LP_REDIRECT ( the ' from '
* item ) will point to an existing item ( the ' to ' item ) that is
* already a heap - only tuple . There can be at most one LP_REDIRECT
* item per HOT chain .
*
* We need to keep around an LP_REDIRECT item ( after original
* non - heap - only root tuple gets pruned away ) so that it ' s always
* possible for VACUUM to easily figure out what TID to delete from
* indexes when an entire HOT chain becomes dead . A heap - only tuple
* can never become LP_DEAD ; an LP_REDIRECT item or a regular heap
* tuple can .
*
* This check may miss problems , e . g . the target of a redirect could
* be marked as unused subsequently . The page_verify_redirects ( ) check
* below will catch such problems .
*/
tolp = PageGetItemId(page, tooff);
Assert(ItemIdHasStorage(tolp) && ItemIdIsNormal(tolp));
htup = (HeapTupleHeader) PageGetItem(page, tolp);
Assert(HeapTupleHeaderIsHeapOnly(htup));
#endif
ItemIdSetRedirect(fromlp, tooff);
}
/* Update all now-dead line pointers */
offnum = nowdead;
for (int i = 0 ; i < ndead; i++)
{
OffsetNumber off = *offnum++;
ItemId lp = PageGetItemId(page, off);
#ifdef USE_ASSERT_CHECKING
/*
* An LP_DEAD line pointer must be left behind when the original item
* ( which is dead to everybody ) could still be referenced by a TID in
* an index . This should never be necessary with any individual
* heap - only tuple item , though . ( It ' s not clear how much of a problem
* that would be , but there is no reason to allow it . )
*/
if (ItemIdHasStorage(lp))
{
Assert(ItemIdIsNormal(lp));
htup = (HeapTupleHeader) PageGetItem(page, lp);
Assert(!HeapTupleHeaderIsHeapOnly(htup));
}
else
{
/* Whole HOT chain becomes dead */
Assert(ItemIdIsRedirected(lp));
}
#endif
ItemIdSetDead(lp);
}
/* Update all now-unused line pointers */
offnum = nowunused;
for (int i = 0 ; i < nunused; i++)
{
OffsetNumber off = *offnum++;
ItemId lp = PageGetItemId(page, off);
#ifdef USE_ASSERT_CHECKING
if (lp_truncate_only)
{
/* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
}
else
{
/*
* When heap_page_prune_and_freeze ( ) was called , mark_unused_now
* may have been passed as true , which allows would - be LP_DEAD
* items to be made LP_UNUSED instead . This is only possible if
* the relation has no indexes . If there are any dead items , then
* mark_unused_now was not true and every item being marked
* LP_UNUSED must refer to a heap - only tuple .
*/
if (ndead > 0 )
{
Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
htup = (HeapTupleHeader) PageGetItem(page, lp);
Assert(HeapTupleHeaderIsHeapOnly(htup));
}
else
Assert(ItemIdIsUsed(lp));
}
#endif
ItemIdSetUnused(lp);
}
if (lp_truncate_only)
PageTruncateLinePointerArray(page);
else
{
/*
* Finally , repair any fragmentation , and update the page ' s hint bit
* about whether it has free pointers .
*/
PageRepairFragmentation(page);
/*
* Now that the page has been modified , assert that redirect items
* still point to valid targets .
*/
page_verify_redirects(page);
}
}
/*
* If built with assertions , verify that all LP_REDIRECT items point to a
* valid item .
*
* One way that bugs related to HOT pruning show is redirect items pointing to
* removed tuples . It ' s not trivial to reliably check that marking an item
* unused will not orphan a redirect item during heap_prune_chain ( ) /
* heap_page_prune_execute ( ) , so we additionally check the whole page after
* pruning . Without this check such bugs would typically only cause asserts
* later , potentially well after the corruption has been introduced .
*
* Also check comments in heap_page_prune_execute ( ) ' s redirection loop .
*/
static void
page_verify_redirects(Page page)
{
#ifdef USE_ASSERT_CHECKING
OffsetNumber offnum;
OffsetNumber maxoff;
maxoff = PageGetMaxOffsetNumber(page);
for (offnum = FirstOffsetNumber;
offnum <= maxoff;
offnum = OffsetNumberNext(offnum))
{
ItemId itemid = PageGetItemId(page, offnum);
OffsetNumber targoff;
ItemId targitem;
HeapTupleHeader htup;
if (!ItemIdIsRedirected(itemid))
continue ;
targoff = ItemIdGetRedirect(itemid);
targitem = PageGetItemId(page, targoff);
Assert(ItemIdIsUsed(targitem));
Assert(ItemIdIsNormal(targitem));
Assert(ItemIdHasStorage(targitem));
htup = (HeapTupleHeader) PageGetItem(page, targitem);
Assert(HeapTupleHeaderIsHeapOnly(htup));
}
#endif
}
/*
* For all items in this page , find their respective root line pointers .
* If item k is part of a HOT - chain with root at item j , then we set
* root_offsets [ k - 1 ] = j .
*
* The passed - in root_offsets array must have MaxHeapTuplesPerPage entries .
* Unused entries are filled with InvalidOffsetNumber ( zero ) .
*
* The function must be called with at least share lock on the buffer , to
* prevent concurrent prune operations .
*
* Note : The information collected here is valid only as long as the caller
* holds a pin on the buffer . Once pin is released , a tuple might be pruned
* and reused by a completely unrelated tuple .
*/
void
heap_get_root_tuples(Page page, OffsetNumber *root_offsets)
{
OffsetNumber offnum,
maxoff;
MemSet(root_offsets, InvalidOffsetNumber,
MaxHeapTuplesPerPage * sizeof (OffsetNumber));
maxoff = PageGetMaxOffsetNumber(page);
for (offnum = FirstOffsetNumber; offnum <= maxoff; offnum = OffsetNumberNext(offnum))
{
ItemId lp = PageGetItemId(page, offnum);
HeapTupleHeader htup;
OffsetNumber nextoffnum;
TransactionId priorXmax;
/* skip unused and dead items */
if (!ItemIdIsUsed(lp) || ItemIdIsDead(lp))
continue ;
if (ItemIdIsNormal(lp))
{
htup = (HeapTupleHeader) PageGetItem(page, lp);
/*
* Check if this tuple is part of a HOT - chain rooted at some other
* tuple . If so , skip it for now ; we ' ll process it when we find
* its root .
*/
if (HeapTupleHeaderIsHeapOnly(htup))
continue ;
/*
* This is either a plain tuple or the root of a HOT - chain .
* Remember it in the mapping .
*/
root_offsets[offnum - 1 ] = offnum;
/* If it's not the start of a HOT-chain, we're done with it */
if (!HeapTupleHeaderIsHotUpdated(htup))
continue ;
/* Set up to scan the HOT-chain */
nextoffnum = ItemPointerGetOffsetNumber(&htup->t_ctid);
priorXmax = HeapTupleHeaderGetUpdateXid(htup);
}
else
{
/* Must be a redirect item. We do not set its root_offsets entry */
Assert(ItemIdIsRedirected(lp));
/* Set up to scan the HOT-chain */
nextoffnum = ItemIdGetRedirect(lp);
priorXmax = InvalidTransactionId;
}
/*
* Now follow the HOT - chain and collect other tuples in the chain .
*
* Note : Even though this is a nested loop , the complexity of the
* function is O ( N ) because a tuple in the page should be visited not
* more than twice , once in the outer loop and once in HOT - chain
* chases .
*/
for (;;)
{
/* Sanity check (pure paranoia) */
if (offnum < FirstOffsetNumber)
break ;
/*
* An offset past the end of page ' s line pointer array is possible
* when the array was truncated
*/
if (offnum > maxoff)
break ;
lp = PageGetItemId(page, nextoffnum);
/* Check for broken chains */
if (!ItemIdIsNormal(lp))
break ;
htup = (HeapTupleHeader) PageGetItem(page, lp);
if (TransactionIdIsValid(priorXmax) &&
!TransactionIdEquals(priorXmax, HeapTupleHeaderGetXmin(htup)))
break ;
/* Remember the root line pointer for this item */
root_offsets[nextoffnum - 1 ] = offnum;
/* Advance to next chain member, if any */
if (!HeapTupleHeaderIsHotUpdated(htup))
break ;
/* HOT implies it can't have moved to different partition */
Assert(!HeapTupleHeaderIndicatesMovedPartitions(htup));
nextoffnum = ItemPointerGetOffsetNumber(&htup->t_ctid);
priorXmax = HeapTupleHeaderGetUpdateXid(htup);
}
}
}
/*
* Compare fields that describe actions required to freeze tuple with caller ' s
* open plan . If everything matches then the frz tuple plan is equivalent to
* caller ' s plan .
*/
static inline bool
heap_log_freeze_eq(xlhp_freeze_plan *plan, HeapTupleFreeze *frz)
{
if (plan->xmax == frz->xmax &&
plan->t_infomask2 == frz->t_infomask2 &&
plan->t_infomask == frz->t_infomask &&
plan->frzflags == frz->frzflags)
return true ;
/* Caller must call heap_log_freeze_new_plan again for frz */
return false ;
}
/*
* Comparator used to deduplicate the freeze plans used in WAL records .
*/
static int
heap_log_freeze_cmp(const void *arg1, const void *arg2)
{
HeapTupleFreeze *frz1 = (HeapTupleFreeze *) arg1;
HeapTupleFreeze *frz2 = (HeapTupleFreeze *) arg2;
if (frz1->xmax < frz2->xmax)
return -1 ;
else if (frz1->xmax > frz2->xmax)
return 1 ;
if (frz1->t_infomask2 < frz2->t_infomask2)
return -1 ;
else if (frz1->t_infomask2 > frz2->t_infomask2)
return 1 ;
if (frz1->t_infomask < frz2->t_infomask)
return -1 ;
else if (frz1->t_infomask > frz2->t_infomask)
return 1 ;
if (frz1->frzflags < frz2->frzflags)
return -1 ;
else if (frz1->frzflags > frz2->frzflags)
return 1 ;
/*
* heap_log_freeze_eq would consider these tuple - wise plans to be equal .
* ( So the tuples will share a single canonical freeze plan . )
*
* We tiebreak on page offset number to keep each freeze plan ' s page
* offset number array individually sorted . ( Unnecessary , but be tidy . )
*/
if (frz1->offset < frz2->offset)
return -1 ;
else if (frz1->offset > frz2->offset)
return 1 ;
Assert(false );
return 0 ;
}
/*
* Start new plan initialized using tuple - level actions . At least one tuple
* will have steps required to freeze described by caller ' s plan during REDO .
*/
static inline void
heap_log_freeze_new_plan(xlhp_freeze_plan *plan, HeapTupleFreeze *frz)
{
plan->xmax = frz->xmax;
plan->t_infomask2 = frz->t_infomask2;
plan->t_infomask = frz->t_infomask;
plan->frzflags = frz->frzflags;
plan->ntuples = 1 ; /* for now */
}
/*
* Deduplicate tuple - based freeze plans so that each distinct set of
* processing steps is only stored once in the WAL record .
* Called during original execution of freezing ( for logged relations ) .
*
* Return value is number of plans set in * plans_out for caller . Also writes
* an array of offset numbers into * offsets_out output argument for caller
* ( actually there is one array per freeze plan , but that ' s not of immediate
* concern to our caller ) .
*/
static int
heap_log_freeze_plan(HeapTupleFreeze *tuples, int ntuples,
xlhp_freeze_plan *plans_out,
OffsetNumber *offsets_out)
{
int nplans = 0 ;
/* Sort tuple-based freeze plans in the order required to deduplicate */
qsort(tuples, ntuples, sizeof (HeapTupleFreeze), heap_log_freeze_cmp);
for (int i = 0 ; i < ntuples; i++)
{
HeapTupleFreeze *frz = tuples + i;
if (i == 0 )
{
/* New canonical freeze plan starting with first tup */
heap_log_freeze_new_plan(plans_out, frz);
nplans++;
}
else if (heap_log_freeze_eq(plans_out, frz))
{
/* tup matches open canonical plan -- include tup in it */
Assert(offsets_out[i - 1 ] < frz->offset);
plans_out->ntuples++;
}
else
{
/* Tup doesn't match current plan -- done with it now */
plans_out++;
/* New canonical freeze plan starting with this tup */
heap_log_freeze_new_plan(plans_out, frz);
nplans++;
}
/*
* Save page offset number in dedicated buffer in passing .
*
* REDO routine relies on the record ' s offset numbers array grouping
* offset numbers by freeze plan . The sort order within each grouping
* is ascending offset number order , just to keep things tidy .
*/
offsets_out[i] = frz->offset;
}
Assert(nplans > 0 && nplans <= ntuples);
return nplans;
}
/*
* Write an XLOG_HEAP2_PRUNE_FREEZE WAL record
*
* This is used for several different page maintenance operations :
*
* - Page pruning , in VACUUM ' s 1 st pass or on access : Some items are
* redirected , some marked dead , and some removed altogether .
*
* - Freezing : Items are marked as ' frozen ' .
*
* - Vacuum , 2 nd pass : Items that are already LP_DEAD are marked as unused .
*
* They have enough commonalities that we use a single WAL record for them
* all .
*
* If replaying the record requires a cleanup lock , pass cleanup_lock = true .
* Replaying ' redirected ' or ' dead ' items always requires a cleanup lock , but
* replaying ' unused ' items depends on whether they were all previously marked
* as dead .
*
* Note : This function scribbles on the ' frozen ' array .
*
* Note : This is called in a critical section , so careful what you do here .
*/
void
log_heap_prune_and_freeze(Relation relation, Buffer buffer,
TransactionId conflict_xid,
bool cleanup_lock,
PruneReason reason,
HeapTupleFreeze *frozen, int nfrozen,
OffsetNumber *redirected, int nredirected,
OffsetNumber *dead, int ndead,
OffsetNumber *unused, int nunused)
{
xl_heap_prune xlrec;
XLogRecPtr recptr;
uint8 info;
/* The following local variables hold data registered in the WAL record: */
xlhp_freeze_plan plans[MaxHeapTuplesPerPage];
xlhp_freeze_plans freeze_plans;
xlhp_prune_items redirect_items;
xlhp_prune_items dead_items;
xlhp_prune_items unused_items;
OffsetNumber frz_offsets[MaxHeapTuplesPerPage];
xlrec.flags = 0 ;
/*
* Prepare data for the buffer . The arrays are not actually in the
* buffer , but we pretend that they are . When XLogInsert stores a full
* page image , the arrays can be omitted .
*/
XLogBeginInsert();
XLogRegisterBuffer(0 , buffer, REGBUF_STANDARD);
if (nfrozen > 0 )
{
int nplans;
xlrec.flags |= XLHP_HAS_FREEZE_PLANS;
/*
* Prepare deduplicated representation for use in the WAL record . This
* destructively sorts frozen tuples array in - place .
*/
nplans = heap_log_freeze_plan(frozen, nfrozen, plans, frz_offsets);
freeze_plans.nplans = nplans;
XLogRegisterBufData(0 , &freeze_plans,
offsetof(xlhp_freeze_plans, plans));
XLogRegisterBufData(0 , plans,
sizeof (xlhp_freeze_plan) * nplans);
}
if (nredirected > 0 )
{
xlrec.flags |= XLHP_HAS_REDIRECTIONS;
redirect_items.ntargets = nredirected;
XLogRegisterBufData(0 , &redirect_items,
offsetof(xlhp_prune_items, data));
XLogRegisterBufData(0 , redirected,
sizeof (OffsetNumber[2 ]) * nredirected);
}
if (ndead > 0 )
{
xlrec.flags |= XLHP_HAS_DEAD_ITEMS;
dead_items.ntargets = ndead;
XLogRegisterBufData(0 , &dead_items,
offsetof(xlhp_prune_items, data));
XLogRegisterBufData(0 , dead,
sizeof (OffsetNumber) * ndead);
}
if (nunused > 0 )
{
xlrec.flags |= XLHP_HAS_NOW_UNUSED_ITEMS;
unused_items.ntargets = nunused;
XLogRegisterBufData(0 , &unused_items,
offsetof(xlhp_prune_items, data));
XLogRegisterBufData(0 , unused,
sizeof (OffsetNumber) * nunused);
}
if (nfrozen > 0 )
XLogRegisterBufData(0 , frz_offsets,
sizeof (OffsetNumber) * nfrozen);
/*
* Prepare the main xl_heap_prune record . We already set the XLHP_HAS_ *
* flag above .
*/
if (RelationIsAccessibleInLogicalDecoding(relation))
xlrec.flags |= XLHP_IS_CATALOG_REL;
if (TransactionIdIsValid(conflict_xid))
xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
if (cleanup_lock)
xlrec.flags |= XLHP_CLEANUP_LOCK;
else
{
Assert(nredirected == 0 && ndead == 0 );
/* also, any items in 'unused' must've been LP_DEAD previously */
}
XLogRegisterData(&xlrec, SizeOfHeapPrune);
if (TransactionIdIsValid(conflict_xid))
XLogRegisterData(&conflict_xid, sizeof (TransactionId));
switch (reason)
{
case PRUNE_ON_ACCESS:
info = XLOG_HEAP2_PRUNE_ON_ACCESS;
break ;
case PRUNE_VACUUM_SCAN:
info = XLOG_HEAP2_PRUNE_VACUUM_SCAN;
break ;
case PRUNE_VACUUM_CLEANUP:
info = XLOG_HEAP2_PRUNE_VACUUM_CLEANUP;
break ;
default :
elog(ERROR, "unrecognized prune reason: %d" , (int ) reason);
break ;
}
recptr = XLogInsert(RM_HEAP2_ID, info);
PageSetLSN(BufferGetPage(buffer), recptr);
}
Messung V0.5 in Prozent C=93 H=91 G=91
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.93Angebot
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-08)
¤
*Eine klare Vorstellung vom Zielzustand
2026-08-25
Neuigkeiten
Aktuelles
Motto des Tages
Open Source Software
Jenseits des Üblichen ....
Besucherstatistik
Statistik
#Sources=277311
#Domains=752002