diff options
| author | Richard Guo <rguo@postgresql.org> | 2025-12-29 11:38:49 +0900 |
|---|---|---|
| committer | Richard Guo <rguo@postgresql.org> | 2025-12-29 11:38:49 +0900 |
| commit | ad66f705fa6796b40311a8210e9f37144df02ef5 (patch) | |
| tree | d2063f9ad00746a1df068c5c24ab55f9dd2fc0bd /src/backend/optimizer | |
| parent | b7057e43467ff2d7c04c3abcf5ec35fcc7db9611 (diff) | |
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. A
PlaceHolderVar appearing in a relation-scan-level expression is
effectively a no-op. Nevertheless, to play it safe, we strip only
PlaceHolderVars that are not marked nullable.
The stripping is performed recursively to handle cases where
PlaceHolderVars are nested or interleaved with other node types. To
minimize performance impact, we first use a lightweight walker to
check for the presence of strippable PlaceHolderVars. The expensive
mutator is invoked only if a candidate is found, avoiding unnecessary
memory allocation and tree copying in the common case where no
PlaceHolderVars are present.
Back-patch to v18. Although this issue exists before that, changes in
this version made it common enough to notice. Given the lack of field
reports for older versions, I am not back-patching further.
Reported-by: Haowu Ge <gehaowu@bitmoe.com>
Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/62af586c-c270-44f3-9c5e-02c81d537e3d.gehaowu@bitmoe.com
Backpatch-through: 18
Diffstat (limited to 'src/backend/optimizer')
| -rw-r--r-- | src/backend/optimizer/path/indxpath.c | 109 | ||||
| -rw-r--r-- | src/backend/optimizer/plan/createplan.c | 14 |
2 files changed, 114 insertions, 9 deletions
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 5d4f81ee77e..cf2b5f95d13 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -197,6 +197,8 @@ static Expr *match_clause_to_ordering_op(IndexOptInfo *index, static bool ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel, EquivalenceClass *ec, EquivalenceMember *em, void *arg); +static bool contain_strippable_phv_walker(Node *node, void *context); +static Node *strip_phvs_in_index_operand_mutator(Node *node, void *context); /* @@ -4358,12 +4360,23 @@ match_index_to_operand(Node *operand, int indkey; /* - * 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. + * Ignore any PlaceHolderVar node contained in the operand. This is + * needed to be able to apply indexscanning in cases where the operand (or + * a subtree) has been wrapped in PlaceHolderVars to enforce separate + * identity or as a result of outer joins. */ - if (operand && IsA(operand, RelabelType)) + operand = strip_phvs_in_index_operand(operand); + + /* + * Ignore any RelabelType node above the operand. This is needed to be + * able to apply indexscanning in binary-compatible-operator cases. + * + * Note: we must handle nested RelabelType nodes here. While + * eval_const_expressions() will have simplified them to at most one + * layer, our prior stripping of PlaceHolderVars may have brought separate + * RelabelTypes into adjacency. + */ + while (operand && IsA(operand, RelabelType)) operand = (Node *) ((RelabelType *) operand)->arg; indkey = index->indexkeys[indexcol]; @@ -4417,6 +4430,92 @@ match_index_to_operand(Node *operand, } /* + * strip_phvs_in_index_operand + * Strip PlaceHolderVar nodes from the given operand expression to + * facilitate matching against an index's key. + * + * A PlaceHolderVar appearing in a relation-scan-level expression is + * effectively a no-op. Nevertheless, to play it safe, we strip only + * PlaceHolderVars that are not marked nullable. + * + * The removal is performed recursively because PlaceHolderVars can be nested + * or interleaved with other node types. We must peel back all layers to + * expose the base operand. + * + * As a performance optimization, we first use a lightweight walker to check + * for the presence of strippable PlaceHolderVars. The expensive mutator is + * invoked only if a candidate is found, avoiding unnecessary memory allocation + * and tree copying in the common case where no PlaceHolderVars are present. + */ +Node * +strip_phvs_in_index_operand(Node *operand) +{ + /* Don't mutate/copy if no target PHVs exist */ + if (!contain_strippable_phv_walker(operand, NULL)) + return operand; + + return strip_phvs_in_index_operand_mutator(operand, NULL); +} + +/* + * contain_strippable_phv_walker + * Detect if there are any PlaceHolderVars in the tree that are candidates + * for stripping. + * + * We identify a PlaceHolderVar as strippable only if its phnullingrels is + * empty. + */ +static bool +contain_strippable_phv_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + if (bms_is_empty(phv->phnullingrels)) + return true; + } + + return expression_tree_walker(node, contain_strippable_phv_walker, + context); +} + +/* + * strip_phvs_in_index_operand_mutator + * Recursively remove PlaceHolderVars in the tree that match the criteria. + * + * We strip a PlaceHolderVar only if its phnullingrels is empty, replacing it + * with its contained expression. + */ +static Node * +strip_phvs_in_index_operand_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + /* If matches the criteria, strip it */ + if (bms_is_empty(phv->phnullingrels)) + { + /* Recurse on its contained expression */ + return strip_phvs_in_index_operand_mutator((Node *) phv->phexpr, + context); + } + + /* Otherwise, keep this PHV but check its contained expression */ + } + + return expression_tree_mutator(node, strip_phvs_in_index_operand_mutator, + context); +} + +/* * is_pseudo_constant_for_index() * Test whether the given expression can be used as an indexscan * comparison value. diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index bc417f93840..f1a01cfc544 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,19 @@ 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 + */ + node = strip_phvs_in_index_operand(node); + /* * Remove any binary-compatible relabeling of the indexkey */ - if (IsA(node, RelabelType)) + while (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 */ |
