diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 4fe8e5164c7..99caadb72b4 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -31,6 +31,7 @@ #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/placeholder.h" #include "optimizer/prep.h" #include "optimizer/restrictinfo.h" #include "utils/lsyscache.h" @@ -196,8 +197,6 @@ 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); /* @@ -4422,7 +4421,7 @@ match_index_to_operand(Node *operand, * a subtree) has been wrapped in PlaceHolderVars to enforce separate * identity or as a result of outer joins. */ - operand = strip_phvs_in_index_operand(operand); + operand = strip_noop_phvs(operand); /* * Ignore any RelabelType node above the operand. This is needed to be @@ -4488,88 +4487,15 @@ 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. + * Retained as a backward-compatibility wrapper around strip_noop_phvs() to + * avoid breaking third-party extensions that may reference this function. New + * code should call strip_noop_phvs() directly. */ 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); + return strip_noop_phvs(operand); } /* diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 06128a9f243..10b7358c28b 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -5271,7 +5271,7 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol) /* * Remove any PlaceHolderVar wrapping of the indexkey */ - node = strip_phvs_in_index_operand(node); + node = strip_noop_phvs(node); /* * Remove any binary-compatible relabeling of the indexkey diff --git a/src/backend/optimizer/util/placeholder.c b/src/backend/optimizer/util/placeholder.c index e1cd00a72fb..86e3c1e751f 100644 --- a/src/backend/optimizer/util/placeholder.c +++ b/src/backend/optimizer/util/placeholder.c @@ -35,6 +35,8 @@ static void find_placeholders_recurse(PlannerInfo *root, Node *jtnode); static void find_placeholders_in_expr(PlannerInfo *root, Node *expr); static bool contain_placeholder_references_walker(Node *node, contain_placeholder_references_context *context); +static bool contain_noop_phv_walker(Node *node, void *context); +static Node *strip_noop_phvs_mutator(Node *node, void *context); /* @@ -585,3 +587,92 @@ get_placeholder_nulling_relids(PlannerInfo *root, PlaceHolderInfo *phinfo) result = bms_del_members(result, phinfo->ph_eval_at); return result; } + +/* + * strip_noop_phvs + * Strip no-op PlaceHolderVar nodes from the given expression tree. + * + * A PlaceHolderVar that is not marked as nullable (i.e., its phnullingrels + * is empty) is effectively a no-op when it appears in a relation-scan-level + * expression. This function strips such PlaceHolderVars, which is useful + * for matching expressions to index keys or partition keys in cases where + * the expression has been wrapped in PlaceHolderVars during subquery pullup. + * + * IMPORTANT: the caller must ensure that the expression is a scan-level + * expression, so that non-nullable PlaceHolderVars in it are indeed no-ops. + * + * 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 expression. + * + * 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_noop_phvs(Node *node) +{ + /* Don't mutate/copy if no target PHVs exist */ + if (!contain_noop_phv_walker(node, NULL)) + return node; + + return strip_noop_phvs_mutator(node, NULL); +} + +/* + * contain_noop_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_noop_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_noop_phv_walker, + context); +} + +/* + * strip_noop_phvs_mutator + * Recursively remove PlaceHolderVars that are not marked nullable. + * + * We strip a PlaceHolderVar only if its phnullingrels is empty, replacing it + * with its contained expression. + */ +static Node * +strip_noop_phvs_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + if (bms_is_empty(phv->phnullingrels)) + { + /* Recurse on its contained expression */ + return strip_noop_phvs_mutator((Node *) phv->phexpr, + context); + } + + /* Otherwise, keep this PHV but check its contained expression */ + } + + return expression_tree_mutator(node, strip_noop_phvs_mutator, + context); +} diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index 48a35f763e9..2ef98d6ad6e 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -49,6 +49,7 @@ #include "optimizer/cost.h" #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" +#include "optimizer/placeholder.h" #include "parser/parsetree.h" #include "partitioning/partbounds.h" #include "partitioning/partprune.h" @@ -1814,6 +1815,15 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context, * and couldn't possibly match any other one either, due to its form or * properties (such as containing a volatile function). * Output arguments: none set. + * + * Note that when pulling up a subquery, the clause operands may get wrapped + * in PlaceHolderVars to enforce separate identity or as a result of outer + * joins. We must strip such no-op PlaceHolderVars before comparing operands + * to the partition key, otherwise the equal() checks will fail to recognize + * valid matches. This is safe because the clauses here are always + * relation-scan-level expressions, where a PlaceHolderVar with empty + * phnullingrels is effectively a no-op. Stripping may also bring separate + * RelabelType nodes into adjacency, so we must loop when peeling those. */ static PartClauseMatchStatus match_clause_to_partition_key(GeneratePruningStepsContext *context, @@ -1929,10 +1939,12 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, PartClauseInfo *partclause; leftop = (Expr *) get_leftop(clause); - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; rightop = (Expr *) get_rightop(clause); - if (IsA(rightop, RelabelType)) + rightop = (Expr *) strip_noop_phvs((Node *) rightop); + while (IsA(rightop, RelabelType)) rightop = ((RelabelType *) rightop)->arg; opno = opclause->opno; @@ -2180,7 +2192,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, *elem_clauses; ListCell *lc1; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; /* check if the LHS matches this partition key */ @@ -2406,7 +2419,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, NullTest *nulltest = (NullTest *) clause; Expr *arg = nulltest->arg; - if (IsA(arg, RelabelType)) + arg = (Expr *) strip_noop_phvs((Node *) arg); + while (IsA(arg, RelabelType)) arg = ((RelabelType *) arg)->arg; /* Does arg match with this partition key column? */ @@ -3718,7 +3732,8 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, BooleanTest *btest = (BooleanTest *) clause; leftop = btest->arg; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; if (equal(leftop, partkey)) @@ -3755,7 +3770,8 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, leftop = is_not_clause ? get_notclausearg(clause) : clause; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; /* Compare to the partition key, and make up a clause ... */ diff --git a/src/include/optimizer/placeholder.h b/src/include/optimizer/placeholder.h index db92d8861ba..0186f18bb06 100644 --- a/src/include/optimizer/placeholder.h +++ b/src/include/optimizer/placeholder.h @@ -32,5 +32,6 @@ extern bool contain_placeholder_references_to(PlannerInfo *root, Node *clause, int relid); extern Relids get_placeholder_nulling_relids(PlannerInfo *root, PlaceHolderInfo *phinfo); +extern Node *strip_noop_phvs(Node *node); #endif /* PLACEHOLDER_H */ diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index d1966cd7d82..7aa3e5cbaf9 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -4801,3 +4801,135 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o drop view part_abc_view; drop table part_abc; +-- +-- Check that operands wrapped in PlaceHolderVars are matched to partition +-- keys, allowing partition pruning to occur. PlaceHolderVars can be +-- introduced when a subquery's output is used with grouping sets. +-- +create table phv_part (a int, b text) partition by list (a); +create table phv_part_1 partition of phv_part for values in (1); +create table phv_part_2 partition of phv_part for values in (2); +create table phv_part_null partition of phv_part for values in (null); +insert into phv_part values (1, 'one'), (2, 'two'), (null, 'null'); +-- OpExpr: PHV-wrapped operand matched via equal() +explain (costs off) +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + QUERY PLAN +--------------------------------------- + MixedAggregate + Hash Key: phv_part.b + Group Key: phv_part.a + -> Seq Scan on phv_part_1 phv_part + Filter: (a = 1) +(5 rows) + +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + a | b +---+----- + 1 | + | one +(2 rows) + +-- OpExpr with RelabelType: PHV wrapped around a casted column +explain (costs off) +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + QUERY PLAN +------------------------------------------- + HashAggregate + Hash Key: (phv_part.a)::oid + Hash Key: phv_part.b + -> Seq Scan on phv_part_1 phv_part + Filter: (((a)::oid)::integer = 1) +(5 rows) + +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + x | b +---+----- + 1 | + | one +(2 rows) + +-- ScalarArrayOpExpr: IN clause with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + QUERY PLAN +--------------------------------------------------- + HashAggregate + Hash Key: phv_part.a + Hash Key: phv_part.b + -> Seq Scan on phv_part_1 phv_part + Filter: (a = ANY ('{1,NULL}'::integer[])) +(5 rows) + +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + a | b +---+----- + 1 | + | one +(2 rows) + +-- NullTest: IS NULL with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + QUERY PLAN +------------------------------------------ + HashAggregate + Hash Key: phv_part.a + Hash Key: phv_part.b + -> Seq Scan on phv_part_null phv_part + Filter: (a IS NULL) +(5 rows) + +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + a | b +---+------ + | + | null +(2 rows) + +drop table phv_part; +-- BooleanTest: IS TRUE with PHV-wrapped boolean partition key +create table phv_boolpart (a bool, b text) partition by list (a); +create table phv_boolpart_t partition of phv_boolpart for values in (true); +create table phv_boolpart_f partition of phv_boolpart for values in (false); +create table phv_boolpart_null partition of phv_boolpart default; +insert into phv_boolpart values (true, 'yes'), (false, 'no'), (null, 'unknown'); +explain (costs off) +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + QUERY PLAN +----------------------------------------------- + HashAggregate + Hash Key: phv_boolpart.a + Hash Key: phv_boolpart.b + -> Seq Scan on phv_boolpart_t phv_boolpart + Filter: (a IS TRUE) +(5 rows) + +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + a | b +---+----- + t | + | yes +(2 rows) + +drop table phv_boolpart; diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index d93c0c03bab..359a9208056 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -1447,3 +1447,74 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o drop view part_abc_view; drop table part_abc; + +-- +-- Check that operands wrapped in PlaceHolderVars are matched to partition +-- keys, allowing partition pruning to occur. PlaceHolderVars can be +-- introduced when a subquery's output is used with grouping sets. +-- +create table phv_part (a int, b text) partition by list (a); +create table phv_part_1 partition of phv_part for values in (1); +create table phv_part_2 partition of phv_part for values in (2); +create table phv_part_null partition of phv_part for values in (null); +insert into phv_part values (1, 'one'), (2, 'two'), (null, 'null'); + +-- OpExpr: PHV-wrapped operand matched via equal() +explain (costs off) +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +-- OpExpr with RelabelType: PHV wrapped around a casted column +explain (costs off) +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +-- ScalarArrayOpExpr: IN clause with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +-- NullTest: IS NULL with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +drop table phv_part; + +-- BooleanTest: IS TRUE with PHV-wrapped boolean partition key +create table phv_boolpart (a bool, b text) partition by list (a); +create table phv_boolpart_t partition of phv_boolpart for values in (true); +create table phv_boolpart_f partition of phv_boolpart for values in (false); +create table phv_boolpart_null partition of phv_boolpart default; +insert into phv_boolpart values (true, 'yes'), (false, 'no'), (null, 'unknown'); + +explain (costs off) +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +drop table phv_boolpart;