From 8ce54e0e21c3c669c0a4834e8cdb7fb42506f93a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20D=C3=B6tsch?= <matthias.doetsch@innogames.com>
Date: Tue, 13 Oct 2026 13:12:12 +0200
Subject: [PATCH v2] Specialize ExecIndexOnlyScan() like ExecSeqScan()

ExecIndexOnlyScan() went through the out-of-line ExecScan(), which
calls IndexOnlyNext() through a function pointer for every tuple and
checks for EPQ, qual and projection at runtime.  As fb9f95502 and
83a53572a6f did for SeqScan, add ExecIndexOnlyScan*() variants that
pass const-NULLs to the inline ExecScanExtended() for whatever the node
doesn't need, and choose one in ExecInitIndexOnlyScan().  The EPQ case
keeps using ExecScan().  Mark IndexOnlyNext() as pg_always_inline so
that it gets inlined into each variant.

The runtime-key setup has to come before the pg_assume() calls,
because the compiler can't rely on them across the ExecReScan() call.
It would then keep the code for the no-qual, no-projection case too,
with a second copy of IndexOnlyNext().

In a 1M-row index-only range scan (gcc 12, -O2), executed instructions
drop by 5.2% without qual or projection, by 4.8% with a qual and by
6.3% with a projection (callgrind).  Wall-clock time for the same
queries drops by 3-11%, but that varies by several percent with code
layout alone.
---
 src/backend/executor/nodeIndexonlyscan.c | 145 ++++++++++++++++++++++-
 1 file changed, 140 insertions(+), 5 deletions(-)

diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index b1009027ac8..20d798e3e02 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -33,6 +33,7 @@
 #include "access/genam.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
+#include "executor/execScan.h"
 #include "executor/executor.h"
 #include "executor/instrument.h"
 #include "executor/nodeIndexonlyscan.h"
@@ -50,7 +51,7 @@ static TupleTableSlot *IndexOnlyNext(IndexOnlyScanState *node);
  *		Retrieve a tuple from the IndexOnlyScan node's index.
  * ----------------------------------------------------------------
  */
-static TupleTableSlot *
+static pg_always_inline TupleTableSlot *
 IndexOnlyNext(IndexOnlyScanState *node)
 {
 	EState	   *estate;
@@ -164,8 +165,28 @@ IndexOnlyRecheck(IndexOnlyScanState *node, TupleTableSlot *slot)
 	return false;				/* keep compiler quiet */
 }
 
+/*
+ * IndexOnlyScanSetupRuntimeKeys -- compute runtime keys if not done yet
+ */
+static pg_always_inline void
+IndexOnlyScanSetupRuntimeKeys(IndexOnlyScanState *node)
+{
+	/*
+	 * If we have runtime keys and they've not already been set up, do it now.
+	 */
+	if (node->ioss_NumRuntimeKeys != 0 && !node->ioss_RuntimeKeysReady)
+		ExecReScan((PlanState *) node);
+}
+
 /* ----------------------------------------------------------------
  *		ExecIndexOnlyScan(node)
+ *
+ *		Returns the next qualifying tuple from the index-only scan.  This
+ *		variant is used when there is no es_epq_active, no qual and no
+ *		projection.  As with the ExecSeqScan() variants, passing const-NULLs
+ *		for these to ExecScanExtended() allows the compiler to eliminate the
+ *		additional code that would ordinarily be required for the evaluation
+ *		of these, and to inline IndexOnlyNext().
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
@@ -173,11 +194,104 @@ ExecIndexOnlyScan(PlanState *pstate)
 {
 	IndexOnlyScanState *node = castNode(IndexOnlyScanState, pstate);
 
+	IndexOnlyScanSetupRuntimeKeys(node);
+
+	Assert(pstate->state->es_epq_active == NULL);
+	Assert(pstate->qual == NULL);
+	Assert(pstate->ps_ProjInfo == NULL);
+
+	return ExecScanExtended(&node->ss,
+							(ExecScanAccessMtd) IndexOnlyNext,
+							(ExecScanRecheckMtd) IndexOnlyRecheck,
+							NULL,
+							NULL,
+							NULL);
+}
+
+/*
+ * Variant of ExecIndexOnlyScan() but when qual evaluation is required.
+ */
+static TupleTableSlot *
+ExecIndexOnlyScanWithQual(PlanState *pstate)
+{
+	IndexOnlyScanState *node = castNode(IndexOnlyScanState, pstate);
+
+	IndexOnlyScanSetupRuntimeKeys(node);
+
 	/*
-	 * If we have runtime keys and they've not already been set up, do it now.
+	 * Use pg_assume() for != NULL tests to make the compiler realize no
+	 * runtime check for the field is needed in ExecScanExtended().  This has
+	 * to come after IndexOnlyScanSetupRuntimeKeys(), as the compiler can't
+	 * know that ExecReScan() leaves the field alone.
 	 */
-	if (node->ioss_NumRuntimeKeys != 0 && !node->ioss_RuntimeKeysReady)
-		ExecReScan((PlanState *) node);
+	Assert(pstate->state->es_epq_active == NULL);
+	pg_assume(pstate->qual != NULL);
+	Assert(pstate->ps_ProjInfo == NULL);
+
+	return ExecScanExtended(&node->ss,
+							(ExecScanAccessMtd) IndexOnlyNext,
+							(ExecScanRecheckMtd) IndexOnlyRecheck,
+							NULL,
+							pstate->qual,
+							NULL);
+}
+
+/*
+ * Variant of ExecIndexOnlyScan() but when projection is required.
+ */
+static TupleTableSlot *
+ExecIndexOnlyScanWithProject(PlanState *pstate)
+{
+	IndexOnlyScanState *node = castNode(IndexOnlyScanState, pstate);
+
+	IndexOnlyScanSetupRuntimeKeys(node);
+
+	Assert(pstate->state->es_epq_active == NULL);
+	Assert(pstate->qual == NULL);
+	pg_assume(pstate->ps_ProjInfo != NULL);
+
+	return ExecScanExtended(&node->ss,
+							(ExecScanAccessMtd) IndexOnlyNext,
+							(ExecScanRecheckMtd) IndexOnlyRecheck,
+							NULL,
+							NULL,
+							pstate->ps_ProjInfo);
+}
+
+/*
+ * Variant of ExecIndexOnlyScan() but when qual evaluation and projection are
+ * required.
+ */
+static TupleTableSlot *
+ExecIndexOnlyScanWithQualProject(PlanState *pstate)
+{
+	IndexOnlyScanState *node = castNode(IndexOnlyScanState, pstate);
+
+	IndexOnlyScanSetupRuntimeKeys(node);
+
+	Assert(pstate->state->es_epq_active == NULL);
+	pg_assume(pstate->qual != NULL);
+	pg_assume(pstate->ps_ProjInfo != NULL);
+
+	return ExecScanExtended(&node->ss,
+							(ExecScanAccessMtd) IndexOnlyNext,
+							(ExecScanRecheckMtd) IndexOnlyRecheck,
+							NULL,
+							pstate->qual,
+							pstate->ps_ProjInfo);
+}
+
+/*
+ * Variant of ExecIndexOnlyScan() for when EPQ evaluation is required.  As
+ * with ExecSeqScanEPQ(), we don't bother adding variants of this for
+ * with/without qual and projection, and just use the out-of-line ExecScan().
+ */
+static TupleTableSlot *
+ExecIndexOnlyScanEPQ(PlanState *pstate)
+{
+	IndexOnlyScanState *node = castNode(IndexOnlyScanState, pstate);
+
+	IndexOnlyScanSetupRuntimeKeys(node);
 
 	return ExecScan(&node->ss,
 					(ExecScanAccessMtd) IndexOnlyNext,
@@ -368,7 +482,6 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 	indexstate = makeNode(IndexOnlyScanState);
 	indexstate->ss.ps.plan = (Plan *) node;
 	indexstate->ss.ps.state = estate;
-	indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScan;
 
 	/*
 	 * Miscellaneous initialization
@@ -416,6 +529,28 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
 	indexstate->recheckqual =
 		ExecInitQual(node->recheckqual, (PlanState *) indexstate);
 
+	/*
+	 * Assign ExecProcNode based on whether EvalPlanQual is in use, and on the
+	 * presence of qual and projection.  Each ExecIndexOnlyScan*() variant is
+	 * optimized for the specific combination of these conditions.
+	 */
+	if (estate->es_epq_active != NULL)
+		indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScanEPQ;
+	else if (indexstate->ss.ps.qual == NULL)
+	{
+		if (indexstate->ss.ps.ps_ProjInfo == NULL)
+			indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScan;
+		else
+			indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScanWithProject;
+	}
+	else
+	{
+		if (indexstate->ss.ps.ps_ProjInfo == NULL)
+			indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScanWithQual;
+		else
+			indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScanWithQualProject;
+	}
+
 	/*
 	 * If we are just doing EXPLAIN (ie, aren't going to run the plan), stop
 	 * here.  This allows an index-advisor plugin to EXPLAIN a plan containing
-- 
2.53.0

