From 398037392fc347c3da2419d0d4c421d883f10ad2 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 22 Oct 2026 04:08:08 +0000
Subject: [PATCH v1 4/5] Report buffer access strategy for table scans in
 EXPLAIN

When BUFFERS and ANALYZE are enabled, record the strategy selected by the table
scan descriptor for sequential, sample, bitmap heap, TID, and TID range scans,
and propagate it through parallel worker instrumentation.

Track whether the strategy value is valid so a table AM can leave it
unavailable without EXPLAIN confusing it with BAS_NORMAL.

Move BufferAccessStrategyType to buf.h so executor instrumentation can use it
without including the heavier bufmgr.h.

Report the strategy in text and structured output for each executed scan. Add
documentation and TAP coverage.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/
---
 doc/src/sgml/ref/explain.sgml                 |   7 +
 src/backend/access/heap/heapam.c              |  24 +-
 src/backend/commands/explain.c                |  97 ++++++++
 src/backend/executor/nodeBitmapHeapscan.c     |  12 +-
 src/backend/executor/nodeSamplescan.c         |  10 +-
 src/backend/executor/nodeSeqscan.c            |  20 +-
 src/backend/executor/nodeTidrangescan.c       |  18 +-
 src/backend/executor/nodeTidscan.c            |  11 +-
 src/backend/storage/buffer/freelist.c         |  14 ++
 src/backend/utils/adt/tid.c                   |   2 +-
 src/include/access/tableam.h                  |  12 +-
 src/include/executor/instrument_node.h        |   6 +
 src/include/storage/buf.h                     |  15 ++
 src/include/storage/bufmgr.h                  |  16 +-
 src/test/modules/test_misc/meson.build        |   1 +
 .../t/016_explain_buffer_strategy.pl          | 167 +++++++++++++
 src/test/regress/expected/explain.out         | 231 +++++++++---------
 src/test/regress/sql/explain.sql              |   2 +
 18 files changed, 514 insertions(+), 151 deletions(-)
   3.8% src/backend/access/heap/
  10.7% src/backend/commands/
  13.3% src/backend/executor/
   4.2% src/include/storage/
  22.0% src/test/modules/test_misc/t/
  38.6% src/test/regress/expected/
   5.1% src/

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index f38d31e106a..f7ae8f1b2bd 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -200,6 +200,13 @@ ROLLBACK;
       number of blocks <emphasis>written</emphasis> indicates the number of
       previously-dirtied blocks evicted from cache by this backend during
       query processing.
+      When both <literal>ANALYZE</literal> and <literal>BUFFERS</literal> are
+      enabled, executed <literal>Seq Scan</literal>,
+      <literal>Sample Scan</literal>, <literal>Bitmap Heap Scan</literal>,
+      <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.
       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/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6a09a1a1517..fd9702abcc5 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1317,12 +1317,22 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 														  sizeof(TBMIterateResult));
 	}
 
-	/* enable read stream instrumentation */
-	if ((flags & SO_SCAN_INSTRUMENT) && (scan->rs_read_stream != NULL))
+	/* enable scan instrumentation */
+	if (flags & (SO_SCAN_INSTRUMENT | SO_SCAN_INSTRUMENT_STRATEGY))
 	{
 		scan->rs_base.rs_instrument = palloc0_object(TableScanInstrumentation);
-		read_stream_enable_stats(scan->rs_read_stream,
-								 &scan->rs_base.rs_instrument->io);
+
+		/* strategy instrumentation */
+		if (flags & SO_SCAN_INSTRUMENT_STRATEGY)
+		{
+			scan->rs_base.rs_instrument->strategy = GetAccessStrategyType(scan->rs_strategy);
+			scan->rs_base.rs_instrument->strategy_valid = true;
+		}
+
+		/* read stream instrumentation */
+		if ((flags & SO_SCAN_INSTRUMENT) && scan->rs_read_stream != NULL)
+			read_stream_enable_stats(scan->rs_read_stream,
+									 &scan->rs_base.rs_instrument->io);
 	}
 
 	scan->rs_vmbuffer = InvalidBuffer;
@@ -1387,6 +1397,12 @@ heap_rescan(TableScanDesc sscan, ScanKey key, bool set_params,
 	 * reinitialize scan descriptor
 	 */
 	initscan(scan, key, true);
+
+	if (scan->rs_base.rs_flags & SO_SCAN_INSTRUMENT_STRATEGY)
+	{
+		scan->rs_base.rs_instrument->strategy = GetAccessStrategyType(scan->rs_strategy);
+		scan->rs_base.rs_instrument->strategy_valid = true;
+	}
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d0ce72d83ff..7e113a8950c 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -140,6 +140,7 @@ 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 show_scan_buffer_access_strategy(ScanState *planstate, ExplainState *es);
 static void show_scan_io_usage(ScanState *planstate,
 							   ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -2010,6 +2011,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_scan_buffer_access_strategy((ScanState *) planstate, es);
 			show_scan_io_usage((ScanState *) planstate, es);
 			break;
 		case T_SampleScan:
@@ -2029,6 +2031,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			if (IsA(plan, CteScan))
 				show_ctescan_info(castNode(CteScanState, planstate), es);
+			show_scan_buffer_access_strategy((ScanState *) planstate, es);
 			show_scan_io_usage((ScanState *) planstate, es);
 			break;
 		case T_Gather:
@@ -2129,6 +2132,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
 											   planstate, es);
+				show_scan_buffer_access_strategy((ScanState *) planstate, es);
 			}
 			break;
 		case T_TidRangeScan:
@@ -2146,6 +2150,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
 											   planstate, es);
+				show_scan_buffer_access_strategy((ScanState *) planstate, es);
 				show_scan_io_usage((ScanState *) planstate, es);
 			}
 			break;
@@ -4027,6 +4032,38 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 	}
 }
 
+/*
+ * Return the name of a buffer access strategy for EXPLAIN.
+ */
+static const char *
+buffer_access_strategy_name(BufferAccessStrategyType strategy)
+{
+	switch (strategy)
+	{
+		case BAS_NORMAL:
+			return "normal";
+		case BAS_BULKREAD:
+			return "bulkread";
+		case BAS_BULKWRITE:
+			return "bulkwrite";
+		case BAS_VACUUM:
+			return "vacuum";
+	}
+
+	elog(ERROR, "unrecognized buffer access strategy type: %d", strategy);
+	pg_unreachable();
+}
+
+/*
+ * Print the buffer access strategy.
+ */
+static void
+print_buffer_access_strategy(ExplainState *es,
+							 BufferAccessStrategyType strategy)
+{
+	ExplainPropertyText("Buffer Access Strategy", buffer_access_strategy_name(strategy), es);
+}
+
 /*
  * Print I/O stats - prefetching and I/O performed
  *
@@ -4140,6 +4177,66 @@ get_table_scan_instrumentation(ScanState *planstate, int worker)
 	return NULL;
 }
 
+/*
+ * Show the buffer access strategy for a scan node.
+ */
+static void
+show_scan_buffer_access_strategy(ScanState *planstate, ExplainState *es)
+{
+	WorkerNodeInstrumentation *worker_instrument = planstate->ps.worker_instrument;
+	TableScanInstrumentation *instrument;
+	double		leader_nloops = planstate->ps.instrument ?
+		planstate->ps.instrument->nloops : 0;
+
+	if (!es->buffers)
+		return;
+
+	/* Report the buffer access strategy only for these scan types. */
+	switch (nodeTag(planstate->ps.plan))
+	{
+		case T_BitmapHeapScan:
+		case T_SampleScan:
+		case T_SeqScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+			break;
+		default:
+			return;
+	}
+
+	instrument = get_table_scan_instrumentation(planstate, -1);
+
+	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)
+		{
+			double		worker_nloops = worker_instrument->instrument[i].nloops;
+			TableScanInstrumentation *winstrument =
+				get_table_scan_instrumentation(planstate, i);
+
+			leader_nloops -= worker_nloops;
+
+			if (!winstrument || !winstrument->strategy_valid ||
+				worker_nloops <= 0)
+				continue;
+
+			if (es->workers_state)
+				ExplainOpenWorker(i, es);
+			print_buffer_access_strategy(es, winstrument->strategy);
+			if (es->workers_state)
+				ExplainCloseWorker(i, es);
+		}
+	}
+
+	if (instrument && instrument->strategy_valid && leader_nloops > 0)
+		print_buffer_access_strategy(es, instrument->strategy);
+}
+
 /*
  * Show information about prefetch and I/O in a scan node.
  */
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index a9e83f30687..40a561f3c70 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -149,6 +149,8 @@ BitmapTableScanSetup(BitmapHeapScanState *node)
 
 		if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 			flags |= SO_SCAN_INSTRUMENT;
+		if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 		node->ss.ss_currentScanDesc =
 			table_beginscan_bm(node->ss.ss_currentRelation,
@@ -334,12 +336,16 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node)
 		si->exact_pages += node->stats.exact_pages;
 		si->lossy_pages += node->stats.lossy_pages;
 
-		/* collect I/O instrumentation for this process */
+		/* Collect scan instrumentation for this process */
 		if (node->ss.ss_currentScanDesc &&
 			node->ss.ss_currentScanDesc->rs_instrument)
 		{
-			AccumulateIOStats(&si->stats.io,
-							  &node->ss.ss_currentScanDesc->rs_instrument->io);
+			TableScanInstrumentation *stats =
+				node->ss.ss_currentScanDesc->rs_instrument;
+
+			AccumulateIOStats(&si->stats.io, &stats->io);
+			si->stats.strategy = stats->strategy;
+			si->stats.strategy_valid = stats->strategy_valid;
 		}
 	}
 
diff --git a/src/backend/executor/nodeSamplescan.c b/src/backend/executor/nodeSamplescan.c
index f3d273e1c5e..79025f28727 100644
--- a/src/backend/executor/nodeSamplescan.c
+++ b/src/backend/executor/nodeSamplescan.c
@@ -19,6 +19,7 @@
 #include "access/tsmapi.h"
 #include "common/pg_prng.h"
 #include "executor/executor.h"
+#include "executor/instrument.h"
 #include "executor/nodeSamplescan.h"
 #include "utils/fmgrprotos.h"
 #include "utils/rel.h"
@@ -292,6 +293,12 @@ tablesample_init(SampleScanState *scanstate)
 	/* Now we can create or reset the HeapScanDesc */
 	if (scanstate->ss.ss_currentScanDesc == NULL)
 	{
+		uint32		flags = ScanRelIsReadOnly(&scanstate->ss) ?
+			SO_HINT_REL_READ_ONLY : SO_NONE;
+
+		if (scanstate->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
+
 		scanstate->ss.ss_currentScanDesc =
 			table_beginscan_sampling(scanstate->ss.ss_currentRelation,
 									 scanstate->ss.ps.state->es_snapshot,
@@ -299,8 +306,7 @@ tablesample_init(SampleScanState *scanstate)
 									 scanstate->use_bulkread,
 									 allow_sync,
 									 scanstate->use_pagemode,
-									 ScanRelIsReadOnly(&scanstate->ss) ?
-									 SO_HINT_REL_READ_ONLY : SO_NONE);
+									 flags);
 	}
 	else
 	{
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index b8c528ca089..7eb46fa3ba6 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -73,6 +73,8 @@ SeqNext(SeqScanState *node)
 
 		if (estate->es_instrument & INSTRUMENT_IO)
 			flags |= SO_SCAN_INSTRUMENT;
+		if (estate->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 		/*
 		 * We reach here if the scan is not parallel, or if we're serially
@@ -309,9 +311,7 @@ ExecEndSeqScan(SeqScanState *node)
 	 */
 	scanDesc = node->ss.ss_currentScanDesc;
 
-	/*
-	 * Collect I/O stats for this process into shared instrumentation.
-	 */
+	/* Collect scan statistics for this process into shared instrumentation */
 	if (node->sinstrument != NULL && IsParallelWorker())
 	{
 		SeqScanInstrumentation *si;
@@ -322,6 +322,8 @@ ExecEndSeqScan(SeqScanState *node)
 		if (scanDesc && scanDesc->rs_instrument)
 		{
 			AccumulateIOStats(&si->stats.io, &scanDesc->rs_instrument->io);
+			si->stats.strategy = scanDesc->rs_instrument->strategy;
+			si->stats.strategy_valid = scanDesc->rs_instrument->strategy_valid;
 		}
 	}
 
@@ -400,6 +402,8 @@ ExecSeqScanInitializeDSM(SeqScanState *node,
 
 	if (estate->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (estate->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_allocate(pcxt->toc, node->pscan_len);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
@@ -445,6 +449,8 @@ ExecSeqScanInitializeWorker(SeqScanState *node,
 
 	if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
@@ -461,7 +467,8 @@ ExecSeqScanInstrumentEstimate(SeqScanState *node, ParallelContext *pcxt)
 	EState	   *estate = node->ss.ps.state;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument),
@@ -481,7 +488,8 @@ ExecSeqScanInstrumentInitDSM(SeqScanState *node, ParallelContext *pcxt)
 	SharedSeqScanInstrumentation *sinstrument;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument),
@@ -505,7 +513,7 @@ ExecSeqScanInstrumentInitWorker(SeqScanState *node,
 {
 	EState	   *estate = node->ss.ps.state;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0)
 		return;
 
 	node->sinstrument = shm_toc_lookup(pwcxt->toc,
diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c
index b387ed6c308..dc001d20e10 100644
--- a/src/backend/executor/nodeTidrangescan.c
+++ b/src/backend/executor/nodeTidrangescan.c
@@ -251,6 +251,8 @@ TidRangeNext(TidRangeScanState *node)
 
 			if (estate->es_instrument & INSTRUMENT_IO)
 				flags |= SO_SCAN_INSTRUMENT;
+			if (estate->es_instrument & INSTRUMENT_BUFFERS)
+				flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 			scandesc = table_beginscan_tidrange(node->ss.ss_currentRelation,
 												estate->es_snapshot,
@@ -351,7 +353,7 @@ ExecEndTidRangeScan(TidRangeScanState *node)
 {
 	TableScanDesc scan = node->ss.ss_currentScanDesc;
 
-	/* Collect IO stats for this process into shared instrumentation */
+	/* Collect scan statistics for this process into shared instrumentation */
 	if (node->trss_sinstrument != NULL && IsParallelWorker())
 	{
 		TidRangeScanInstrumentation *si;
@@ -362,6 +364,8 @@ ExecEndTidRangeScan(TidRangeScanState *node)
 		if (scan && scan->rs_instrument)
 		{
 			AccumulateIOStats(&si->stats.io, &scan->rs_instrument->io);
+			si->stats.strategy = scan->rs_instrument->strategy;
+			si->stats.strategy_valid = scan->rs_instrument->strategy_valid;
 		}
 	}
 
@@ -484,6 +488,8 @@ ExecTidRangeScanInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt)
 
 	if (estate->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (estate->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_allocate(pcxt->toc, node->trss_pscanlen);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
@@ -529,6 +535,8 @@ ExecTidRangeScanInitializeWorker(TidRangeScanState *node,
 
 	if (node->ss.ps.state->es_instrument & INSTRUMENT_IO)
 		flags |= SO_SCAN_INSTRUMENT;
+	if (node->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+		flags |= SO_SCAN_INSTRUMENT_STRATEGY;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
@@ -547,7 +555,8 @@ ExecTidRangeScanInstrumentEstimate(TidRangeScanState *node,
 	EState	   *estate = node->ss.ps.state;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument),
@@ -568,7 +577,8 @@ ExecTidRangeScanInstrumentInitDSM(TidRangeScanState *node,
 	SharedTidRangeScanInstrumentation *sinstrument;
 	Size		size;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0 ||
+		pcxt->nworkers == 0)
 		return;
 
 	size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument),
@@ -592,7 +602,7 @@ ExecTidRangeScanInstrumentInitWorker(TidRangeScanState *node,
 {
 	EState	   *estate = node->ss.ps.state;
 
-	if ((estate->es_instrument & INSTRUMENT_IO) == 0)
+	if ((estate->es_instrument & (INSTRUMENT_BUFFERS | INSTRUMENT_IO)) == 0)
 		return;
 
 	node->trss_sinstrument = shm_toc_lookup(pwcxt->toc,
diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c
index b63329de330..28cefb20b8d 100644
--- a/src/backend/executor/nodeTidscan.c
+++ b/src/backend/executor/nodeTidscan.c
@@ -26,6 +26,7 @@
 #include "access/tableam.h"
 #include "catalog/pg_type.h"
 #include "executor/executor.h"
+#include "executor/instrument.h"
 #include "executor/nodeTidscan.h"
 #include "lib/qunique.h"
 #include "miscadmin.h"
@@ -146,9 +147,17 @@ TidListEval(TidScanState *tidstate)
 	 * the node might never get executed.
 	 */
 	if (tidstate->ss.ss_currentScanDesc == NULL)
+	{
+		uint32		flags = SO_NONE;
+
+		if (tidstate->ss.ps.state->es_instrument & INSTRUMENT_BUFFERS)
+			flags |= SO_SCAN_INSTRUMENT_STRATEGY;
+
 		tidstate->ss.ss_currentScanDesc =
 			table_beginscan_tid(tidstate->ss.ss_currentRelation,
-								tidstate->ss.ps.state->es_snapshot);
+								tidstate->ss.ps.state->es_snapshot,
+								flags);
+	}
 	scan = tidstate->ss.ss_currentScanDesc;
 
 	/*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index f4b5f2cf44e..57dba7a893e 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -542,6 +542,20 @@ GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
 	return strategy;
 }
 
+/*
+ * GetAccessStrategyType -- return the type of a buffer access strategy.
+ *
+ * A NULL strategy represents normal buffer replacement.
+ */
+BufferAccessStrategyType
+GetAccessStrategyType(BufferAccessStrategy strategy)
+{
+	if (strategy == NULL)
+		return BAS_NORMAL;
+
+	return strategy->btype;
+}
+
 /*
  * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
  *		the ring
diff --git a/src/backend/utils/adt/tid.c b/src/backend/utils/adt/tid.c
index f730b2f07c2..223a75866f6 100644
--- a/src/backend/utils/adt/tid.c
+++ b/src/backend/utils/adt/tid.c
@@ -355,7 +355,7 @@ currtid_internal(Relation rel, const ItemPointerData *tid)
 	ItemPointerCopy(tid, result);
 
 	snapshot = RegisterSnapshot(GetLatestSnapshot());
-	scan = table_beginscan_tid(rel, snapshot);
+	scan = table_beginscan_tid(rel, snapshot, SO_NONE);
 	table_tuple_get_latest_tid(scan, result);
 	table_endscan(scan);
 	UnregisterSnapshot(snapshot);
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index ea3f2a6be99..2a649431e13 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -71,8 +71,10 @@ typedef enum ScanOptions
 	/* set if the query doesn't modify the relation */
 	SO_HINT_REL_READ_ONLY = 1 << 10,
 
-	/* collect scan instrumentation */
+	/* collect read stream instrumentation */
 	SO_SCAN_INSTRUMENT = 1 << 11,
+	/* collect buffer access strategy instrumentation */
+	SO_SCAN_INSTRUMENT_STRATEGY = 1 << 12,
 }			ScanOptions;
 
 /*
@@ -1030,14 +1032,16 @@ table_beginscan_sampling(Relation rel, Snapshot snapshot,
  * table_beginscan_tid is an alternative entry point for setting up a
  * TableScanDesc for a Tid scan. As with bitmap scans, it's worth using
  * the same data structure although the behavior is rather different.
+ *
+ * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted.
  */
 static inline TableScanDesc
-table_beginscan_tid(Relation rel, Snapshot snapshot)
+table_beginscan_tid(Relation rel, Snapshot snapshot, uint32 flags)
 {
-	uint32		flags = SO_TYPE_TIDSCAN;
+	uint32		internal_flags = SO_TYPE_TIDSCAN;
 
 	return table_beginscan_common(rel, snapshot, 0, NULL, NULL,
-								  flags, SO_NONE);
+								  internal_flags, flags);
 }
 
 /*
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index 4759a628afe..31188729835 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -18,6 +18,8 @@
 #ifndef INSTRUMENT_NODE_H
 #define INSTRUMENT_NODE_H
 
+#include "storage/buf.h"
+
 /*
  * Offset added to plan_node_id to create a second TOC key for per-worker scan
  * instrumentation. Instrumentation and parallel-awareness are independent, so
@@ -78,6 +80,10 @@ typedef struct IOStats
 typedef struct TableScanInstrumentation
 {
 	IOStats		io;
+
+	/* Buffer access strategy reported by the table AM, if available. */
+	BufferAccessStrategyType strategy;
+	bool		strategy_valid;
 } TableScanInstrumentation;
 
 /* merge IO statistics from 'src' into 'dst' */
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..06583a253fd 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,6 +36,21 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
+/*
+ * Possible arguments for GetAccessStrategy().
+ *
+ * If adding a new BufferAccessStrategyType, also add a new IOContext so
+ * IO statistics using this strategy are tracked.
+ */
+typedef enum BufferAccessStrategyType
+{
+	BAS_NORMAL,					/* Normal random access */
+	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
+								 * ok) */
+	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
+	BAS_VACUUM,					/* VACUUM */
+} BufferAccessStrategyType;
+
 /*
  * Buffer access strategy objects.
  *
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..be45f000982 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -379,6 +364,7 @@ extern void AtProcExit_LocalBuffers(void);
 extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
 extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
 													  int ring_size_kb);
+extern BufferAccessStrategyType GetAccessStrategyType(BufferAccessStrategy strategy);
 extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
 extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
 
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 5d81f5b13be..e71754ebbc3 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -24,6 +24,7 @@ tests += {
       't/013_temp_obj_multisession.pl',
       't/014_log_statement_max_length.pl',
       't/015_temp_schema_exit_deferrable.pl',
+      't/016_explain_buffer_strategy.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_explain_buffer_strategy.pl b/src/test/modules/test_misc/t/016_explain_buffer_strategy.pl
new file mode 100644
index 00000000000..fb48278b81b
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_explain_buffer_strategy.pl
@@ -0,0 +1,167 @@
+# 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;
+$node->append_conf(
+	'postgresql.conf', qq{
+shared_buffers = '1MB'
+max_parallel_workers_per_gather = 0
+});
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE TABLE test_strategy AS
+  SELECT g, repeat('x', 1000) AS payload
+  FROM generate_series(1, 4000) AS g;
+ALTER TABLE test_strategy SET (parallel_workers = 2);
+CREATE INDEX test_strategy_g_idx ON test_strategy (g);
+CREATE TABLE test_normal AS SELECT 1 AS g;
+});
+
+my $seqscan = explain_json('SELECT * FROM test_strategy');
+is($seqscan->{'Node Type'}, 'Seq Scan', 'sequential scan plan');
+check_strategy($seqscan, 'bulkread', 'sequential scan');
+
+my $normal_scan = explain_json('SELECT * FROM test_normal');
+is($normal_scan->{'Node Type'}, 'Seq Scan', 'small sequential scan plan');
+check_strategy($normal_scan, 'normal', 'small sequential scan');
+
+my $never_executed_plan = decode_json(
+	$node->safe_psql(
+		'postgres', qq{
+SET plan_cache_mode = force_generic_plan;
+PREPARE never_execute(bool) AS SELECT * FROM test_normal WHERE \$1;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  EXECUTE never_execute(false);
+}))->[0]->{'Plan'};
+my $never_executed_scan = find_plan_node($never_executed_plan, 'Seq Scan');
+ok(defined $never_executed_scan,
+	'never-executed plan contains a sequential scan');
+is($never_executed_scan->{'Actual Loops'},
+	0, 'sequential scan was not executed');
+ok( !exists $never_executed_scan->{'Buffer Access Strategy'},
+	'strategy is not shown for a never-executed scan');
+
+# Sample scans use the buffer access strategy without a read stream.
+my $samplescan =
+  explain_json('SELECT * FROM test_strategy TABLESAMPLE SYSTEM (100)');
+is($samplescan->{'Node Type'}, 'Sample Scan', 'sample scan plan');
+check_strategy($samplescan, 'bulkread', 'sample scan');
+
+my $bitmap_plan = decode_json(
+	$node->safe_psql(
+		'postgres', qq{
+SET enable_seqscan = off;
+SET enable_indexscan = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT * FROM test_strategy WHERE g BETWEEN 1 AND 1000;
+}))->[0]->{'Plan'};
+my $bitmap_scan = find_plan_node($bitmap_plan, 'Bitmap Heap Scan');
+ok(defined $bitmap_scan, 'bitmap heap scan plan');
+check_strategy($bitmap_scan, 'normal', 'bitmap heap scan');
+
+my $tid_plan =
+  explain_json("SELECT * FROM test_strategy WHERE ctid = '(0,1)'");
+my $tid_scan = find_plan_node($tid_plan, 'Tid Scan');
+ok(defined $tid_scan, 'TID scan plan');
+check_strategy($tid_scan, 'normal', 'TID scan');
+
+my $tidrange_plan =
+  explain_json(
+	"SELECT * FROM test_strategy WHERE ctid >= '(0,1)' AND ctid < '(1,1)'");
+my $tidrange_scan = find_plan_node($tidrange_plan, 'Tid Range Scan');
+ok(defined $tidrange_scan, 'TID range scan plan');
+check_strategy($tidrange_scan, 'normal', 'TID range scan');
+
+my $without_buffers = decode_json(
+	$node->safe_psql(
+		'postgres',
+		'EXPLAIN (ANALYZE, BUFFERS OFF, IO, FORMAT JSON) '
+		  . 'SELECT * FROM test_strategy'))->[0]->{'Plan'};
+ok( !exists $without_buffers->{'Buffer Access Strategy'},
+	'strategy is not shown when BUFFERS is disabled');
+ok( exists $without_buffers->{'Average Prefetch Distance'},
+	'read stream statistics are shown when only IO is enabled');
+
+my $text = $node->safe_psql('postgres',
+		'EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, TIMING OFF, SUMMARY OFF) '
+	  . 'SELECT * FROM test_strategy');
+like(
+	$text,
+	qr/Buffer Access Strategy: bulkread/,
+	'text output reports the strategy');
+
+my $parallel_plan = decode_json(
+	$node->safe_psql(
+		'postgres', qq{
+SET max_parallel_workers_per_gather = 2;
+SET min_parallel_table_scan_size = 0;
+SET parallel_setup_cost = 0;
+SET parallel_tuple_cost = 0;
+SET parallel_leader_participation = off;
+EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
+  SELECT count(*) FROM test_strategy;
+}))->[0]->{'Plan'};
+my $gather = find_plan_node($parallel_plan, 'Gather');
+my $parallel_scan = find_plan_node($parallel_plan, 'Seq Scan');
+
+ok(defined $gather, 'parallel plan contains a Gather');
+cmp_ok($gather->{'Workers Launched'}, '>', 0, 'parallel workers launched');
+ok(defined $parallel_scan, 'parallel plan contains a sequential scan');
+ok( !exists $parallel_scan->{'Buffer Access Strategy'},
+	'strategy is not shown for a non-participating leader');
+my $workers = $parallel_scan->{'Workers'};
+ok(ref $workers eq 'ARRAY' && @$workers > 0,
+	'parallel scan reports worker strategy information');
+
+foreach my $worker (@$workers)
+{
+	is($worker->{'Buffer Access Strategy'},
+		'bulkread', 'parallel worker reports the strategy');
+}
+
+$node->stop;
+
+done_testing();
+
+sub explain_json
+{
+	my ($query) = @_;
+	my $output = $node->safe_psql('postgres',
+		"EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) $query");
+
+	return decode_json($output)->[0]->{'Plan'};
+}
+
+sub check_strategy
+{
+	my ($plan, $strategy, $description) = @_;
+
+	is($plan->{'Buffer Access Strategy'},
+		$strategy, "$description uses the $strategy strategy");
+}
+
+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;
+}
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 74a4d87801e..1b7a94f4e10 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -89,9 +89,10 @@ select explain_filter('explain (analyze, buffers, format text) select * from int
                                          explain_filter                                          
 -------------------------------------------------------------------------------------------------
  Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+   Buffer Access Strategy: normal
  Planning Time: N.N ms
  Execution Time: N.N ms
-(3 rows)
+(4 rows)
 
 select explain_filter('explain (buffers, format text) select * from int8_tbl i8');
                      explain_filter                      
@@ -119,6 +120,7 @@ explain_filter
       <Actual-Rows>N.N</Actual-Rows>
       <Actual-Loops>N</Actual-Loops>
       <Disabled>false</Disabled>
+      <Buffer-Access-Strategy>normal</Buffer-Access-Strategy>
       <Average-Prefetch-Distance>N.N</Average-Prefetch-Distance>
       <Max-Prefetch-Distance>N</Max-Prefetch-Distance>
       <Prefetch-Capacity>N</Prefetch-Capacity>
@@ -173,6 +175,7 @@ explain_filter
     Actual Rows: N.N
     Actual Loops: N
     Disabled: false
+    Buffer Access Strategy: "normal"
     Average Prefetch Distance: N.N
     Max Prefetch Distance: N
     Prefetch Capacity: N
@@ -301,65 +304,66 @@ select explain_filter('explain verbose select sum(unique1) over w1, sum(unique2)
 -- but always set in JSON format, so check them only in this case.
 set track_io_timing = on;
 select explain_filter('explain (analyze, buffers, format json) select * from int8_tbl i8');
-           explain_filter            
--------------------------------------
- [                                  +
-   {                                +
-     "Plan": {                      +
-       "Node Type": "Seq Scan",     +
-       "Parallel Aware": false,     +
-       "Async Capable": false,      +
-       "Relation Name": "int8_tbl", +
-       "Alias": "i8",               +
-       "Startup Cost": N.N,         +
-       "Total Cost": N.N,           +
-       "Plan Rows": N,              +
-       "Plan Width": N,             +
-       "Actual Startup Time": N.N,  +
-       "Actual Total Time": N.N,    +
-       "Actual Rows": N.N,          +
-       "Actual Loops": N,           +
-       "Disabled": false,           +
-       "Shared Hit Blocks": N,      +
-       "Shared Read Blocks": N,     +
-       "Shared Dirtied Blocks": N,  +
-       "Shared Written Blocks": N,  +
-       "Local Hit Blocks": N,       +
-       "Local Read Blocks": N,      +
-       "Local Dirtied Blocks": N,   +
-       "Local Written Blocks": N,   +
-       "Temp Read Blocks": N,       +
-       "Temp Written Blocks": N,    +
-       "Shared I/O Read Time": N.N, +
-       "Shared I/O Write Time": N.N,+
-       "Local I/O Read Time": N.N,  +
-       "Local I/O Write Time": N.N, +
-       "Temp I/O Read Time": N.N,   +
-       "Temp I/O Write Time": N.N   +
-     },                             +
-     "Planning": {                  +
-       "Shared Hit Blocks": N,      +
-       "Shared Read Blocks": N,     +
-       "Shared Dirtied Blocks": N,  +
-       "Shared Written Blocks": N,  +
-       "Local Hit Blocks": N,       +
-       "Local Read Blocks": N,      +
-       "Local Dirtied Blocks": N,   +
-       "Local Written Blocks": N,   +
-       "Temp Read Blocks": N,       +
-       "Temp Written Blocks": N,    +
-       "Shared I/O Read Time": N.N, +
-       "Shared I/O Write Time": N.N,+
-       "Local I/O Read Time": N.N,  +
-       "Local I/O Write Time": N.N, +
-       "Temp I/O Read Time": N.N,   +
-       "Temp I/O Write Time": N.N   +
-     },                             +
-     "Planning Time": N.N,          +
-     "Triggers": [                  +
-     ],                             +
-     "Execution Time": N.N          +
-   }                                +
+              explain_filter               
+-------------------------------------------
+ [                                        +
+   {                                      +
+     "Plan": {                            +
+       "Node Type": "Seq Scan",           +
+       "Parallel Aware": false,           +
+       "Async Capable": false,            +
+       "Relation Name": "int8_tbl",       +
+       "Alias": "i8",                     +
+       "Startup Cost": N.N,               +
+       "Total Cost": N.N,                 +
+       "Plan Rows": N,                    +
+       "Plan Width": N,                   +
+       "Actual Startup Time": N.N,        +
+       "Actual Total Time": N.N,          +
+       "Actual Rows": N.N,                +
+       "Actual Loops": N,                 +
+       "Disabled": false,                 +
+       "Buffer Access Strategy": "normal",+
+       "Shared Hit Blocks": N,            +
+       "Shared Read Blocks": N,           +
+       "Shared Dirtied Blocks": N,        +
+       "Shared Written Blocks": N,        +
+       "Local Hit Blocks": N,             +
+       "Local Read Blocks": N,            +
+       "Local Dirtied Blocks": N,         +
+       "Local Written Blocks": N,         +
+       "Temp Read Blocks": N,             +
+       "Temp Written Blocks": N,          +
+       "Shared I/O Read Time": N.N,       +
+       "Shared I/O Write Time": N.N,      +
+       "Local I/O Read Time": N.N,        +
+       "Local I/O Write Time": N.N,       +
+       "Temp I/O Read Time": N.N,         +
+       "Temp I/O Write Time": N.N         +
+     },                                   +
+     "Planning": {                        +
+       "Shared Hit Blocks": N,            +
+       "Shared Read Blocks": N,           +
+       "Shared Dirtied Blocks": N,        +
+       "Shared Written Blocks": N,        +
+       "Local Hit Blocks": N,             +
+       "Local Read Blocks": N,            +
+       "Local Dirtied Blocks": N,         +
+       "Local Written Blocks": N,         +
+       "Temp Read Blocks": N,             +
+       "Temp Written Blocks": N,          +
+       "Shared I/O Read Time": N.N,       +
+       "Shared I/O Write Time": N.N,      +
+       "Local I/O Read Time": N.N,        +
+       "Local I/O Write Time": N.N,       +
+       "Temp I/O Read Time": N.N,         +
+       "Temp I/O Write Time": N.N         +
+     },                                   +
+     "Planning Time": N.N,                +
+     "Triggers": [                        +
+     ],                                   +
+     "Execution Time": N.N                +
+   }                                      +
  ]
 (1 row)
 
@@ -436,55 +440,56 @@ select explain_filter('explain (memory, summary, format yaml) select * from int8
 (1 row)
 
 select explain_filter('explain (memory, analyze, format json) select * from int8_tbl i8');
-           explain_filter           
-------------------------------------
- [                                 +
-   {                               +
-     "Plan": {                     +
-       "Node Type": "Seq Scan",    +
-       "Parallel Aware": false,    +
-       "Async Capable": false,     +
-       "Relation Name": "int8_tbl",+
-       "Alias": "i8",              +
-       "Startup Cost": N.N,        +
-       "Total Cost": N.N,          +
-       "Plan Rows": N,             +
-       "Plan Width": N,            +
-       "Actual Startup Time": N.N, +
-       "Actual Total Time": N.N,   +
-       "Actual Rows": N.N,         +
-       "Actual Loops": N,          +
-       "Disabled": false,          +
-       "Shared Hit Blocks": N,     +
-       "Shared Read Blocks": N,    +
-       "Shared Dirtied Blocks": N, +
-       "Shared Written Blocks": N, +
-       "Local Hit Blocks": N,      +
-       "Local Read Blocks": N,     +
-       "Local Dirtied Blocks": N,  +
-       "Local Written Blocks": N,  +
-       "Temp Read Blocks": N,      +
-       "Temp Written Blocks": N    +
-     },                            +
-     "Planning": {                 +
-       "Shared Hit Blocks": N,     +
-       "Shared Read Blocks": N,    +
-       "Shared Dirtied Blocks": N, +
-       "Shared Written Blocks": N, +
-       "Local Hit Blocks": N,      +
-       "Local Read Blocks": N,     +
-       "Local Dirtied Blocks": N,  +
-       "Local Written Blocks": N,  +
-       "Temp Read Blocks": N,      +
-       "Temp Written Blocks": N,   +
-       "Memory Used": N,           +
-       "Memory Allocated": N       +
-     },                            +
-     "Planning Time": N.N,         +
-     "Triggers": [                 +
-     ],                            +
-     "Execution Time": N.N         +
-   }                               +
+              explain_filter               
+-------------------------------------------
+ [                                        +
+   {                                      +
+     "Plan": {                            +
+       "Node Type": "Seq Scan",           +
+       "Parallel Aware": false,           +
+       "Async Capable": false,            +
+       "Relation Name": "int8_tbl",       +
+       "Alias": "i8",                     +
+       "Startup Cost": N.N,               +
+       "Total Cost": N.N,                 +
+       "Plan Rows": N,                    +
+       "Plan Width": N,                   +
+       "Actual Startup Time": N.N,        +
+       "Actual Total Time": N.N,          +
+       "Actual Rows": N.N,                +
+       "Actual Loops": N,                 +
+       "Disabled": false,                 +
+       "Buffer Access Strategy": "normal",+
+       "Shared Hit Blocks": N,            +
+       "Shared Read Blocks": N,           +
+       "Shared Dirtied Blocks": N,        +
+       "Shared Written Blocks": N,        +
+       "Local Hit Blocks": N,             +
+       "Local Read Blocks": N,            +
+       "Local Dirtied Blocks": N,         +
+       "Local Written Blocks": N,         +
+       "Temp Read Blocks": N,             +
+       "Temp Written Blocks": N           +
+     },                                   +
+     "Planning": {                        +
+       "Shared Hit Blocks": N,            +
+       "Shared Read Blocks": N,           +
+       "Shared Dirtied Blocks": N,        +
+       "Shared Written Blocks": N,        +
+       "Local Hit Blocks": N,             +
+       "Local Read Blocks": N,            +
+       "Local Dirtied Blocks": N,         +
+       "Local Written Blocks": N,         +
+       "Temp Read Blocks": N,             +
+       "Temp Written Blocks": N,          +
+       "Memory Used": N,                  +
+       "Memory Allocated": N              +
+     },                                   +
+     "Planning Time": N.N,                +
+     "Triggers": [                        +
+     ],                                   +
+     "Execution Time": N.N                +
+   }                                      +
  ]
 (1 row)
 
@@ -542,6 +547,8 @@ select jsonb_pretty(
                          select * from tenk1 order by tenthous')
   -- remove "Workers" node of the Seq Scan plan node
   #- '{0,Plan,Plans,0,Plans,0,Workers}'
+  -- remove its buffer access strategy, as it depends on shared_buffers
+  #- '{0,Plan,Plans,0,Plans,0,Buffer Access Strategy}'
   -- remove "Workers" node of the Sort plan node
   #- '{0,Plan,Plans,0,Workers}'
   -- Also remove its sort-type fields, as those aren't 100% stable
@@ -767,19 +774,21 @@ select explain_filter('explain (analyze,serialize text,buffers,timing off) selec
                                   explain_filter                                   
 -----------------------------------------------------------------------------------
  Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N) (actual rows=N.N loops=N)
+   Buffer Access Strategy: normal
  Planning Time: N.N ms
  Serialization: output=NkB  format=text
  Execution Time: N.N ms
-(4 rows)
+(5 rows)
 
 select explain_filter('explain (analyze,serialize binary,buffers,timing) select * from int8_tbl i8');
                                          explain_filter                                          
 -------------------------------------------------------------------------------------------------
  Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+   Buffer Access Strategy: normal
  Planning Time: N.N ms
  Serialization: time=N.N ms  output=NkB  format=binary
  Execution Time: N.N ms
-(4 rows)
+(5 rows)
 
 -- this tests an edge case where we have no data to return
 select explain_filter('explain (analyze,buffers off,serialize) create temp table explain_temp as select * from int8_tbl i8');
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 2f163c64bf6..589e14a312a 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -151,6 +151,8 @@ select jsonb_pretty(
                          select * from tenk1 order by tenthous')
   -- remove "Workers" node of the Seq Scan plan node
   #- '{0,Plan,Plans,0,Plans,0,Workers}'
+  -- remove its buffer access strategy, as it depends on shared_buffers
+  #- '{0,Plan,Plans,0,Plans,0,Buffer Access Strategy}'
   -- remove "Workers" node of the Sort plan node
   #- '{0,Plan,Plans,0,Workers}'
   -- Also remove its sort-type fields, as those aren't 100% stable
-- 
2.34.1

