From fa703ad98206a477c42b38cd7b28bc6a3aceb1c0 Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 18:14:25 -0700 Subject: [PATCH v2 15/17] Drop a deduplication repeated by the query's own grouping A deduplication pushed below the joins can group on the same keys the query groups by, and the Agg placed above it then groups on them a second time. We should plan its subpath instead. --- src/backend/optimizer/plan/planner.c | 44 +++++++++++++ src/test/regress/expected/eager_aggregate.out | 61 +++++++++---------- src/test/regress/sql/eager_aggregate.sql | 4 +- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index fc8a0d63d40..8b11e3dd930 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -4017,6 +4017,38 @@ get_number_of_groups(PlannerInfo *root, return dNumGroups; } +/* + * dedup_groups_as_query_does + * Does this deduplication group on exactly the query's grouping keys? + * + * The caller's grouping Agg groups on those keys again, so it can take the + * path's subpath instead. + */ +static bool +dedup_groups_as_query_does(PlannerInfo *root, Path *path) +{ + AggPath *aggpath; + ListCell *lc; + + if (!IsA(path, AggPath)) + return false; + + aggpath = (AggPath *) path; + + if (aggpath->groupClause == NIL || + list_length(aggpath->groupClause) != + list_length(root->processed_groupClause)) + return false; + + foreach(lc, aggpath->groupClause) + { + if (!list_member_ptr(root->processed_groupClause, lfirst(lc))) + return false; + } + + return true; +} + /* * create_grouping_paths * @@ -4408,6 +4440,18 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, { Path *path = (Path *) lfirst(lc); + /* + * The aggregate above repeats a deduplication on the query's own + * keys, so take its input instead. Any projection under it goes + * too, since we project onto the target we want here anyway. + */ + if (dedup_groups_as_query_does(root, path)) + { + path = ((AggPath *) path)->subpath; + if (IsA(path, ProjectionPath)) + path = ((ProjectionPath *) path)->subpath; + } + add_path(dedup_rel, (Path *) create_projection_path(root, dedup_rel, path, diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index 3e9451d2a7a..18579bcd788 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -2092,30 +2092,27 @@ SELECT t1.id FROM eager_distinct_t1 t1 JOIN eager_distinct_t2 t2 ON t2.t1_id = t1.id GROUP BY t1.id ORDER BY t1.id; - QUERY PLAN ------------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------- Group Output: t1.id Group Key: t1.id - -> GroupAggregate + -> Sort Output: t1.id - Group Key: t1.id - -> Sort + Sort Key: t1.id + -> Hash Join Output: t1.id - Sort Key: t1.id - -> Hash Join - Output: t1.id - Hash Cond: (t1.id = t2.t1_id) - -> Seq Scan on public.eager_distinct_t1 t1 - Output: t1.id, t1.val - -> Hash + Hash Cond: (t1.id = t2.t1_id) + -> Seq Scan on public.eager_distinct_t1 t1 + Output: t1.id, t1.val + -> Hash + Output: t2.t1_id + -> HashAggregate Output: t2.t1_id - -> HashAggregate - Output: t2.t1_id - Group Key: t2.t1_id - -> Seq Scan on public.eager_distinct_t2 t2 - Output: t2.id, t2.t1_id, t2.flag -(21 rows) + Group Key: t2.t1_id + -> Seq Scan on public.eager_distinct_t2 t2 + Output: t2.id, t2.t1_id, t2.flag +(18 rows) SELECT t1.id FROM eager_distinct_t1 t1 @@ -2917,31 +2914,29 @@ HAVING count(*) > 5; -> Seq Scan on eager_distinct_a1 a1 (10 rows) --- A DISTINCT aggregate is idempotent, so the query's own grouping clause --- drives the deduplication +-- The deduplication groups on the keys the query groups by, so the query's +-- own aggregate takes its input directly 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 ----------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------- GroupAggregate Group Key: a1.id -> Sort Sort Key: a1.id, a1.title - -> 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) + -> 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 +(12 rows) -- The deduplication threshold governs the derived keys too SET min_eager_distinct_group_size TO 8; diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql index fafa60edc78..604fe61f63b 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -908,8 +908,8 @@ SELECT count(DISTINCT a1.id) WHERE a2.flag HAVING count(*) > 5; --- A DISTINCT aggregate is idempotent, so the query's own grouping clause --- drives the deduplication +-- The deduplication groups on the keys the query groups by, so the query's +-- own aggregate takes its input directly EXPLAIN (COSTS OFF) SELECT a1.id, count(DISTINCT a1.title) FROM eager_distinct_a1 a1