From 10ab2e74bfd8f0149317db5b00e85b83eb1d391a Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 13:44:07 -0700 Subject: [PATCH v2 11/17] Compute aggregates above a pushed-down deduplication Group the deduplicated rows as another path. Also split copy_rel_without_paths() out of build_grouped_rel(). --- src/backend/optimizer/path/allpaths.c | 6 +++-- src/backend/optimizer/plan/planner.c | 37 ++++++++++++++++++++++++-- src/backend/optimizer/util/relnode.c | 38 ++++++++++++++++++--------- src/include/optimizer/pathnode.h | 1 + 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 7c73a0ab387..53047e6a69e 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3566,9 +3566,11 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, /* * Determine whether we should consider hash-based implementations of - * grouping. + * grouping. An ordered aggregate cannot be hashed. Under eager + * deduplication the aggregates stay above the join. */ - Assert(root->numOrderedAggs == 0); + Assert(root->numOrderedAggs == 0 || + root->eager_agg_mode == EAGER_AGG_DEDUP); can_hash = (agg_info->group_clauses != NIL && grouping_is_hashable(agg_info->group_clauses)); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index c8ca9dcbf02..fc8a0d63d40 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -4388,6 +4388,37 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, partially_grouped_rel, agg_costs, gd, extra); + /* + * Eager aggregation may have pushed a deduplication below the joins. + * Every aggregate the query computes here ignores how often a row + * arrives, so grouping the deduplicated rows yields the same results as + * grouping the join's rows. Group them as a path. + */ + if (root->eager_agg_mode == EAGER_AGG_DEDUP && + input_rel->grouped_rel != NULL && + !IS_DUMMY_REL(input_rel->grouped_rel) && + input_rel->grouped_rel->pathlist != NIL) + { + RelOptInfo *dedup_rel; + ListCell *lc; + + dedup_rel = copy_rel_without_paths(input_rel); + + foreach(lc, input_rel->grouped_rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + + add_path(dedup_rel, + (Path *) create_projection_path(root, dedup_rel, + path, + input_rel->reltarget)); + } + + set_cheapest(dedup_rel); + add_paths_to_grouping_rel(root, dedup_rel, grouped_rel, NULL, + agg_costs, gd, extra); + } + /* Give a helpful error if we failed to find any implementation */ if (grouped_rel->pathlist == NIL) ereport(ERROR, @@ -7636,9 +7667,11 @@ create_partial_grouping_paths(PlannerInfo *root, /* * Check whether any partially aggregated paths have been generated - * through eager aggregation. + * through eager aggregation. A deduplication emits final rows, so + * create_ordinary_grouping_paths() groups them instead. */ - if (input_rel->grouped_rel && + if (root->eager_agg_mode != EAGER_AGG_DEDUP && + input_rel->grouped_rel && !IS_DUMMY_REL(input_rel->grouped_rel) && input_rel->grouped_rel->pathlist != NIL) eager_agg_rel = input_rel->grouped_rel; diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index e9677abc2a5..47a2016b49b 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -500,18 +500,7 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel) { RelOptInfo *grouped_rel; - grouped_rel = makeNode(RelOptInfo); - memcpy(grouped_rel, rel, sizeof(RelOptInfo)); - - /* - * clear path info - */ - grouped_rel->pathlist = NIL; - grouped_rel->ppilist = NIL; - grouped_rel->partial_pathlist = NIL; - grouped_rel->cheapest_startup_path = NULL; - grouped_rel->cheapest_total_path = NULL; - grouped_rel->cheapest_parameterized_paths = NIL; + grouped_rel = copy_rel_without_paths(rel); /* * clear partition info @@ -536,6 +525,31 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel) return grouped_rel; } +/* + * copy_rel_without_paths + * Flat copy a relation, leaving the copy's path lists empty. + * + * The copy shares everything else with the original, so a caller wanting + * different size estimates or partitioning must reset those itself. + */ +RelOptInfo * +copy_rel_without_paths(RelOptInfo *rel) +{ + RelOptInfo *newrel; + + newrel = makeNode(RelOptInfo); + memcpy(newrel, rel, sizeof(RelOptInfo)); + + newrel->pathlist = NIL; + newrel->ppilist = NIL; + newrel->partial_pathlist = NIL; + newrel->cheapest_startup_path = NULL; + newrel->cheapest_total_path = NULL; + newrel->cheapest_parameterized_paths = NIL; + + return newrel; +} + /* * find_base_rel * Find a base or otherrel relation entry, which must already exist. diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index e8db321f92b..1f5a911c388 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -346,6 +346,7 @@ extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent); extern RelOptInfo *build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel); extern RelOptInfo *build_grouped_rel(PlannerInfo *root, RelOptInfo *rel); +extern RelOptInfo *copy_rel_without_paths(RelOptInfo *rel); extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid); extern RelOptInfo *find_base_rel_noerr(PlannerInfo *root, int relid); extern RelOptInfo *find_base_rel_ignore_join(PlannerInfo *root, int relid);