From 59baf8c1261da4518020e296480b4bc1dd71c31e Mon Sep 17 00:00:00 2001 Message-ID: <59baf8c1261da4518020e296480b4bc1dd71c31e.1785332874.git.darthunix@gmail.com> From: Denis Smirnov Date: Sun, 19 Jul 2026 16:45:48 +0700 Subject: [PATCH v4 1/3] Expose heap page batches through tuple slots Heap scans in page mode already collect the offsets of all visible tuples on the current page. Nevertheless, heap_getnextslot() calls heapgettup_pagemode() separately for every tuple, repeating scan-navigation work that has already been done. Add an optional batch interface to TupleTableSlot and implement it for buffer heap slots. A heap scan can publish the visible tuples from its current page as a batch and advance through them without calling heapgettup_pagemode() again. The page remains pinned as before, and the executor and table AM interfaces continue to return one tuple at a time. The slot interface also allows attributes to be obtained from a selected window of batch rows. This provides a foundation for processing heap tuples in tight loops without exposing heap page layout to executor nodes or requiring batch-aware table AM callbacks. Track each batch's generation so that rescans and slot reuse cannot consume stale contents. A non-NULL batch interface promises that attributes remain stable while the batch is active; producers whose attributes can change in place must leave tts_batch NULL. Batches are published only for page-mode scans without scan keys. Buffer heap slots expose the batch interface only when explicitly requested. SeqScan makes that request under an MVCC snapshot, and the slot implementation decides whether to accept it. Relations known to receive in-place updates and user catalog tables are excluded. --- src/backend/access/heap/heapam.c | 91 ++++++++++++++++++++++++++++++ src/backend/access/index/genam.c | 6 ++ src/backend/executor/execTuples.c | 82 ++++++++++++++++++++++++--- src/backend/executor/nodeSeqscan.c | 16 +++++- src/include/executor/tuptable.h | 80 ++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 3 + 6 files changed, 268 insertions(+), 10 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 8b488cfd8f6..74b8e636bd2 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -1471,6 +1471,75 @@ heap_getnext(TableScanDesc sscan, ScanDirection direction) return &scan->rs_ctup; } +static inline bool +heap_page_batch_is_valid(HeapScanDesc scan, TupleTableSlot *slot) +{ + BufferHeapTupleTableSlot *bslot; + HeapPageBatch *batch; + + if (!TTS_IS_BUFFERTUPLE(slot)) + return false; + + bslot = (BufferHeapTupleTableSlot *) slot; + batch = &bslot->batch; + if (slot_getbatch(slot) != &batch->batch || + batch->offsets != scan->rs_vistuples || + batch->batch.ntuples != (int) scan->rs_ntuples || + batch->batch.current != (int) scan->rs_cindex || + bslot->buffer != scan->rs_cbuf) + return false; + + Assert(!TTS_EMPTY(slot)); + Assert(ItemPointerIsValid(&slot->tts_tid)); + Assert(batch->offsets[batch->batch.current] == + ItemPointerGetOffsetNumber(&slot->tts_tid)); + Assert(bslot->base.tuple == &scan->rs_ctup); + + return true; +} + +static inline bool +heap_page_batch_next(HeapScanDesc scan, ScanDirection direction, + TupleTableSlot *slot) +{ + BufferHeapTupleTableSlot *bslot; + HeapPageBatch *batch; + HeapTuple tuple = &scan->rs_ctup; + Page page; + ItemId lpp; + OffsetNumber lineoff; + int next; + + Assert(direction == ForwardScanDirection || + direction == BackwardScanDirection); + + if (!heap_page_batch_is_valid(scan, slot)) + return false; + + bslot = (BufferHeapTupleTableSlot *) slot; + batch = &bslot->batch; + next = batch->batch.current + direction; + if (next < 0 || next >= batch->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->batch.current = next; + tts_buffer_heap_set_tuple(bslot, tuple); + + Assert(heap_page_batch_is_valid(scan, slot)); + + return true; +} + bool heap_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *slot) { @@ -1478,6 +1547,12 @@ heap_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *s /* 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); else @@ -1498,6 +1573,22 @@ heap_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableSlot *s ExecStoreBufferHeapTuple(&scan->rs_ctup, slot, scan->rs_cbuf); + + if (slot_supports_batch(slot) && + (sscan->rs_flags & SO_ALLOW_PAGEMODE) && + sscan->rs_nkeys == 0) + { + HeapPageBatch *batch = + &((BufferHeapTupleTableSlot *) slot)->batch; + + batch->offsets = scan->rs_vistuples; + batch->batch.current = scan->rs_cindex; + batch->batch.generation++; + batch->batch.ntuples = scan->rs_ntuples; + + Assert(heap_page_batch_is_valid(scan, slot)); + } + return true; } diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c index 1408989c568..f7e6816fc82 100644 --- a/src/backend/access/index/genam.c +++ b/src/backend/access/index/genam.c @@ -805,6 +805,12 @@ systable_endscan_ordered(SysScanDesc sysscan) * condition.) If "oldtupcopy" gets non-NULL, you must pass output parameter * "state" to systable_inplace_update_finish() or * systable_inplace_update_cancel(). + * + * An extension using this function on a non-system relation must either mark + * the relation with user_catalog_table before concurrent access begins, or + * serialize its in-place updates with all readers. SeqScan does not expose + * tuple batches for user catalog tables, because a batch requires stable + * backing tuple contents. */ void systable_inplace_update_begin(Relation relation, diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index 97ae019d10a..e0903c30aa0 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -64,6 +64,7 @@ #include "catalog/pg_type.h" #include "funcapi.h" #include "nodes/nodeFuncs.h" +#include "port/pg_bitutils.h" #include "storage/bufmgr.h" #include "utils/builtins.h" #include "utils/expandeddatum.h" @@ -707,9 +708,75 @@ tts_minimal_store_tuple(TupleTableSlot *slot, MinimalTuple mtup, bool shouldFree * TupleTableSlotOps implementation for BufferHeapTupleTableSlot. */ +static TupleBatchMask +tts_buffer_heap_getattrs(TupleTableSlot *slot, AttrNumber attnum, + int first, int nrows, TupleBatchMask rows, + Datum *values) +{ + BufferHeapTupleTableSlot *bslot; + HeapPageBatch *batch; + TupleDesc tupledesc = slot->tts_tupleDescriptor; + TupleBatchMask nulls = 0; + Page page; + + Assert(TTS_IS_BUFFERTUPLE(slot)); + bslot = (BufferHeapTupleTableSlot *) slot; + batch = &bslot->batch; + + Assert(!TTS_EMPTY(slot)); + Assert(slot_getbatch(slot) == &batch->batch); + Assert(batch->offsets != NULL); + Assert(BufferIsValid(bslot->buffer)); + Assert(attnum > 0 && attnum <= tupledesc->natts); + Assert(first >= 0); + Assert(nrows >= 0 && nrows <= TUPLE_BATCH_MASK_BITS); + Assert(nrows <= batch->batch.ntuples); + Assert(first <= batch->batch.ntuples - nrows); + Assert(nrows == TUPLE_BATCH_MASK_BITS || (rows >> nrows) == 0); + Assert(values != NULL); + + if (rows == 0) + return nulls; + + page = BufferGetPage(bslot->buffer); + while (rows != 0) + { + int i = pg_rightmost_one_pos64(rows); + OffsetNumber lineoff = batch->offsets[first + i]; + ItemId lpp = PageGetItemId(page, lineoff); + HeapTupleData tuple; + bool isnull; + + Assert(ItemIdIsNormal(lpp)); + tuple.t_data = (HeapTupleHeader) PageGetItem(page, lpp); + tuple.t_len = ItemIdGetLength(lpp); + values[i] = heap_getattr(&tuple, attnum, tupledesc, &isnull); + if (isnull) + nulls |= UINT64CONST(1) << i; + + rows &= rows - 1; + } + + return nulls; +} + +static inline void +heap_page_batch_reset(HeapPageBatch *batch) +{ + batch->batch.ntuples = 0; +} + static void tts_buffer_heap_init(TupleTableSlot *slot) { + BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; + + Assert(slot->tts_batch == NULL); + if (slot->tts_flags & TTS_FLAG_SUPPORTS_BATCH) + { + bslot->batch.batch.getattrs = tts_buffer_heap_getattrs; + slot->tts_batch = &bslot->batch.batch; + } } static void @@ -722,6 +789,8 @@ tts_buffer_heap_clear(TupleTableSlot *slot) { BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; + heap_page_batch_reset(&bslot->batch); + /* * Free the memory for heap tuple if allowed. A tuple coming from buffer * can never be freed. But we may have materialized a tuple from buffer. @@ -807,6 +876,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 +1016,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 */ @@ -955,11 +1027,7 @@ tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, slot->tts_flags &= ~TTS_FLAG_SHOULDFREE; } - slot->tts_flags &= ~TTS_FLAG_EMPTY; - slot->tts_nvalid = 0; - bslot->base.tuple = tuple; - bslot->base.off = 0; - slot->tts_tid = tuple->t_self; + tts_buffer_heap_set_tuple(bslot, tuple); /* * If tuple is on a disk page, keep the page pinned as long as we hold a @@ -1677,8 +1745,6 @@ ExecStoreBufferHeapTuple(HeapTuple tuple, elog(ERROR, "trying to store an on-disk heap tuple into wrong type of slot"); tts_buffer_heap_store_tuple(slot, tuple, buffer, false); - slot->tts_tableOid = tuple->t_tableOid; - return slot; } @@ -1703,8 +1769,6 @@ ExecStorePinnedBufferHeapTuple(HeapTuple tuple, elog(ERROR, "trying to store an on-disk heap tuple into wrong type of slot"); tts_buffer_heap_store_tuple(slot, tuple, buffer, true); - slot->tts_tableOid = tuple->t_tableOid; - return slot; } diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c index b8c528ca089..a40e1b9e51d 100644 --- a/src/backend/executor/nodeSeqscan.c +++ b/src/backend/executor/nodeSeqscan.c @@ -29,11 +29,13 @@ #include "access/relscan.h" #include "access/tableam.h" +#include "catalog/catalog.h" #include "executor/execParallel.h" #include "executor/execScan.h" #include "executor/executor.h" #include "executor/nodeSeqscan.h" #include "utils/rel.h" +#include "utils/snapmgr.h" static TupleTableSlot *SeqNext(SeqScanState *node); @@ -220,6 +222,7 @@ SeqScanState * ExecInitSeqScan(SeqScan *node, EState *estate, int eflags) { SeqScanState *scanstate; + uint16 slot_flags = TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS; /* * Once upon a time it was possible to have an outerPlan of a SeqScan, but @@ -250,11 +253,22 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags) node->scan.scanrelid, eflags); + /* + * Request the batch interface from the slot implementation when relation + * semantics allow its attributes to remain stable. The slot may ignore + * the request if its backing storage cannot provide a stable batch. + */ + if (estate->es_snapshot != NULL && + IsMVCCSnapshot(estate->es_snapshot) && + !IsInplaceUpdateRelation(scanstate->ss.ss_currentRelation) && + !RelationIsUsedAsCatalogTable(scanstate->ss.ss_currentRelation)) + slot_flags |= TTS_FLAG_SUPPORTS_BATCH; + /* and create slot with the appropriate rowtype */ ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(scanstate->ss.ss_currentRelation), table_slot_callbacks(scanstate->ss.ss_currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + slot_flags); /* * Initialize result type and projection. diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h index 3db6c9c9bd0..d9117ade80c 100644 --- a/src/include/executor/tuptable.h +++ b/src/include/executor/tuptable.h @@ -107,6 +107,9 @@ #define TTS_FLAG_FIXED (1 << 4) #define TTS_FIXED(slot) (((slot)->tts_flags & TTS_FLAG_FIXED) != 0) +/* request that the slot implementation expose its optional batch interface */ +#define TTS_FLAG_SUPPORTS_BATCH (1 << 5) + /* * Defines which of the above flags should never be set in tts_flags when the * TupleTableSlot is created. @@ -115,6 +118,8 @@ struct TupleTableSlotOps; typedef struct TupleTableSlotOps TupleTableSlotOps; +struct TupleTableSlotBatch; +typedef struct TupleTableSlotBatch TupleTableSlotBatch; /* base tuple table slot type */ typedef struct TupleTableSlot @@ -141,8 +146,60 @@ typedef struct TupleTableSlot MemoryContext tts_mcxt; /* slot itself is in this context */ ItemPointerData tts_tid; /* stored tuple's tid */ Oid tts_tableOid; /* table oid of tuple */ + TupleTableSlotBatch *tts_batch; /* optional batch of tuples */ } TupleTableSlot; +typedef uint64 TupleBatchMask; + +#define TUPLE_BATCH_MASK_BITS \ + (sizeof(TupleBatchMask) * BITS_PER_BYTE) + +/* + * A slot implementation accepts TTS_FLAG_SUPPORTS_BATCH by setting tts_batch + * during initialization; it may ignore the request by leaving tts_batch NULL. + * A non-NULL tts_batch means that batches can be published through the slot. + * A batch with zero tuples is inactive; otherwise, 0 <= current < ntuples and + * current identifies the slot's tuple in the batch. The batch pointer and + * generation together identify the batch contents. Attributes available + * through getattrs must remain stable while a batch is active. Slots backed + * by storage that can change those attributes in place must leave tts_batch + * NULL. + * + * getattrs requests at most TUPLE_BATCH_MASK_BITS tuples. Bit i in rows and + * the returned NULL mask, and values[i], refer to tuple first + i in the + * batch. + */ +struct TupleTableSlotBatch +{ + TupleBatchMask (*getattrs) (TupleTableSlot *slot, + AttrNumber attnum, int first, int nrows, + TupleBatchMask rows, Datum *values); + uint64 generation; + int ntuples; + int current; +}; + +static inline bool +slot_supports_batch(const TupleTableSlot *slot) +{ + return slot->tts_batch != NULL; +} + +static inline const TupleTableSlotBatch * +slot_getbatch(const TupleTableSlot *slot) +{ + const TupleTableSlotBatch *batch = slot->tts_batch; + + if (batch == NULL || batch->ntuples <= 0) + return NULL; + + Assert(batch->getattrs != NULL); + Assert(batch->current >= 0); + Assert(batch->current < batch->ntuples); + + return batch; +} + /* routines for a TupleTableSlot implementation */ struct TupleTableSlotOps { @@ -282,6 +339,15 @@ typedef struct HeapTupleTableSlot HeapTupleData tupdata; /* optional workspace for storing tuple */ } HeapTupleTableSlot; +struct HeapScanDescData; + +typedef struct HeapPageBatch +{ + TupleTableSlotBatch batch; + const OffsetNumber *offsets; + struct HeapScanDescData *scan; +} HeapPageBatch; + /* heap tuple residing in a buffer */ typedef struct BufferHeapTupleTableSlot { @@ -296,8 +362,22 @@ 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; +static inline void +tts_buffer_heap_set_tuple(BufferHeapTupleTableSlot *bslot, HeapTuple tuple) +{ + TupleTableSlot *slot = &bslot->base.base; + + slot->tts_flags &= ~TTS_FLAG_EMPTY; + slot->tts_nvalid = 0; + bslot->base.tuple = tuple; + bslot->base.off = 0; + slot->tts_tid = tuple->t_self; + slot->tts_tableOid = tuple->t_tableOid; +} + typedef struct MinimalTupleTableSlot { pg_node_attr(abstract) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 85d989f395d..c42f4a3453c 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 @@ -3271,6 +3272,7 @@ TsmRoutine TupOutputState TupSortStatus TupStoreStatus +TupleBatchMask TupleConstr TupleConversionMap TupleDesc @@ -3281,6 +3283,7 @@ TupleHashTable TupleHashTableData TupleQueueReader TupleTableSlot +TupleTableSlotBatch TupleTableSlotOps TuplesortClusterArg TuplesortDatumArg -- 2.54.0