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