From 460f18ce96684ee844243eca28e8a79bed1143d8 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Wed, 19 Nov 2025 16:38:44 +0900
Subject: [PATCH v1] Strip PlaceHolderVars from index operands

When pulling up a subquery, we may need to wrap its targetlist items
in PlaceHolderVars to enforce separate identity or as a result of
outer joins.  However, this causes any upper-level WHERE clauses
referencing these outputs to contain PlaceHolderVars, which prevents
indxpath.c from recognizing that they could be matched to index
columns or index expressions, potentially affecting the planner's
ability to use indexes.

To fix, explicitly strip PlaceHolderVars from index operands.  This is
safe because a PlaceHolderVar appearing in a relation-scan-level
expression is effectively a no-op.
---
 src/backend/optimizer/path/indxpath.c      | 29 ++++++++++++++++++-
 src/backend/optimizer/plan/createplan.c    | 23 +++++++++++++--
 src/test/regress/expected/groupingsets.out | 33 ++++++++++++++++++++++
 src/test/regress/sql/groupingsets.sql      | 20 +++++++++++++
 4 files changed, 101 insertions(+), 4 deletions(-)

diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index c62e3f87724..bf07119939f 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -4349,7 +4349,34 @@ match_index_to_operand(Node *operand,
 	int			indkey;
 
 	/*
-	 * Ignore any RelabelType node above the operand.   This is needed to be
+	 * Ignore any PlaceHolderVar node above the operand.  This is needed to be
+	 * able to apply indexscanning in cases where the operand has been wrapped
+	 * in PlaceHolderVars to enforce separate identity or as a result of outer
+	 * joins.  Note: there may be multiple layers of PlaceHolderVar nodes, as
+	 * one PlaceHolderVar can be wrapped inside another.
+	 *
+	 * It's safe because a PlaceHolderVar appearing in a relation-scan-level
+	 * expression is effectively a no-op.
+	 *
+	 * To be on the safe side, punt if we encounter a PlaceHolderVar that is
+	 * marked nullable or whose syntactic scope is beyond this index.  XXX Is
+	 * it possible to have such a PlaceHolderVar at the relation-scan level?
+	 */
+	while (operand && IsA(operand, PlaceHolderVar))
+	{
+		PlaceHolderVar *phv = (PlaceHolderVar *) operand;
+		int			varno;
+
+		if (bms_is_empty(phv->phnullingrels) &&
+			bms_get_singleton_member(phv->phrels, &varno) &&
+			varno == index->rel->relid)
+			operand = (Node *) phv->phexpr;
+		else
+			return false;
+	}
+
+	/*
+	 * Ignore any RelabelType node above the operand.  This is needed to be
 	 * able to apply indexscanning in binary-compatible-operator cases. Note:
 	 * we can assume there is at most one RelabelType node;
 	 * eval_const_expressions() will have simplified if more than one.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8af091ba647..99413d7187c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -5100,7 +5100,8 @@ fix_indexqual_clause(PlannerInfo *root, IndexOptInfo *index, int indexcol,
  * equal to the index's attribute number (index column position).
  *
  * Most of the code here is just for sanity cross-checking that the given
- * expression actually matches the index column it's claimed to.
+ * expression actually matches the index column it's claimed to.  It should
+ * match the logic in match_index_to_operand().
  */
 static Node *
 fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
@@ -5109,14 +5110,30 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
 	int			pos;
 	ListCell   *indexpr_item;
 
+	Assert(indexcol >= 0 && indexcol < index->ncolumns);
+
+	/*
+	 * Remove any PlaceHolderVar wrapping of the indexkey
+	 */
+	while (IsA(node, PlaceHolderVar))
+	{
+		PlaceHolderVar *phv = (PlaceHolderVar *) node;
+		int			varno;
+
+		if (bms_is_empty(phv->phnullingrels) &&
+			bms_get_singleton_member(phv->phrels, &varno) &&
+			varno == index->rel->relid)
+			node = (Node *) phv->phexpr;
+		else
+			elog(ERROR, "index key does not match expected index column");
+	}
+
 	/*
 	 * Remove any binary-compatible relabeling of the indexkey
 	 */
 	if (IsA(node, RelabelType))
 		node = (Node *) ((RelabelType *) node)->arg;
 
-	Assert(indexcol >= 0 && indexcol < index->ncolumns);
-
 	if (index->indexkeys[indexcol] != 0)
 	{
 		/* It's a simple index column */
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 398cf6965e0..f3af6cb5797 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -463,6 +463,39 @@ select x, y || 'y'
    | 3y
 (8 rows)
 
+-- check that operands wrapped in PlaceHolderVars are capable of index matching
+begin;
+set local enable_bitmapscan = off;
+explain (costs off)
+select x, y
+  from (select unique1 as x, unique2 as y from tenk1) as t
+  where x = 1
+  group by grouping sets (x, y)
+  order by 1, 2;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Sort
+   Sort Key: tenk1.unique1, tenk1.unique2
+   ->  GroupAggregate
+         Group Key: tenk1.unique1
+         Sort Key: tenk1.unique2
+           Group Key: tenk1.unique2
+         ->  Index Scan using tenk1_unique1 on tenk1
+               Index Cond: (unique1 = 1)
+(8 rows)
+
+select x, y
+  from (select unique1 as x, unique2 as y from tenk1) as t
+  where x = 1
+  group by grouping sets (x, y)
+  order by 1, 2;
+ x |  y   
+---+------
+ 1 |     
+   | 2838
+(2 rows)
+
+rollback;
 -- check qual push-down rules for a subquery with grouping sets
 explain (verbose, costs off)
 select * from (
diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql
index 6d875475fae..3ea5b03e79d 100644
--- a/src/test/regress/sql/groupingsets.sql
+++ b/src/test/regress/sql/groupingsets.sql
@@ -183,6 +183,26 @@ select x, y || 'y'
   group by grouping sets (x, y)
   order by 1, 2;
 
+-- check that operands wrapped in PlaceHolderVars are capable of index matching
+begin;
+
+set local enable_bitmapscan = off;
+
+explain (costs off)
+select x, y
+  from (select unique1 as x, unique2 as y from tenk1) as t
+  where x = 1
+  group by grouping sets (x, y)
+  order by 1, 2;
+
+select x, y
+  from (select unique1 as x, unique2 as y from tenk1) as t
+  where x = 1
+  group by grouping sets (x, y)
+  order by 1, 2;
+
+rollback;
+
 -- check qual push-down rules for a subquery with grouping sets
 explain (verbose, costs off)
 select * from (
-- 
2.39.5 (Apple Git-154)

