On Fri, Sep 11, 2026 at 1:24 PM Melanie Plageman
<[email protected]> wrote:
>
> On Thu, Sep 10, 2026 at 3:25 PM Melanie Plageman
> <[email protected]> wrote:
>
> Here is a patch for this. While writing it, I realized I do not
> understand why we don't update the FSM when restoring an FPI --
> especially for COPY FREEZE. The FSM won't be up-to-date after
> promotion for those pages and vacuum won't scan them so they'll remain
> that way forever. Obviously not something to change in backbranches,
> but it strikes me as odd that we do that in any cases -- but
> especially for COPY FREEZE.

Correcting myself here: pre-19 COPY FREEZE relied on
XLOG_HEAP2_VISIBLE records to update the FSM when the heap pages in
the XLOG_HEAP2_MULTI_INSERT had heap page FPIs. But my question
remains, what is the point of skipping FSM updates when there is a
heap page FPI?

On another note, an LLM found a bug in the commit in this series that
set pd_prune_xid for multi-inserts. I set pd_prune_xid as long as the
page wasn't being set all-frozen, but you can insert frozen tuples
into a page when you are not then setting it all-frozen. In that case,
you do not want to set pd_prune_xid. The worst that can happen is a
spurious round of pruning for the page, but it's best to avoid this
(and it wasn't the intent). Inserting a frozen tuple should not set a
prune hint. Patch attached. Simple repro below:

CREATE EXTENSION pageinspect;
CREATE EXTENSION pg_visibility;
BEGIN;
CREATE TABLE t (id integer);
COPY t FROM PROGRAM 'seq 1 3000' FREEZE;
COMMIT;
SELECT g AS blkno, (page_header(get_raw_page('t', g))).prune_xid,
vm.all_visible, vm.all_frozen FROM generate_series( 0,
pg_relation_size('t') / current_setting('block_size')::integer - 1) AS
g JOIN pg_visibility_map('t') AS vm ON vm.blkno = g ORDER BY g;

- Melanie
From c9430477f9323903f94359461793df61c91c6a1c Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email protected]>
Date: Fri, 11 Sep 2026 15:55:34 -0400
Subject: [PATCH v1] Avoid setting pd_prune_xid when inserting frozen tuples

378a216187a set pd_prune_xid on inserts so that on-access pruning could
later set the pages all-visible. heap_multi_insert() skipped this only
when setting the page all-frozen, but it can insert frozen tuples even
when not setting the page all-frozen. Inserting frozen tuples should not
set pd_prune_xid as they introduce no work for on-access pruning. Fix by
only setting pd_prune_xid when inserting non-frozen tuples.
---
 src/backend/access/heap/heapam.c      |  6 +++---
 src/backend/access/heap/heapam_xlog.c | 21 ++++++++++++++++-----
 2 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 72d6541734c..1afd5dc9ca0 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2493,10 +2493,10 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 
 		/*
 		 * Set pd_prune_xid. See heap_insert() for more on why we do this when
-		 * inserting tuples. This only makes sense if we aren't already
-		 * setting the page frozen in the VM and we're not in bootstrap mode.
+		 * inserting tuples. This only makes sense if the tuples aren't frozen
+		 * and we're not in bootstrap mode.
 		 */
-		if (!all_frozen_set && TransactionIdIsNormal(xid))
+		if (TransactionIdIsNormal(xid) && !(options & HEAP_INSERT_FROZEN))
 			PageSetPrunable(page, xid);
 
 		MarkBufferDirty(buffer);
diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c
index 7a7bc7ea740..88f99bb9c5f 100644
--- a/src/backend/access/heap/heapam_xlog.c
+++ b/src/backend/access/heap/heapam_xlog.c
@@ -583,6 +583,7 @@ heap_xlog_multi_insert(XLogReaderState *record)
 		char	   *tupdata;
 		char	   *endptr;
 		Size		len;
+		bool		inserted_tuples_frozen = false;
 
 		/* Tuples are stored as block data */
 		tupdata = XLogRecGetBlockData(record, HEAP_MULTI_INSERT_BLKREF_HEAP,
@@ -630,6 +631,10 @@ heap_xlog_multi_insert(XLogReaderState *record)
 			ItemPointerSetBlockNumber(&htup->t_ctid, blkno);
 			ItemPointerSetOffsetNumber(&htup->t_ctid, offnum);
 
+			/* If one inserted tuple was frozen, they all were */
+			if (i == 0)
+				inserted_tuples_frozen = HeapTupleHeaderXminFrozen(htup);
+
 			offnum = PageAddItem(page, htup, newlen, offnum, true, true);
 			if (offnum == InvalidOffsetNumber)
 				elog(PANIC, "failed to add tuple");
@@ -645,17 +650,23 @@ heap_xlog_multi_insert(XLogReaderState *record)
 			PageClearAllVisible(page);
 
 		/*
-		 * XLH_INSERT_ALL_FROZEN_SET implies that all tuples are visible. If
-		 * we are not setting the page frozen, then set the page's prunable
-		 * hint so that we trigger on-access pruning later which may set the
-		 * page all-visible in the VM.
+		 * XLH_INSERT_ALL_FROZEN_SET implies that all tuples are visible, so
+		 * set PD_ALL_VISIBLE and clear pd_prune_xid.
+		 *
+		 * If the page isn't being set all-frozen and we aren't inserting
+		 * frozen tuples, set pd_prune_xid so that the page gets on-access
+		 * pruned.
+		 *
+		 * Frozen tuples may be added to an already all-frozen page or to a
+		 * page containing non-frozen tuples, but they introduce nothing new
+		 * for on-access pruning, so preserve the existing hint.
 		 */
 		if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET)
 		{
 			PageSetAllVisible(page);
 			PageClearPrunable(page);
 		}
-		else
+		else if (!inserted_tuples_frozen)
 			PageSetPrunable(page, XLogRecGetXid(record));
 
 		MarkBufferDirty(buffer);
-- 
2.43.0

Reply via email to