diff --git a/src/backend/access/heap/heapam_indexscan.c b/src/backend/access/heap/heapam_indexscan.c index 33d14f1de7d..590a26e6785 100644 --- a/src/backend/access/heap/heapam_indexscan.c +++ b/src/backend/access/heap/heapam_indexscan.c @@ -16,6 +16,8 @@ #include "access/heapam.h" #include "access/relscan.h" +#include "access/tableam.h" +#include "executor/tuptable.h" #include "storage/predicate.h" @@ -287,6 +289,34 @@ heapam_index_fetch_tuple(struct IndexFetchTableData *scan, slot->tts_tableOid = RelationGetRelid(scan->rel); ExecStoreBufferHeapTuple(&bslot->base.tupdata, slot, hscan->xs_cbuf); + + /* + * If requested, release the buffer pin early to avoid holding pins + * across executor node boundaries, which can cause recovery conflicts + * on standbys. + * + * SO_MATERIALIZE_ON_FETCH: materialize the tuple into the slot so + * the caller can still read it, then release both pins. + * + * SO_RELEASE_PIN_ON_FETCH: just release the AM's private pin. + * The slot retains its own pin (from ExecStoreBufferHeapTuple) + * until the caller clears it. Use this when the caller doesn't + * need the heap tuple data (e.g. index-only scans that only use + * the visibility answer). + */ + if (hscan->xs_base.flags & SO_MATERIALIZE_ON_FETCH) + { + ExecMaterializeSlot(slot); + ReleaseBuffer(hscan->xs_cbuf); + hscan->xs_cbuf = InvalidBuffer; + hscan->xs_blk = InvalidBlockNumber; + } + else if (hscan->xs_base.flags & SO_RELEASE_PIN_ON_FETCH) + { + ReleaseBuffer(hscan->xs_cbuf); + hscan->xs_cbuf = InvalidBuffer; + hscan->xs_blk = InvalidBlockNumber; + } } else { diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c index d52012e8a69..bc0ff987671 100644 --- a/src/backend/executor/nodeIndexonlyscan.c +++ b/src/backend/executor/nodeIndexonlyscan.c @@ -85,19 +85,35 @@ IndexOnlyNext(IndexOnlyScanState *node) if (scandesc == NULL) { + uint32 scan_flags; + /* * We reach here if the index only scan is not parallel, or if we're * serially executing an index only scan that was planned to be * parallel. */ + scan_flags = ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE; + + /* + * For unique index scans with equality on all key columns, release + * the heap buffer pin immediately after the visibility check. IOS + * fills its output slot from index data, so we don't need the heap + * tuple — just the visibility answer. The slot's own pin (from + * ExecStoreBufferHeapTuple) is released by the ExecClearTuple call + * that follows index_fetch_heap. + */ + if (node->ioss_RelationDesc->rd_index->indisunique && + node->ioss_NumScanKeys >= node->ioss_RelationDesc->rd_index->indnkeyatts) + scan_flags |= SO_RELEASE_PIN_ON_FETCH; + scandesc = index_beginscan(node->ss.ss_currentRelation, node->ioss_RelationDesc, estate->es_snapshot, node->ioss_Instrument, node->ioss_NumScanKeys, node->ioss_NumOrderByKeys, - ScanRelIsReadOnly(&node->ss) ? - SO_HINT_REL_READ_ONLY : SO_NONE); + scan_flags); node->ioss_ScanDesc = scandesc; diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 39f6691ee35..05e3316f577 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -104,18 +104,32 @@ IndexNext(IndexScanState *node) if (scandesc == NULL) { + uint32 scan_flags; + /* * We reach here if the index scan is not parallel, or if we're * serially executing an index scan that was planned to be parallel. */ + scan_flags = ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE; + + /* + * For unique index scans with equality on all key columns, + * materialize the tuple on fetch to release the buffer pin + * immediately. This avoids holding pins across executor node + * boundaries, reducing recovery conflicts on standbys. + */ + if (node->iss_RelationDesc->rd_index->indisunique && + node->iss_NumScanKeys >= node->iss_RelationDesc->rd_index->indnkeyatts) + scan_flags |= SO_MATERIALIZE_ON_FETCH; + scandesc = index_beginscan(node->ss.ss_currentRelation, node->iss_RelationDesc, estate->es_snapshot, node->iss_Instrument, node->iss_NumScanKeys, node->iss_NumOrderByKeys, - ScanRelIsReadOnly(&node->ss) ? - SO_HINT_REL_READ_ONLY : SO_NONE); + scan_flags); node->iss_ScanDesc = scandesc; @@ -202,18 +216,26 @@ IndexNextWithReorder(IndexScanState *node) if (scandesc == NULL) { + uint32 scan_flags; + /* * We reach here if the index scan is not parallel, or if we're * serially executing an index scan that was planned to be parallel. */ + scan_flags = ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE; + + if (node->iss_RelationDesc->rd_index->indisunique && + node->iss_NumScanKeys >= node->iss_RelationDesc->rd_index->indnkeyatts) + scan_flags |= SO_MATERIALIZE_ON_FETCH; + scandesc = index_beginscan(node->ss.ss_currentRelation, node->iss_RelationDesc, estate->es_snapshot, node->iss_Instrument, node->iss_NumScanKeys, node->iss_NumOrderByKeys, - ScanRelIsReadOnly(&node->ss) ? - SO_HINT_REL_READ_ONLY : SO_NONE); + scan_flags); node->iss_ScanDesc = scandesc; diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index f2c36696bca..55b41cd676f 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -72,6 +72,12 @@ typedef enum ScanOptions /* collect scan instrumentation */ SO_SCAN_INSTRUMENT = 1 << 11, + + /* materialize tuple on fetch, releasing the buffer pin immediately */ + SO_MATERIALIZE_ON_FETCH = 1 << 12, + + /* release the AM's buffer pin on fetch; slot retains its own pin until cleared */ + SO_RELEASE_PIN_ON_FETCH = 1 << 13, } ScanOptions; /*