From 11daacd64aea2a972243c878cf955a7d5b59b507 Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 18:14:25 -0700 Subject: [PATCH v1 14/16] 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 | 48 ++++++++++++++++++- src/test/regress/expected/eager_aggregate.out | 28 +++++------ src/test/regress/sql/eager_aggregate.sql | 4 +- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 69f769da050..0787d308bb9 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -4056,6 +4056,38 @@ complete_dedup_path(Path *path) return (Path *) aggpath; } +/* + * 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 * @@ -4447,9 +4479,23 @@ 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; + } + else + path = complete_dedup_path(path); + add_path(dedup_rel, (Path *) create_projection_path(root, dedup_rel, - complete_dedup_path(path), + path, input_rel->reltarget)); } diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index 64929d07d68..5dd43d39f3a 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -2917,31 +2917,29 @@ HAVING count(*) > 5; -> 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 +-- A DISTINCT aggregate beside a grouping clause of the query's own adds its +-- argument as a further key, and the deduplication goes down as usual 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) - -> 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) + -> 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 +(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 cd37db8e79a..f929a2a4db0 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 beside a grouping clause of the query's own is no --- obstacle, though at this size the plain join still costs less +-- A DISTINCT aggregate beside a grouping clause of the query's own adds its +-- argument as a further key, and the deduplication goes down as usual EXPLAIN (COSTS OFF) SELECT a1.id, count(DISTINCT a1.title) FROM eager_distinct_a1 a1