From 4e0cffbfd728213677fa9f15800f4ac6a4607503 Mon Sep 17 00:00:00 2001
Message-ID: <4e0cffbfd728213677fa9f15800f4ac6a4607503.1783875123.git.darthunix@gmail.com>
From: Denis Smirnov <darthunix@gmail.com>
Date: Sun, 12 Jul 2026 21:48:00 +0800
Subject: [PATCH v1] Avoid repeated heapgettup_pagemode() calls within a heap
 page

heap_getnextslot() currently calls heapgettup_pagemode() for every
tuple, even though page mode has already collected the offsets of all
visible tuples on the current page. This repeats tuple-at-a-time scan
bookkeeping that can become noticeable for analytical workloads
dominated by sequential heap scans.

Add HeapPageBatch to BufferHeapTupleTableSlot. It references the
visible tuple offsets collected by the heap scan and records which one
is currently stored in the slot. After the first tuple from a page,
heap_getnextslot() can advance directly through those offsets, update
rs_ctup and invalidate the slot's tuple deformation state. The regular
heap scan path is used again when the page is exhausted.

Use this fast path only for page-mode scans without scan keys. Scans
using non-MVCC snapshots or heap scan keys retain the existing behavior.
Clear the page batch whenever the slot is cleared, materialized, or
assigned another tuple. The batch does not require additional buffer
pins and is valid only while the scan and slot remain on the same
buffer.

This reduces per-tuple overhead while preserving the existing
tuple-at-a-time executor interface. It also provides initial
infrastructure for passing page-sized groups of heap tuples to
batch-aware executor nodes in the future.

Discussion: https://postgr.es/m/3D8D1BA5-B8A6-400A-B605-42EEE37890B4%40gmail.com
---
 src/backend/access/heap/heapam.c  | 84 +++++++++++++++++++++++++++++++
 src/backend/executor/execTuples.c | 12 +++++
 src/include/executor/tuptable.h   |  8 +++
 src/tools/pgindent/typedefs.list  |  1 +
 4 files changed, 105 insertions(+)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9cdc221675b..e0a620cd172 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -44,6 +44,7 @@
 #include "catalog/pg_database_d.h"
 #include "commands/vacuum.h"
 #include "executor/instrument_node.h"
+#include "executor/tuptable.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
 #include "storage/lmgr.h"
@@ -1470,12 +1471,85 @@ heap_getnext(TableScanDesc sscan, ScanDirection direction)
 	return &scan->rs_ctup;
 }
 
+static inline void
+AssertHeapPageBatchValid(HeapScanDesc scan, TupleTableSlot *slot)
+{
+#ifdef USE_ASSERT_CHECKING
+	BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
+	HeapPageBatch *batch = &bslot->batch;
+
+	Assert(!TTS_EMPTY(slot));
+	Assert(batch->offsets != NULL);
+	Assert(batch->offsets == scan->rs_vistuples);
+	Assert(batch->ntuples == scan->rs_ntuples);
+	Assert(batch->current == scan->rs_cindex);
+	Assert(batch->current >= 0);
+	Assert(batch->current < batch->ntuples);
+	Assert(batch->offsets[batch->current] ==
+		   ItemPointerGetOffsetNumber(&slot->tts_tid));
+	Assert(bslot->buffer == scan->rs_cbuf);
+	Assert(bslot->base.tuple == &scan->rs_ctup);
+#endif
+}
+
+static bool
+heap_page_batch_next(HeapScanDesc scan, ScanDirection direction,
+					 TupleTableSlot *slot)
+{
+	BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
+	HeapPageBatch *batch = &bslot->batch;
+	HeapTuple	tuple = &scan->rs_ctup;
+	Page		page;
+	ItemId		lpp;
+	OffsetNumber lineoff;
+	int			next;
+
+	if (batch->offsets == NULL)
+		return false;
+
+	if (unlikely(batch->current != scan->rs_cindex ||
+				 bslot->buffer != scan->rs_cbuf))
+		return false;
+
+	Assert(direction == ForwardScanDirection ||
+		   direction == BackwardScanDirection);
+	AssertHeapPageBatchValid(scan, slot);
+
+	next = batch->current + direction;
+	if (next < 0 || next >= batch->ntuples)
+		return false;
+
+	page = BufferGetPage(scan->rs_cbuf);
+	lineoff = batch->offsets[next];
+	lpp = PageGetItemId(page, lineoff);
+	Assert(ItemIdIsNormal(lpp));
+
+	tuple->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+	tuple->t_len = ItemIdGetLength(lpp);
+	ItemPointerSetOffsetNumber(&tuple->t_self, lineoff);
+
+	scan->rs_cindex = next;
+	batch->current = next;
+	bslot->base.tuple = tuple;
+	bslot->base.off = 0;
+	slot->tts_nvalid = 0;
+	slot->tts_tid = tuple->t_self;
+
+	return true;
+}
+
 bool
 heap_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *slot)
 {
+	BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
 	HeapScanDesc scan = (HeapScanDesc) sscan;
 
 	/* Note: no locking manipulations needed */
+	if (heap_page_batch_next(scan, direction, slot))
+	{
+		pgstat_count_heap_getnext(scan->rs_base.rs_rd);
+		return true;
+	}
 
 	if (sscan->rs_flags & SO_ALLOW_PAGEMODE)
 		heapgettup_pagemode(scan, direction, sscan->rs_nkeys, sscan->rs_key);
@@ -1497,6 +1571,16 @@ heap_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *s
 
 	ExecStoreBufferHeapTuple(&scan->rs_ctup, slot,
 							 scan->rs_cbuf);
+
+	if ((sscan->rs_flags & SO_ALLOW_PAGEMODE) && sscan->rs_nkeys == 0)
+	{
+		bslot->batch.offsets = scan->rs_vistuples;
+		bslot->batch.ntuples = scan->rs_ntuples;
+		bslot->batch.current = scan->rs_cindex;
+
+		AssertHeapPageBatchValid(scan, slot);
+	}
+
 	return true;
 }
 
diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c
index 97ae019d10a..b4431734c3f 100644
--- a/src/backend/executor/execTuples.c
+++ b/src/backend/executor/execTuples.c
@@ -707,6 +707,14 @@ tts_minimal_store_tuple(TupleTableSlot *slot, MinimalTuple mtup, bool shouldFree
  * TupleTableSlotOps implementation for BufferHeapTupleTableSlot.
  */
 
+static inline void
+heap_page_batch_reset(HeapPageBatch *batch)
+{
+	batch->offsets = NULL;
+	batch->ntuples = 0;
+	batch->current = 0;
+}
+
 static void
 tts_buffer_heap_init(TupleTableSlot *slot)
 {
@@ -745,6 +753,7 @@ tts_buffer_heap_clear(TupleTableSlot *slot)
 	bslot->base.tuple = NULL;
 	bslot->base.off = 0;
 	bslot->buffer = InvalidBuffer;
+	heap_page_batch_reset(&bslot->batch);
 }
 
 static void
@@ -807,6 +816,7 @@ tts_buffer_heap_materialize(TupleTableSlot *slot)
 	MemoryContext oldContext;
 
 	Assert(!TTS_EMPTY(slot));
+	heap_page_batch_reset(&bslot->batch);
 
 	/* If slot has its tuple already materialized, nothing to do. */
 	if (TTS_SHOULDFREE(slot))
@@ -946,6 +956,8 @@ tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple,
 {
 	BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
 
+	heap_page_batch_reset(&bslot->batch);
+
 	if (TTS_SHOULDFREE(slot))
 	{
 		/* materialized slot shouldn't have a buffer to release */
diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h
index 3db6c9c9bd0..4ec94d2b84b 100644
--- a/src/include/executor/tuptable.h
+++ b/src/include/executor/tuptable.h
@@ -282,6 +282,13 @@ typedef struct HeapTupleTableSlot
 	HeapTupleData tupdata;		/* optional workspace for storing tuple */
 } HeapTupleTableSlot;
 
+typedef struct HeapPageBatch
+{
+	const OffsetNumber *offsets;	/* visible offsets on slot's page, or NULL */
+	int			ntuples;		/* number of offsets */
+	int			current;		/* index of slot's tuple in offsets */
+} HeapPageBatch;
+
 /* heap tuple residing in a buffer */
 typedef struct BufferHeapTupleTableSlot
 {
@@ -296,6 +303,7 @@ typedef struct BufferHeapTupleTableSlot
 	 * such a case, since presumably base.tuple is pointing into the buffer.)
 	 */
 	Buffer		buffer;			/* tuple's buffer, or InvalidBuffer */
+	HeapPageBatch batch;
 } BufferHeapTupleTableSlot;
 
 typedef struct MinimalTupleTableSlot
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..e843e50ca5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1266,6 +1266,7 @@ HeadlineParsedText
 HeadlineWordEntry
 HeapCheckContext
 HeapCheckReadStreamData
+HeapPageBatch
 HeapPageFreeze
 HeapScanDesc
 HeapScanDescData
-- 
2.54.0

