From 5cfd87b80e642b90072fb77411f8697aa346355d 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 v2 8/8] 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 core 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: Sami Imseih <samimseih.pg@gmail.com>
Discussion: https://postgr.es/m/auCYYbDyrQEwIsQq@bdtpg
---
 contrib/bloom/blscan.c                     |   2 +
 contrib/bloom/meson.build                  |   1 +
 contrib/bloom/t/002_explain.pl             | 204 +++++++++++++++++++++
 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     |  47 +++++
 15 files changed, 326 insertions(+), 15 deletions(-)
  54.6% contrib/bloom/t/
   3.2% doc/src/sgml/ref/
   3.4% src/backend/access/
  19.2% src/backend/commands/
   7.4% src/backend/executor/
  10.9% 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..312e2b8b8f5
--- /dev/null
+++ b/contrib/bloom/t/002_explain.pl
@@ -0,0 +1,204 @@
+# 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 $btree_index_only_plan = explain_btree_index_only_json();
+my $btree_index_only_scan =
+  find_plan_node($btree_index_only_plan, 'Index Only Scan');
+
+ok(defined $btree_index_only_scan, 'plan contains a B-tree index-only scan');
+is($btree_index_only_scan->{'Buffer Access Strategy'},
+	'normal', 'B-tree index-only 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 for a B-tree index-only scan and return the top-level plan.
+sub explain_btree_index_only_json
+{
+	my $output = $node->safe_psql(
+		'postgres', qq{
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT i 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 c2a30077b4a..1f35efa0412 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..dfea3fdd84c 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;
+		CombineIndexScanInstrumentation(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..14ce758c00f 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;
+		CombineIndexScanInstrumentation(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..83262d4ea6e 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;
+		CombineIndexScanInstrumentation(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 005fdc6f57b..68888e49b08 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -133,8 +133,55 @@ 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;
+
+	/* Most recently reported buffer access strategy, 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.  The most
+ * recently reported strategy is retained.
+ */
+static inline void
+InstrRecordIndexScanStrategy(IndexScanInstrumentation *instrument,
+							 BufferAccessStrategyType strategy)
+{
+	if (!instrument)
+		return;
+
+	if (instrument->need_strategy)
+	{
+		instrument->strategy = strategy;
+		instrument->strategy_valid = true;
+	}
+}
+
+/*
+ * Combine index scan instrumentation from 'src' into 'dst'.
+ *
+ * Counter values are added, while the strategy from 'src' replaces the
+ * strategy in 'dst' when available.
+ */
+static inline void
+CombineIndexScanInstrumentation(IndexScanInstrumentation *dst,
+								IndexScanInstrumentation *src)
+{
+	dst->nsearches += src->nsearches;
+	dst->ntabletuplefetches += src->ntabletuplefetches;
+
+	if (src->strategy_valid)
+	{
+		dst->strategy = src->strategy;
+		dst->strategy_valid = true;
+	}
+}
+
 /*
  * Shared memory container for per-worker information
  */
-- 
2.34.1

