From 460f18ce96684ee844243eca28e8a79bed1143d8 Mon Sep 17 00:00:00 2001 From: Richard Guo 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)