From 9249c18d67b99f9d1b53a2aa94c47e2a94cf4c63 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Sat, 24 Oct 2026 09:38:46 +0000
Subject: [PATCH v1 5/5] Report buffer access strategy for index scans in
 EXPLAIN

When ANALYZE and BUFFERS are enabled, report the buffer access strategy
selected by the index access method for index, index-only, and bitmap index
scans.

Extend index scan instrumentation with an optional strategy value and request
it only when buffer instrumentation is enabled. Have each in-tree index access
method record its selected strategy during scan setup. The standard access
methods report normal, while Bloom reports bulkread.

Propagate the strategy through parallel worker instrumentation and report it only
for the leader and workers that execute the scan. Track whether the value is
valid so another index access method can leave it unavailable.

Add documentation and TAP coverage for normal and bulkread strategies,
parallel workers, never-executed scans, and omission when BUFFERS is disabled.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/
---
 contrib/bloom/blscan.c                     |   2 +
 contrib/bloom/meson.build                  |   1 +
 contrib/bloom/t/002_explain.pl             | 182 +++++++++++++++++++++
 doc/src/sgml/ref/explain.sgml              |   6 +-
 src/backend/access/brin/brin.c             |   2 +
 src/backend/access/gin/ginscan.c           |   2 +
 src/backend/access/gist/gistscan.c         |   2 +
 src/backend/access/hash/hash.c             |   2 +
 src/backend/access/nbtree/nbtree.c         |   2 +
 src/backend/access/spgist/spgscan.c        |   2 +
 src/backend/commands/explain.c             |  50 ++++--
 src/backend/executor/nodeBitmapIndexscan.c |   6 +-
 src/backend/executor/nodeIndexonlyscan.c   |   7 +-
 src/backend/executor/nodeIndexscan.c       |   6 +-
 src/include/executor/instrument_node.h     |  44 +++++
 15 files changed, 301 insertions(+), 15 deletions(-)
  51.5% contrib/bloom/t/
   3.5% doc/src/sgml/ref/
   3.7% src/backend/access/
  20.6% src/backend/commands/
   8.0% src/backend/executor/
  11.6% src/include/executor/

diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e42021ec..3bf71f081de 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -49,6 +49,8 @@ blrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 {
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_BULKREAD);
+
 	if (so->sign)
 		pfree(so->sign);
 	so->sign = NULL;
diff --git a/contrib/bloom/meson.build b/contrib/bloom/meson.build
index fa4f4ea796b..3e6a5667ef1 100644
--- a/contrib/bloom/meson.build
+++ b/contrib/bloom/meson.build
@@ -40,6 +40,7 @@ tests += {
   'tap': {
     'tests': [
       't/001_wal.pl',
+      't/002_explain.pl',
     ],
   },
 }
diff --git a/contrib/bloom/t/002_explain.pl b/contrib/bloom/t/002_explain.pl
new file mode 100644
index 00000000000..d8c5ad23436
--- /dev/null
+++ b/contrib/bloom/t/002_explain.pl
@@ -0,0 +1,182 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use JSON::PP qw(decode_json);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Keep the test table below the bulk-read threshold
+$node->append_conf('postgresql.conf', 'shared_buffers = 1MB');
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE EXTENSION bloom;
+CREATE TABLE tst AS SELECT i % 10 AS i FROM generate_series(1, 2000) AS i;
+CREATE INDEX bloomidx ON tst USING bloom (i);
+CREATE TABLE btree_tst AS
+  SELECT i, i AS payload FROM generate_series(1, 2000) AS i;
+CREATE INDEX btreeidx ON btree_tst (i);
+CREATE TABLE gin_tst AS
+  SELECT ARRAY[i] AS a FROM generate_series(1, 2000) AS i;
+CREATE INDEX ginidx ON gin_tst USING gin (a);
+});
+
+my $btree_plan = explain_btree_json();
+my $btree_scan = find_plan_node($btree_plan, 'Index Scan');
+
+ok(defined $btree_scan, 'plan contains a B-tree index scan');
+is($btree_scan->{'Buffer Access Strategy'},
+	'normal', 'B-tree index scan uses the normal strategy');
+
+my $never_executed_plan = explain_never_executed_gin_json();
+my $never_executed_scan =
+	find_never_executed_plan_node($never_executed_plan, 'Bitmap Index Scan');
+
+ok(defined $never_executed_scan,
+	'plan contains a never-executed GIN index scan');
+ok( !exists $never_executed_scan->{'Buffer Access Strategy'},
+	'strategy is not shown for a never-executed index scan');
+
+my $plan = explain_json('BUFFERS');
+my $heap_scan = find_plan_node($plan, 'Bitmap Heap Scan');
+my $index_scan = find_plan_node($plan, 'Bitmap Index Scan');
+
+ok(defined $heap_scan, 'plan contains a bitmap heap scan');
+is($heap_scan->{'Buffer Access Strategy'},
+	'normal', 'bitmap heap scan uses the normal strategy');
+ok(defined $index_scan, 'plan contains a bitmap index scan');
+is($index_scan->{'Buffer Access Strategy'},
+	'bulkread', 'bloom index scan uses the bulkread strategy');
+
+my $parallel_plan = explain_parallel_json();
+my $gather = find_plan_node($parallel_plan, 'Gather');
+my $parallel_index_scan = find_plan_node($parallel_plan, 'Bitmap Index Scan');
+
+ok(defined $gather, 'parallel plan contains a Gather');
+cmp_ok(defined $gather ? $gather->{'Workers Launched'} : 0,
+	'>', 0, 'parallel worker launched');
+ok(defined $parallel_index_scan,
+	'parallel plan contains a bitmap index scan');
+ok( !exists $parallel_index_scan->{'Buffer Access Strategy'},
+	'strategy is not shown for the non-participating leader');
+my $workers = $parallel_index_scan->{'Workers'};
+ok(ref $workers eq 'ARRAY' && @$workers > 0,
+	'parallel bitmap index scan reports worker information');
+
+foreach my $worker (@$workers)
+{
+	is($worker->{'Buffer Access Strategy'},
+		'bulkread', 'parallel worker uses the bulkread strategy');
+}
+
+my $without_buffers = explain_json('BUFFERS OFF');
+$index_scan = find_plan_node($without_buffers, 'Bitmap Index Scan');
+
+ok(defined $index_scan && !exists $index_scan->{'Buffer Access Strategy'},
+	'strategy is not shown when BUFFERS is disabled');
+
+$node->stop;
+
+done_testing();
+
+# Run EXPLAIN for a B-tree index scan and return the top-level plan.
+sub explain_btree_json
+{
+	my $output = $node->safe_psql(
+		'postgres', qq{
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT * FROM btree_tst WHERE i = 7;
+});
+
+	return decode_json($output)->[0]->{'Plan'};
+}
+
+# Run EXPLAIN with a never-executed gin index scan.
+sub explain_never_executed_gin_json
+{
+	my $output = $node->safe_psql(
+		'postgres', qq{
+SET enable_seqscan = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT g.*
+  FROM btree_tst b
+  CROSS JOIN LATERAL
+    (SELECT * FROM gin_tst g WHERE g.a @> ARRAY[b.i] OFFSET 0) g
+  WHERE b.i = -1;
+});
+
+	return decode_json($output)->[0]->{'Plan'};
+}
+
+# Run EXPLAIN and return the top-level plan.
+sub explain_json
+{
+	my ($buffers) = @_;
+	my $output = $node->safe_psql(
+		'postgres', qq{
+SET enable_seqscan = off;
+EXPLAIN (ANALYZE, $buffers, FORMAT JSON)
+  SELECT * FROM tst WHERE i = 7;
+});
+
+	return decode_json($output)->[0]->{'Plan'};
+}
+
+# Run EXPLAIN in a parallel worker and return the top-level plan.
+sub explain_parallel_json
+{
+	my $output = $node->safe_psql(
+		'postgres', qq{
+SET max_parallel_workers_per_gather = 1;
+SET debug_parallel_query = on;
+SET enable_seqscan = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT * FROM tst WHERE i = 7;
+});
+
+	return decode_json($output)->[0]->{'Plan'};
+}
+
+# Find the first plan node of the requested type.
+sub find_plan_node
+{
+	my ($plan, $type) = @_;
+
+	return $plan if $plan->{'Node Type'} eq $type;
+
+	foreach my $child (@{ $plan->{'Plans'} // [] })
+	{
+		my $result = find_plan_node($child, $type);
+
+		return $result if defined $result;
+	}
+
+	return;
+}
+
+# Find the first never-executed plan node of the requested type.
+sub find_never_executed_plan_node
+{
+	my ($plan, $type) = @_;
+
+	return $plan
+	  if $plan->{'Node Type'} eq $type && $plan->{'Actual Loops'} == 0;
+
+	foreach my $child (@{ $plan->{'Plans'} // [] })
+	{
+		my $result = find_never_executed_plan_node($child, $type);
+
+		return $result if defined $result;
+	}
+
+	return;
+}
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index f7ae8f1b2bd..582b53e1aae 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -206,7 +206,11 @@ ROLLBACK;
       <literal>TID Scan</literal>, and <literal>TID Range Scan</literal> plan
       nodes report the <glossterm
       linkend="glossary-buffer-access-strategy">buffer access
-      strategy</glossterm> used by the table scan.
+      strategy</glossterm> used by the table scan.  Executed
+      <literal>Index Scan</literal>, <literal>Index Only Scan</literal>, and
+      <literal>Bitmap Index Scan</literal> plan nodes likewise report the
+      strategy used by the index scan when the index access method provides
+      it.
       The number of blocks shown for an
       upper-level node includes those used by all its child nodes.  In text
       format, only non-zero values are printed.  Buffers information is
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 5059da8dce7..71607bfcba3 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -964,6 +964,8 @@ void
 brinrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 		   ScanKey orderbys, int norderbys)
 {
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	/*
 	 * Other index AMs preprocess the scan keys at this point, or sometime
 	 * early during the scan; this lets them optimize by removing redundant
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index 1564b409bdb..a786e3521a8 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -490,6 +490,8 @@ ginrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 {
 	GinScanOpaque so = (GinScanOpaque) scan->opaque;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	ginFreeScanKeys(so);
 
 	if (scankey && scan->numberOfKeys > 0)
diff --git a/src/backend/access/gist/gistscan.c b/src/backend/access/gist/gistscan.c
index b67ba5a8a53..aa030ad7bca 100644
--- a/src/backend/access/gist/gistscan.c
+++ b/src/backend/access/gist/gistscan.c
@@ -133,6 +133,8 @@ gistrescan(IndexScanDesc scan, ScanKey key, int nkeys,
 	int			i;
 	MemoryContext oldCxt;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	/* Before leaving current page, deal with any killed items */
 	if (so->numKilled > 0)
 		gistkillitems(scan);
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 63846c10840..504739149b0 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -419,6 +419,8 @@ hashrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 	HashScanOpaque so = (HashScanOpaque) scan->opaque;
 	Relation	rel = scan->indexRelation;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	if (HashScanPosIsValid(so->currPos))
 	{
 		/* Before leaving current page, deal with any killed items */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 0abdd7b49f5..9e483d2f78f 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -390,6 +390,8 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	/* we aren't holding any read locks, but gotta drop the pins */
 	if (BTScanPosIsValid(so->currPos))
 	{
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 2cc5f06f5d7..87d3df64dd3 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -377,6 +377,8 @@ spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 {
 	SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
 
+	InstrRecordIndexScanStrategy(scan->instrument, BAS_NORMAL);
+
 	/* copy scankeys into local storage */
 	if (scankey && scan->numberOfKeys > 0)
 		memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e113a8950c..418919c8a4a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -140,6 +140,8 @@ static void show_hashagg_info(AggState *aggstate, ExplainState *es);
 static void show_indexscan_info(PlanState *planstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
+static void print_buffer_access_strategy(ExplainState *es,
+										 BufferAccessStrategyType strategy);
 static void show_scan_buffer_access_strategy(ScanState *planstate, ExplainState *es);
 static void show_scan_io_usage(ScanState *planstate,
 							   ExplainState *es);
@@ -3909,8 +3911,10 @@ show_indexscan_info(PlanState *planstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	SharedIndexScanInstrumentation *SharedInfo = NULL;
-	uint64		nsearches = 0,
-				ntabletuplefetches = 0;
+	WorkerNodeInstrumentation *worker_instrument = planstate->worker_instrument;
+	IndexScanInstrumentation stats = {0};
+	double		leader_nloops = planstate->instrument ?
+		planstate->instrument->nloops : 0;
 
 	if (!es->analyze)
 		return;
@@ -3922,7 +3926,7 @@ show_indexscan_info(PlanState *planstate, ExplainState *es)
 			{
 				IndexScanState *indexstate = ((IndexScanState *) planstate);
 
-				nsearches = indexstate->iss_Instrument->nsearches;
+				stats = *indexstate->iss_Instrument;
 				SharedInfo = indexstate->iss_SharedInfo;
 				break;
 			}
@@ -3930,8 +3934,7 @@ show_indexscan_info(PlanState *planstate, ExplainState *es)
 			{
 				IndexOnlyScanState *indexstate = ((IndexOnlyScanState *) planstate);
 
-				nsearches = indexstate->ioss_Instrument->nsearches;
-				ntabletuplefetches = indexstate->ioss_Instrument->ntabletuplefetches;
+				stats = *indexstate->ioss_Instrument;
 				SharedInfo = indexstate->ioss_SharedInfo;
 				break;
 			}
@@ -3939,7 +3942,7 @@ show_indexscan_info(PlanState *planstate, ExplainState *es)
 			{
 				BitmapIndexScanState *indexstate = ((BitmapIndexScanState *) planstate);
 
-				nsearches = indexstate->biss_Instrument->nsearches;
+				stats = *indexstate->biss_Instrument;
 				SharedInfo = indexstate->biss_SharedInfo;
 				break;
 			}
@@ -3947,22 +3950,49 @@ show_indexscan_info(PlanState *planstate, ExplainState *es)
 			break;
 	}
 
+	if (worker_instrument)
+	{
+		/*
+		 * After parallel cleanup, the node's loop count includes worker
+		 * loops. Remove them to determine whether the leader executed the
+		 * scan.
+		 */
+		for (int i = 0; i < worker_instrument->num_workers; ++i)
+			leader_nloops -= worker_instrument->instrument[i].nloops;
+	}
+
 	/* Next get the sum of the counters set within each and every process */
 	if (SharedInfo)
 	{
 		for (int i = 0; i < SharedInfo->num_workers; ++i)
 		{
 			IndexScanInstrumentation *winstrument = &SharedInfo->winstrument[i];
+			double		worker_nloops =
+				worker_instrument && i < worker_instrument->num_workers ?
+				worker_instrument->instrument[i].nloops : 0;
 
-			nsearches += winstrument->nsearches;
-			ntabletuplefetches += winstrument->ntabletuplefetches;
+			stats.nsearches += winstrument->nsearches;
+			stats.ntabletuplefetches += winstrument->ntabletuplefetches;
+
+			if (!es->buffers || worker_nloops <= 0 ||
+				!winstrument->strategy_valid)
+				continue;
+
+			if (es->workers_state)
+				ExplainOpenWorker(i, es);
+			print_buffer_access_strategy(es, winstrument->strategy);
+			if (es->workers_state)
+				ExplainCloseWorker(i, es);
 		}
 	}
 
+	if (es->buffers && stats.strategy_valid && leader_nloops > 0)
+		print_buffer_access_strategy(es, stats.strategy);
+
 	if (nodeTag(plan) == T_IndexOnlyScan)
-		ExplainPropertyUInteger("Heap Fetches", NULL, ntabletuplefetches, es);
+		ExplainPropertyUInteger("Heap Fetches", NULL, stats.ntabletuplefetches, es);
 
-	ExplainPropertyUInteger("Index Searches", NULL, nsearches, es);
+	ExplainPropertyUInteger("Index Searches", NULL, stats.nsearches, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapIndexscan.c b/src/backend/executor/nodeBitmapIndexscan.c
index 90b010f9b71..86e4eb39923 100644
--- a/src/backend/executor/nodeBitmapIndexscan.c
+++ b/src/backend/executor/nodeBitmapIndexscan.c
@@ -203,7 +203,7 @@ ExecEndBitmapIndexScan(BitmapIndexScanState *node)
 		 * shutdown on the workers.  On rescan it will spin up new workers
 		 * which will have a new BitmapIndexScanState and zeroed stats.
 		 */
-		winstrument->nsearches += node->biss_Instrument->nsearches;
+		AccumulateIndexScanInstrumentation(winstrument, node->biss_Instrument);
 		Assert(node->biss_Instrument->ntabletuplefetches == 0);
 	}
 
@@ -277,7 +277,11 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags)
 
 	/* Set up instrumentation of bitmap index scans if requested */
 	if (estate->es_instrument)
+	{
 		indexstate->biss_Instrument = palloc0_object(IndexScanInstrumentation);
+		indexstate->biss_Instrument->need_strategy =
+			(estate->es_instrument & INSTRUMENT_BUFFERS) != 0;
+	}
 
 	/* Open the index relation. */
 	lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode;
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index b1009027ac8..520a166c891 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -260,8 +260,7 @@ ExecEndIndexOnlyScan(IndexOnlyScanState *node)
 		 * shutdown on the workers.  On rescan it will spin up new workers
 		 * which will have a new IndexOnlyScanState and zeroed stats.
 		 */
-		winstrument->nsearches += node->ioss_Instrument->nsearches;
-		winstrument->ntabletuplefetches += node->ioss_Instrument->ntabletuplefetches;
+		AccumulateIndexScanInstrumentation(winstrument, node->ioss_Instrument);
 	}
 
 	/*
@@ -426,7 +425,11 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 
 	/* Set up instrumentation of index-only scans if requested */
 	if (estate->es_instrument)
+	{
 		indexstate->ioss_Instrument = palloc0_object(IndexScanInstrumentation);
+		indexstate->ioss_Instrument->need_strategy =
+			(estate->es_instrument & INSTRUMENT_BUFFERS) != 0;
+	}
 
 	/* Open the index relation. */
 	lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode;
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 129d005f187..429f5567f53 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -819,7 +819,7 @@ ExecEndIndexScan(IndexScanState *node)
 		 * shutdown on the workers.  On rescan it will spin up new workers
 		 * which will have a new IndexOnlyScanState and zeroed stats.
 		 */
-		winstrument->nsearches += node->iss_Instrument->nsearches;
+		AccumulateIndexScanInstrumentation(winstrument, node->iss_Instrument);
 		Assert(node->iss_Instrument->ntabletuplefetches == 0);
 	}
 
@@ -983,7 +983,11 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 
 	/* Set up instrumentation of index scans if requested */
 	if (estate->es_instrument)
+	{
 		indexstate->iss_Instrument = palloc0_object(IndexScanInstrumentation);
+		indexstate->iss_Instrument->need_strategy =
+			(estate->es_instrument & INSTRUMENT_BUFFERS) != 0;
+	}
 
 	/* Open the index relation. */
 	lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode;
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index 31188729835..36f9e2cf1cb 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -114,8 +114,52 @@ typedef struct IndexScanInstrumentation
 
 	/* Table tuples fetched count (incremented during index-only scans) */
 	uint64		ntabletuplefetches;
+
+	/* Whether the index AM should record its buffer access strategy */
+	bool		need_strategy;
+
+	/* Buffer access strategy reported by the index AM, if available */
+	BufferAccessStrategyType strategy;
+	bool		strategy_valid;
 } IndexScanInstrumentation;
 
+/*
+ * Record the buffer access strategy selected by an index AM.
+ *
+ * The strategy is recorded only when requested by the executor.
+ */
+static inline void
+InstrRecordIndexScanStrategy(IndexScanInstrumentation *instrument,
+							 BufferAccessStrategyType strategy)
+{
+	if (!instrument)
+		return;
+
+	if (instrument->need_strategy)
+	{
+		Assert(!instrument->strategy_valid ||
+			   instrument->strategy == strategy);
+		instrument->strategy = strategy;
+		instrument->strategy_valid = true;
+	}
+}
+
+/* Merge index scan instrumentation from 'src' into 'dst'. */
+static inline void
+AccumulateIndexScanInstrumentation(IndexScanInstrumentation *dst,
+								   IndexScanInstrumentation *src)
+{
+	dst->nsearches += src->nsearches;
+	dst->ntabletuplefetches += src->ntabletuplefetches;
+
+	if (src->strategy_valid)
+	{
+		Assert(!dst->strategy_valid || dst->strategy == src->strategy);
+		dst->strategy = src->strategy;
+		dst->strategy_valid = true;
+	}
+}
+
 /*
  * Shared memory container for per-worker information
  */
-- 
2.34.1

