From 4e0cffbfd728213677fa9f15800f4ac6a4607503 Mon Sep 17 00:00:00 2001 Message-ID: <4e0cffbfd728213677fa9f15800f4ac6a4607503.1783875123.git.darthunix@gmail.com> From: Denis Smirnov 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