From 26d47aa8d6367c27ccda2c2c30dd3afead216b3b Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 15:40:31 -0700 Subject: [PATCH v2 13/17] 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. --- src/backend/optimizer/plan/initsplan.c | 218 +++++++- src/backend/optimizer/util/relnode.c | 13 +- src/test/regress/expected/eager_aggregate.out | 501 ++++++++++++++++++ src/test/regress/sql/eager_aggregate.sql | 233 ++++++++ 4 files changed, 949 insertions(+), 16 deletions(-) diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 62978d25566..b80b8ede289 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -99,8 +99,14 @@ static bool is_partial_agg_memory_risky(PlannerInfo *root); static void collect_eager_agg_infos(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 void create_distinct_agg_grouping(PlannerInfo *root); static void create_grouping_expr_infos(PlannerInfo *root); static EquivalenceClass *get_eclass_for_sortgroupclause(PlannerInfo *root, SortGroupClause *sgc, @@ -681,9 +687,6 @@ collect_eager_agg_infos(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. */ @@ -691,31 +694,42 @@ collect_eager_agg_infos(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) + { + create_distinct_agg_grouping(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; /* @@ -972,6 +986,113 @@ 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 + * Does the aggregate depend only on the set of its argument values? + * + * 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. @@ -997,6 +1118,73 @@ add_grouping_expr_infos(PlannerInfo *root, List *exprs, List *clauses) } } +/* + * 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. + * + * Leaves root->group_expr_list NIL if no such keys were found. + */ +static void +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; + + /* 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; + + aggref = (Aggref *) expr; + + if (aggref->aggdistinct == NIL || !aggref_is_plain(aggref)) + return; + + if (!collect_distinct_agg_keys(aggref, &nextref, &exprs, &clauses)) + return; + } + + list_free(agg_level_exprs); + + if (exprs == NIL) + return; + + /* + * 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; +} + /* * create_grouping_expr_infos * Create a GroupingExprInfo for each expression usable as grouping key. diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 47a2016b49b..862f42168f1 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 00a24418ef2..113e6996a47 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -2553,3 +2553,504 @@ 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 the join +-- is made to produce only the rows it keeps +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) + -> 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) + -> 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) + -> 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 requires the join to produce 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) + +-- Deduplication still applies when HAVING names the same aggregate +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) + -> 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) + +-- ... while one that counts the duplicates requires them +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) + +-- Keys are derived only for a query with no grouping clause of its own. +-- This one groups by a1.id, so eager aggregation declines its DISTINCT +-- aggregate. +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 the join may +-- null-extend, 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) + -> 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 d855a7dff69..6610fb8fa52 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -745,3 +745,236 @@ 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 the join +-- is made to produce only the rows it keeps +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 requires the join to produce 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; + +-- Deduplication still applies when HAVING names the same aggregate +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; + +-- ... while one that counts the duplicates requires them +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; + +-- Keys are derived only for a query with no grouping clause of its own. +-- This one groups by a1.id, so eager aggregation declines its DISTINCT +-- aggregate. +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 the join may +-- null-extend, 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;