From 0fa6941af7e189d33ef9dcdf286f1046cf17eb3c Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 15:40:31 -0700 Subject: [PATCH v1 12/16] Derive grouping keys from aggregates carrying DISTINCT agg(DISTINCT x) discards duplicate x before aggregating, so its arguments serve as grouping keys for a query with no GROUP BY or DISTINCT. Eager aggregation can then deduplicate on those keys beneath a to-many join. An aggregate without DISTINCT counts duplicates and rules this out, as does a FILTER clause. --- src/backend/optimizer/plan/initsplan.c | 280 ++++++++-- src/backend/optimizer/util/relnode.c | 13 +- src/test/regress/expected/eager_aggregate.out | 500 ++++++++++++++++++ src/test/regress/sql/eager_aggregate.sql | 232 ++++++++ 4 files changed, 984 insertions(+), 41 deletions(-) diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index d7ff206c1d3..afec4939654 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -98,6 +98,14 @@ typedef struct GroupByColInfo static bool is_partial_agg_memory_risky(PlannerInfo *root); static void create_agg_clause_infos(PlannerInfo *root); static bool grouping_key_usable(Expr *expr); +static Index max_sortgroupref(List *tlist); +static List *pull_agg_level_exprs(PlannerInfo *root); +static bool aggref_is_plain(Aggref *aggref); +static bool collect_distinct_agg_keys(Aggref *aggref, Index *nextref, + List **exprs, List **clauses); +static void add_grouping_expr_infos(PlannerInfo *root, List *exprs, + List *clauses); +static bool create_distinct_agg_grouping(PlannerInfo *root); static void create_grouping_expr_infos(PlannerInfo *root); static EquivalenceClass *get_eclass_for_sortgroupclause(PlannerInfo *root, SortGroupClause *sgc, @@ -711,9 +719,6 @@ setup_eager_aggregation(PlannerInfo *root) else if (root->parse->distinctClause && !root->parse->hasDistinctOn) root->eager_group_clause = root->processed_distinctClause; - if (root->eager_group_clause == NIL) - return; - /* * For now we don't try to support grouping sets. */ @@ -721,31 +726,46 @@ setup_eager_aggregation(PlannerInfo *root) return; /* - * For now we don't try to support DISTINCT or ORDER BY aggregates. + * We don't try to apply eager aggregation if there are set-returning + * functions in targetlist. */ - if (root->numOrderedAggs > 0) + if (root->parse->hasTargetSRFs) return; /* - * If there are any aggregates that do not support partial mode, or any - * partial aggregates that are non-serializable, do not apply eager - * aggregation. + * Eager aggregation only makes sense if there are multiple base rels in + * the query. */ - if (root->hasNonPartialAggs || root->hasNonSerialAggs) + if (bms_membership(root->all_baserels) != BMS_MULTIPLE) return; /* - * We don't try to apply eager aggregation if there are set-returning - * functions in targetlist. + * agg(DISTINCT x) supplies its arguments as grouping keys if the query + * has no GROUP BY or DISTINCT. The pushdown is then a deduplication, and + * the checks below govern aggregates, so return ahead of them. */ - if (root->parse->hasTargetSRFs) + if (root->eager_group_clause == NIL) + { + if (!create_distinct_agg_grouping(root)) + return; + + root->eager_dedup_only = true; + root->filter_only_rels = find_filter_only_rels(root); return; + } /* - * Eager aggregation only makes sense if there are multiple base rels in - * the query. + * For now we don't try to support DISTINCT or ORDER BY aggregates. */ - if (bms_membership(root->all_baserels) != BMS_MULTIPLE) + if (root->numOrderedAggs > 0) + return; + + /* + * If there are any aggregates that do not support partial mode, or any + * partial aggregates that are non-serializable, do not apply eager + * aggregation. + */ + if (root->hasNonPartialAggs || root->hasNonSerialAggs) return; /* @@ -1004,6 +1024,207 @@ grouping_key_usable(Expr *expr) ObjectIdGetDatum(tce->btree_opintype))); } +/* + * max_sortgroupref + * The largest sortgroupref the given targetlist uses. + */ +static Index +max_sortgroupref(List *tlist) +{ + Index maxref = 0; + ListCell *lc; + + foreach(lc, tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + + if (tle->ressortgroupref > maxref) + maxref = tle->ressortgroupref; + } + + return maxref; +} + +/* + * pull_agg_level_exprs + * The aggregates and plain Vars the query evaluates above the joins. + */ +static List * +pull_agg_level_exprs(PlannerInfo *root) +{ + List *exprs; + + exprs = pull_var_clause((Node *) root->processed_tlist, + PVC_INCLUDE_AGGREGATES | + PVC_RECURSE_WINDOWFUNCS | + PVC_RECURSE_PLACEHOLDERS); + + if (root->parse->havingQual != NULL) + exprs = list_concat(exprs, + pull_var_clause(root->parse->havingQual, + PVC_INCLUDE_AGGREGATES | + PVC_RECURSE_PLACEHOLDERS)); + + return exprs; +} + +/* + * aggref_is_plain + * Is the aggregate free of the decorations we cannot see through? + * + * FILTER picks which rows reach the aggregate, while ORDER BY and the direct + * arguments of an ordered-set aggregate make the result depend on the order + * they arrive in. A VARIADIC aggregate takes its arguments as an array, which + * we do not try to match against a grouping key. + */ +static bool +aggref_is_plain(Aggref *aggref) +{ + return (aggref->aggfilter == NULL && + aggref->aggorder == NIL && + aggref->aggdirectargs == NIL && + !aggref->aggvariadic); +} + +/* + * collect_distinct_agg_keys + * Make a grouping key of each expression the aggregate takes DISTINCT. + * + * Appends to *exprs and *clauses, numbering the new clauses from *nextref. + * Returns false if an expression cannot serve as a grouping key. + */ +static bool +collect_distinct_agg_keys(Aggref *aggref, Index *nextref, + List **exprs, List **clauses) +{ + ListCell *lc; + + foreach(lc, aggref->aggdistinct) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); + TargetEntry *tle; + SortGroupClause *key; + + tle = get_sortgroupclause_tle(sgc, aggref->args); + + if (!grouping_key_usable(tle->expr)) + return false; + + /* + * A key already collected from another aggregate needs no second + * clause; grouping on it once is enough. + */ + if (list_member(*exprs, tle->expr)) + continue; + + key = makeNode(SortGroupClause); + key->tleSortGroupRef = ++(*nextref); + key->eqop = sgc->eqop; + key->sortop = sgc->sortop; + key->nulls_first = sgc->nulls_first; + key->hashable = sgc->hashable; + + *exprs = lappend(*exprs, tle->expr); + *clauses = lappend(*clauses, key); + } + + return true; +} + +/* + * add_grouping_expr_infos + * Record a GroupingExprInfo for each of the given keys. + */ +static void +add_grouping_expr_infos(PlannerInfo *root, List *exprs, List *clauses) +{ + ListCell *lc; + ListCell *lc2; + + forboth(lc, exprs, lc2, clauses) + { + Expr *expr = (Expr *) lfirst(lc); + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc2); + GroupingExprInfo *ge_info; + + ge_info = makeNode(GroupingExprInfo); + ge_info->expr = (Expr *) copyObject(expr); + ge_info->sortgroupref = sgc->tleSortGroupRef; + ge_info->ec = get_eclass_for_sortgroupclause(root, sgc, expr); + + root->group_expr_list = lappend(root->group_expr_list, ge_info); + } +} + +/* + * create_distinct_agg_grouping + * Derive grouping keys from aggregates that ignore duplicate input rows. + * + * agg(DISTINCT x) discards duplicate x before aggregating, so its arguments + * serve as grouping keys for a query with no GROUP BY or DISTINCT. Eager + * aggregation can then deduplicate on those keys beneath a to-many join, and + * since the keys are the arguments, the deduplicated rows still carry what + * the aggregates above read. + * + * An aggregate without DISTINCT counts duplicates and rules this out, as does + * a FILTER clause, whose result depends on which rows arrive. + * + * Returns true if such keys were found, having set root->eager_group_clause + * and root->group_expr_list. + */ +static bool +create_distinct_agg_grouping(PlannerInfo *root) +{ + List *exprs = NIL; + List *clauses = NIL; + List *agg_level_exprs; + Index nextref; + ListCell *lc; + + if (!root->parse->hasAggs) + return false; + + /* The synthesized clauses take the refs the query has left over */ + nextref = max_sortgroupref(root->processed_tlist); + + agg_level_exprs = pull_agg_level_exprs(root); + + foreach(lc, agg_level_exprs) + { + Expr *expr = (Expr *) lfirst(lc); + Aggref *aggref; + + /* + * A plain Var outside an aggregate would have to be a grouping key, + * and there is no GROUP BY here, so this cannot happen. + */ + if (!IsA(expr, Aggref)) + return false; + + aggref = (Aggref *) expr; + + if (aggref->aggdistinct == NIL || !aggref_is_plain(aggref)) + return false; + + if (!collect_distinct_agg_keys(aggref, &nextref, &exprs, &clauses)) + return false; + } + + list_free(agg_level_exprs); + + if (exprs == NIL) + return false; + + /* + * These keys have no TargetEntry to be found from, so + * create_grouping_expr_infos() cannot build their GroupingExprInfos. + */ + add_grouping_expr_infos(root, exprs, clauses); + + root->eager_group_clause = clauses; + return true; +} + /* * create_grouping_expr_infos * Create a GroupingExprInfo for each expression usable as grouping key. @@ -1015,12 +1236,8 @@ static void create_grouping_expr_infos(PlannerInfo *root) { List *exprs = NIL; - List *sortgrouprefs = NIL; - List *ecs = NIL; - ListCell *lc, - *lc1, - *lc2, - *lc3; + List *clauses = NIL; + ListCell *lc; Assert(root->group_expr_list == NIL); @@ -1035,27 +1252,10 @@ create_grouping_expr_infos(PlannerInfo *root) return; exprs = lappend(exprs, tle->expr); - sortgrouprefs = lappend_int(sortgrouprefs, tle->ressortgroupref); - ecs = lappend(ecs, get_eclass_for_sortgroupclause(root, sgc, tle->expr)); + clauses = lappend(clauses, sgc); } - /* - * Construct a GroupingExprInfo for each expression. - */ - forthree(lc1, exprs, lc2, sortgrouprefs, lc3, ecs) - { - Expr *expr = (Expr *) lfirst(lc1); - int sortgroupref = lfirst_int(lc2); - EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc3); - GroupingExprInfo *ge_info; - - ge_info = makeNode(GroupingExprInfo); - ge_info->expr = (Expr *) copyObject(expr); - ge_info->sortgroupref = sortgroupref; - ge_info->ec = ec; - - root->group_expr_list = lappend(root->group_expr_list, ge_info); - } + add_grouping_expr_infos(root, exprs, clauses); } /* diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 44de2cebb73..886999a6000 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -2985,7 +2985,11 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, List *possibly_dependent = NIL; Index maxSortGroupRef; - /* Identify the max sortgroupref */ + /* + * Identify the max sortgroupref. Grouping clauses synthesized from + * DISTINCT aggregates carry refs of their own, past the targetlist's, so + * they have to be counted too. + */ maxSortGroupRef = 0; foreach(lc, root->processed_tlist) { @@ -2994,6 +2998,13 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, if (ref > maxSortGroupRef) maxSortGroupRef = ref; } + foreach(lc, root->eager_group_clause) + { + Index ref = lfirst_node(SortGroupClause, lc)->tleSortGroupRef; + + if (ref > maxSortGroupRef) + maxSortGroupRef = ref; + } /* * At this point, all Vars from this relation that are needed by upper diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index a77234e353b..12302ee14cb 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -2553,3 +2553,503 @@ RESET enable_eager_aggregate; DROP TABLE eager_semi_d; DROP TABLE eager_semi_f1; DROP TABLE eager_semi_f2; +-- +-- Test that a DISTINCT aggregate supplies the grouping keys, so that a query +-- with no GROUP BY of its own can still deduplicate below the join +-- +CREATE TABLE eager_distinct_a1 (id int PRIMARY KEY, title text, k numeric); +CREATE TABLE eager_distinct_a2 (id int PRIMARY KEY, a1_id int, flag bool); +CREATE TABLE eager_distinct_a3 (id int PRIMARY KEY, a2_id int, flag bool); +INSERT INTO eager_distinct_a1 + SELECT i, 'p' || i, i::numeric FROM generate_series(1, 100) i; +INSERT INTO eager_distinct_a2 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 1000) i; +INSERT INTO eager_distinct_a3 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 10000) i; +CREATE INDEX ON eager_distinct_a2 (a1_id); +CREATE INDEX ON eager_distinct_a3 (a2_id); +ANALYZE eager_distinct_a1; +ANALYZE eager_distinct_a2; +ANALYZE eager_distinct_a3; +-- count(DISTINCT) discards the duplicate rows the join produces, so they need +-- not be produced at all +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Partial HashAggregate + Group Key: a2.a1_id + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(13 rows) + +-- sum(DISTINCT) is insensitive to duplicates in the same way +EXPLAIN (COSTS OFF) +SELECT sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Partial HashAggregate + Group Key: a2.a1_id + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(13 rows) + +-- Each DISTINCT aggregate contributes a grouping key +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id, a1.title + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Partial HashAggregate + Group Key: a2.a1_id + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(13 rows) + +-- On a chain the deduplication reaches every hop, as it does for DISTINCT +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id + -> Nested Loop Semi Join + -> Seq Scan on eager_distinct_a1 a1 + -> Nested Loop + -> Index Scan using eager_distinct_a2_a1_id_idx on eager_distinct_a2 a2 + Index Cond: (a1_id = a1.id) + -> Index Scan using eager_distinct_a3_a2_id_idx on eager_distinct_a3 a3 + Index Cond: (a2_id = a2.id) + Filter: flag +(13 rows) + +SELECT count(DISTINCT a1.id), sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count | sum +-------+------ + 100 | 5050 +(1 row) + +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count | count +-------+------- + 100 | 100 +(1 row) + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + count +------- + 100 +(1 row) + +SET enable_eager_aggregate TO off; +SELECT count(DISTINCT a1.id), sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count | sum +-------+------ + 100 | 5050 +(1 row) + +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count | count +-------+------- + 100 | 100 +(1 row) + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + count +------- + 100 +(1 row) + +RESET enable_eager_aggregate; +-- An aggregate that counts the duplicates needs every one of them +EXPLAIN (COSTS OFF) +SELECT count(*) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------- + Aggregate + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(7 rows) + +-- ... and so does one that sums over the driver without discarding them +EXPLAIN (COSTS OFF) +SELECT sum(a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------- + Aggregate + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(7 rows) + +-- One such aggregate is enough to require them, even beside a DISTINCT one +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id), count(*) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +-- A DISTINCT aggregate over the to-many side makes its rows observable +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a2.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a2.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +-- An aggregate carrying FILTER or its own ORDER BY is left alone +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) FILTER (WHERE a1.id > 50) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id ORDER BY a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +-- Equality does not imply image equality for numeric, so such an argument +-- cannot become a grouping key +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.k) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.k + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +-- Nor can an argument that is not a plain column +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id + 1) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: ((a1.id + 1)) + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +-- A HAVING clause naming the same aggregate is no obstacle +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag +HAVING count(DISTINCT a1.id) > 5; + QUERY PLAN +---------------------------------------------------------------- + Aggregate + Filter: (count(DISTINCT a1.id) > 5) + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Partial HashAggregate + Group Key: a2.a1_id + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(14 rows) + +-- ... but one that counts the duplicates is +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag +HAVING count(*) > 5; + QUERY PLAN +---------------------------------------------------------- + Aggregate + Filter: (count(*) > 5) + -> Sort + Sort Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(10 rows) + +-- A DISTINCT aggregate beside a grouping clause of the query's own is no +-- obstacle, though at this size the plain join still costs less +EXPLAIN (COSTS OFF) +SELECT a1.id, count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag + GROUP BY a1.id; + QUERY PLAN +---------------------------------------------------------- + GroupAggregate + Group Key: a1.id + -> Sort + Sort Key: a1.id, a1.title + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(10 rows) + +-- The deduplication threshold governs the derived keys too +SET min_eager_distinct_group_size TO 8; +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count +------- + 100 +(1 row) + +RESET min_eager_distinct_group_size; +-- Here the side worth deduplicating is the nullable one, which nothing may +-- be pushed to, so the count is taken over the join as it stands +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + QUERY PLAN +---------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> Hash Right Join + Hash Cond: (a2.a1_id = a1.id) + -> Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Seq Scan on eager_distinct_a1 a1 +(9 rows) + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + count +------- + 100 +(1 row) + +SET enable_eager_aggregate TO off; +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + count +------- + 100 +(1 row) + +RESET enable_eager_aggregate; +-- The derived keys reach parallel plans as well +SET parallel_setup_cost=0; +SET parallel_tuple_cost=0; +SET min_parallel_table_scan_size=0; +SET max_parallel_workers_per_gather=4; +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + QUERY PLAN +------------------------------------------------------------------------------- + Aggregate + -> Sort + Sort Key: a1.id + -> HashAggregate + Group Key: a1.id + -> Hash Join + Hash Cond: (a2.a1_id = a1.id) + -> Partial HashAggregate + Group Key: a2.a1_id + -> Gather + Workers Planned: 2 + -> Parallel Seq Scan on eager_distinct_a2 a2 + Filter: flag + -> Hash + -> Gather + Workers Planned: 1 + -> Parallel Seq Scan on eager_distinct_a1 a1 +(17 rows) + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + count +------- + 100 +(1 row) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP TABLE eager_distinct_a1; +DROP TABLE eager_distinct_a2; +DROP TABLE eager_distinct_a3; diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql index 32650fa945c..224d2b98123 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -745,3 +745,235 @@ RESET enable_eager_aggregate; DROP TABLE eager_semi_d; DROP TABLE eager_semi_f1; DROP TABLE eager_semi_f2; + + +-- +-- Test that a DISTINCT aggregate supplies the grouping keys, so that a query +-- with no GROUP BY of its own can still deduplicate below the join +-- + +CREATE TABLE eager_distinct_a1 (id int PRIMARY KEY, title text, k numeric); +CREATE TABLE eager_distinct_a2 (id int PRIMARY KEY, a1_id int, flag bool); +CREATE TABLE eager_distinct_a3 (id int PRIMARY KEY, a2_id int, flag bool); + +INSERT INTO eager_distinct_a1 + SELECT i, 'p' || i, i::numeric FROM generate_series(1, 100) i; +INSERT INTO eager_distinct_a2 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 1000) i; +INSERT INTO eager_distinct_a3 + SELECT i, ((i - 1) / 10) + 1, i % 2 = 0 FROM generate_series(1, 10000) i; + +CREATE INDEX ON eager_distinct_a2 (a1_id); +CREATE INDEX ON eager_distinct_a3 (a2_id); + +ANALYZE eager_distinct_a1; +ANALYZE eager_distinct_a2; +ANALYZE eager_distinct_a3; + +-- count(DISTINCT) discards the duplicate rows the join produces, so they need +-- not be produced at all +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- sum(DISTINCT) is insensitive to duplicates in the same way +EXPLAIN (COSTS OFF) +SELECT sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- Each DISTINCT aggregate contributes a grouping key +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- On a chain the deduplication reaches every hop, as it does for DISTINCT +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + +SELECT count(DISTINCT a1.id), sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + +SET enable_eager_aggregate TO off; + +SELECT count(DISTINCT a1.id), sum(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id), count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + JOIN eager_distinct_a3 a3 ON a3.a2_id = a2.id + WHERE a3.flag; + +RESET enable_eager_aggregate; + +-- An aggregate that counts the duplicates needs every one of them +EXPLAIN (COSTS OFF) +SELECT count(*) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- ... and so does one that sums over the driver without discarding them +EXPLAIN (COSTS OFF) +SELECT sum(a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- One such aggregate is enough to require them, even beside a DISTINCT one +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id), count(*) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- A DISTINCT aggregate over the to-many side makes its rows observable +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a2.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- An aggregate carrying FILTER or its own ORDER BY is left alone +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) FILTER (WHERE a1.id > 50) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id ORDER BY a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- Equality does not imply image equality for numeric, so such an argument +-- cannot become a grouping key +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.k) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- Nor can an argument that is not a plain column +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id + 1) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +-- A HAVING clause naming the same aggregate is no obstacle +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag +HAVING count(DISTINCT a1.id) > 5; + +-- ... but one that counts the duplicates is +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag +HAVING count(*) > 5; + +-- A DISTINCT aggregate beside a grouping clause of the query's own is no +-- obstacle, though at this size the plain join still costs less +EXPLAIN (COSTS OFF) +SELECT a1.id, count(DISTINCT a1.title) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag + GROUP BY a1.id; + +-- The deduplication threshold governs the derived keys too +SET min_eager_distinct_group_size TO 8; + +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +RESET min_eager_distinct_group_size; + +-- Here the side worth deduplicating is the nullable one, which nothing may +-- be pushed to, so the count is taken over the join as it stands +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + +SET enable_eager_aggregate TO off; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + LEFT JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id AND a2.flag; + +RESET enable_eager_aggregate; + +-- The derived keys reach parallel plans as well +SET parallel_setup_cost=0; +SET parallel_tuple_cost=0; +SET min_parallel_table_scan_size=0; +SET max_parallel_workers_per_gather=4; + +EXPLAIN (COSTS OFF) +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +SELECT count(DISTINCT a1.id) + FROM eager_distinct_a1 a1 + JOIN eager_distinct_a2 a2 ON a2.a1_id = a1.id + WHERE a2.flag; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP TABLE eager_distinct_a1; +DROP TABLE eager_distinct_a2; +DROP TABLE eager_distinct_a3;