From 6c785a0f2a2c11882d1df64dda9126366a55fe51 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Fri, 14 Aug 2026 11:31:43 +0900 Subject: [PATCH v4 3/3] Teach UniqueKeys about eager aggregation's grouped relations Eager aggregation partially aggregates a relation before joining it. That keeps one row per group, NULL-aware just as GROUP BY is, so the grouped relation is distinct over its grouping expressions. Unlike the query's own grouping step, this one has joins above it, so the key can prove such a join inner-unique. Deduce these keys for grouped simple relations only. A grouped join relation may instead get its paths by joining an already-grouped input to a plain one, which emits one row per group per matching row of the other side rather than one row per group. --- .../postgres_fdw/expected/postgres_fdw.out | 3 +- src/backend/optimizer/README | 8 ++ src/backend/optimizer/path/uniquekeys.c | 91 +++++++++++++++++-- src/backend/optimizer/util/relnode.c | 3 + src/include/optimizer/paths.h | 2 + src/test/regress/expected/eager_aggregate.out | 59 +++++++++--- src/test/regress/expected/join.out | 8 +- src/test/regress/expected/uniquekeys.out | 57 ++++++++++++ src/test/regress/sql/uniquekeys.sql | 22 +++++ 9 files changed, 226 insertions(+), 27 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 24cbe5003bb..72ceb8f61d1 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -4442,6 +4442,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w Sort Key: (sum(ft1_1.c1)) -> Hash Join Output: (sum(ft1_1.c1)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (ft1_1.c2 = ft1.c2) -> Foreign Scan Output: ft1_1.c2, (sum(ft1_1.c1)) @@ -4455,7 +4456,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w -> Foreign Scan on public.ft1 Output: ft1.c2 Remote SQL: SELECT c2 FROM "S 1"."T 1" -(24 rows) +(25 rows) select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2; count | b diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README index 5ce68d057c2..a17c1d3b0aa 100644 --- a/src/backend/optimizer/README +++ b/src/backend/optimizer/README @@ -1144,6 +1144,14 @@ join. No translation is needed: a child Var belongs to the same EquivalenceClass as its parent, so the keys' EC indexes carry over unchanged. This is what lets a partitionwise join prove its child joins inner-unique. +Eager aggregation partially aggregates a relation before joining it, and that +too keeps one row per group, so the grouped relation is distinct over its +grouping expressions. Unlike the query's own grouping step this one has joins +above it, so the key can prove such a join inner-unique. We deduce this only +for grouped simple relations: a grouped join relation may instead get its +paths by joining an already-grouped input to a plain one, and those emit one +row per group per matching row of the other side, not one row per group. + Finally, the query's upper relations get keys too. The grouping step is NULL-aware distinct over the grouping columns, and emits a single row (the empty key) for a plain aggregation or grouping only by constants; the DISTINCT diff --git a/src/backend/optimizer/path/uniquekeys.c b/src/backend/optimizer/path/uniquekeys.c index d8c92c704f8..043a92e960f 100644 --- a/src/backend/optimizer/path/uniquekeys.c +++ b/src/backend/optimizer/path/uniquekeys.c @@ -71,7 +71,7 @@ static bool side_is_unique(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, List *restrictlist); static int find_ec_position(PlannerInfo *root, Expr *expr, List *opfamilies, Oid collation, - Bitmapset *candidates); + Bitmapset *candidates, Relids child_relids); static int base_ec_position(PlannerInfo *root, EquivalenceClass *ec); @@ -172,7 +172,7 @@ populate_plain_rel_uniquekeys(PlannerInfo *root, RelOptInfo *rel) pos = find_ec_position(root, colexpr, get_mergejoin_opfamilies(equality_op), ind->indexcollations[c], - rel->eclass_indexes); + rel->eclass_indexes, NULL); if (pos < 0) { useless = true; @@ -322,7 +322,7 @@ add_subquery_key_col(PlannerInfo *root, RelOptInfo *rel, exprTypmod((Node *) tle->expr), collation, 0); pos = find_ec_position(root, (Expr *) colvar, opfamilies, collation, - rel->eclass_indexes); + rel->eclass_indexes, NULL); if (pos < 0) return false; ec = (EquivalenceClass *) list_nth(root->eq_classes, pos); @@ -835,7 +835,7 @@ populate_unique_rel_uniquekeys(PlannerInfo *root, RelOptInfo *unique_rel, /* Match in base-EC space: strip any outer-join nulling first */ expr = (Expr *) remove_nulling_relids((Node *) expr, root->outer_join_rels, NULL); - pos = find_ec_position(root, expr, opfamilies, collation, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL, NULL); if (pos < 0) return; @@ -851,6 +851,75 @@ populate_unique_rel_uniquekeys(PlannerInfo *root, RelOptInfo *unique_rel, add_uniquekey(unique_rel, key_ecs, true); } +/* + * populate_agg_rel_uniquekeys + * Deduce the unique keys of a relation that eager aggregation has + * partially aggregated. + * + * Partial aggregation keeps one row per group, NULL-aware just as GROUP BY is, + * so the grouped relation is distinct over its grouping expressions. + * + * The caller applies this only to a grouped simple relation, whose only way to + * produce rows is to aggregate that relation. A grouped join relation could + * instead be built by joining an already-grouped input to a plain one, which + * emits one row per group per matching row of the other side rather than one + * row per group. + */ +void +populate_agg_rel_uniquekeys(PlannerInfo *root, RelOptInfo *grouped_rel) +{ + RelAggInfo *agg_info = grouped_rel->agg_info; + Bitmapset *key_ecs = NULL; + ListCell *lc1; + ListCell *lc2; + + Assert(agg_info != NULL); + Assert(IS_SIMPLE_REL(grouped_rel)); + Assert(bms_equal(agg_info->apply_agg_at, grouped_rel->relids)); + + forboth(lc1, agg_info->group_clauses, lc2, agg_info->group_exprs) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc1); + Expr *expr = (Expr *) lfirst(lc2); + List *opfamilies = get_mergejoin_opfamilies(sgc->eqop); + Oid collation = exprCollation((Node *) expr); + EquivalenceClass *ec; + int pos; + + /* The key needs all these expressions, so we can't skip one */ + if (opfamilies == NIL) + return; + + /* Match in base-EC space: strip any outer-join nulling first */ + expr = (Expr *) remove_nulling_relids((Node *) expr, + root->outer_join_rels, NULL); + + /* + * For a child relation the grouping expressions are child Vars, which + * are child members of the same ECs as their parents, so we must let + * those match too. + */ + pos = find_ec_position(root, expr, opfamilies, collation, NULL, + IS_OTHER_REL(grouped_rel) ? + grouped_rel->relids : NULL); + if (pos < 0) + return; + + ec = list_nth_node(EquivalenceClass, root->eq_classes, pos); + + /* A column equated to a constant is not needed in the key */ + if (ec->ec_has_const) + continue; + + key_ecs = bms_add_member(key_ecs, pos); + } + + if (!bms_is_subset(key_ecs, get_interesting_unique_ecs(root))) + return; + + add_uniquekey(grouped_rel, key_ecs, true); +} + /* * uniquekeys_distinct_is_noop * Is the input relation of the query's DISTINCT step already provably @@ -1003,7 +1072,7 @@ uniquekeys_uniquification_is_noop(PlannerInfo *root, RelOptInfo *rel, /* Match in base-EC space: strip any outer-join nulling first */ expr = (Expr *) remove_nulling_relids((Node *) expr, root->outer_join_rels, NULL); - pos = find_ec_position(root, expr, opfamilies, collation, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL, NULL); if (pos >= 0) covered = bms_add_member(covered, pos); } @@ -1196,7 +1265,7 @@ collect_clause_ecs(PlannerInfo *root, List *clause, Bitmapset **ecs_p) expr = (Expr *) remove_nulling_relids((Node *) tle->expr, root->outer_join_rels, NULL); - pos = find_ec_position(root, expr, opfamilies, collation, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL, NULL); } if (pos < 0) return false; @@ -1291,12 +1360,16 @@ side_is_unique(PlannerInfo *root, RelOptInfo *joinrel, * matching "expr". If "candidates" is non-NULL, consider only the ECs at * those positions. Returns -1 if there is none. * + * "child_relids" identifies a child relation whose members should be + * considered as well; pass NULL to match only against parent members. + * * Keys live in base-EC space, so a caller matching a reference that an outer * join may have null-extended must first strip its varnullingrels. */ static int find_ec_position(PlannerInfo *root, Expr *expr, - List *opfamilies, Oid collation, Bitmapset *candidates) + List *opfamilies, Oid collation, Bitmapset *candidates, + Relids child_relids) { foreach_node(EquivalenceClass, ec, root->eq_classes) { @@ -1311,7 +1384,7 @@ find_ec_position(PlannerInfo *root, Expr *expr, ec->ec_collation != collation || !equal(ec->ec_opfamilies, opfamilies)) continue; - if (find_ec_member_matching_expr(ec, expr, NULL)) + if (find_ec_member_matching_expr(ec, expr, child_relids)) return pos; } return -1; @@ -1356,7 +1429,7 @@ base_ec_position(PlannerInfo *root, EquivalenceClass *ec) expr = remove_nulling_relids(expr, root->outer_join_rels, NULL); pos = find_ec_position(root, (Expr *) expr, ec->ec_opfamilies, - ec->ec_collation, NULL); + ec->ec_collation, NULL, NULL); if (pos >= 0) return pos; } diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index cb34f302fd3..897e9ecca74 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -486,6 +486,9 @@ build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel) grouped_rel->rows = agg_info->grouped_rows; grouped_rel->agg_info = agg_info; + /* Deduce the grouped rel's unique keys */ + populate_agg_rel_uniquekeys(root, grouped_rel); + rel->grouped_rel = grouped_rel; return grouped_rel; diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 11a577772cc..df45dc94a95 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -310,6 +310,8 @@ extern void populate_unique_rel_uniquekeys(PlannerInfo *root, RelOptInfo *input_rel, SpecialJoinInfo *sjinfo, bool is_noop); +extern void populate_agg_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *grouped_rel); extern bool uniquekeys_distinct_is_noop(PlannerInfo *root, RelOptInfo *input_rel); extern bool uniquekeys_grouping_is_noop(PlannerInfo *root, diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index 091ae48a92b..5096971087d 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -31,6 +31,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t1.a -> Hash Join Output: t1.a, (PARTIAL avg(t2.c)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -41,7 +42,7 @@ GROUP BY t1.a ORDER BY t1.a; Group Key: t2.b -> Seq Scan on public.eager_agg_t2 t2 Output: t2.a, t2.b, t2.c -(18 rows) +(19 rows) SELECT t1.a, avg(t2.c) FROM eager_agg_t1 t1 @@ -77,6 +78,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t1.a -> Hash Join Output: t1.a, (PARTIAL avg(t2.c)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -90,7 +92,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t2.b -> Seq Scan on public.eager_agg_t2 t2 Output: t2.c, t2.b -(21 rows) +(22 rows) SELECT t1.a, avg(t2.c) FROM eager_agg_t1 t1 @@ -246,6 +248,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t1.a -> Hash Right Join Output: t1.a, (PARTIAL avg(t2.c)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -256,7 +259,7 @@ GROUP BY t1.a ORDER BY t1.a; Group Key: t2.b -> Seq Scan on public.eager_agg_t2 t2 Output: t2.a, t2.b, t2.c -(18 rows) +(19 rows) SELECT t1.a, avg(t2.c) FROM eager_agg_t1 t1 @@ -344,6 +347,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t1.a -> Parallel Hash Join Output: t1.a, (PARTIAL avg(t2.c)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Parallel Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -354,7 +358,7 @@ GROUP BY t1.a ORDER BY t1.a; Group Key: t2.b -> Parallel Seq Scan on public.eager_agg_t2 t2 Output: t2.a, t2.b, t2.c -(21 rows) +(22 rows) SELECT t1.a, avg(t2.c) FROM eager_agg_t1 t1 @@ -397,6 +401,7 @@ GROUP BY t1.a ORDER BY t1.a; Sort Key: t1.a -> Hash Join Output: t1.a, (PARTIAL avg(t2.c)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -407,7 +412,7 @@ GROUP BY t1.a ORDER BY t1.a; Group Key: t2.b -> Seq Scan on public.eager_agg_t2 t2 Output: t2.a, t2.b, t2.c -(18 rows) +(19 rows) SELECT t1.a, avg(t2.c) FROM eager_agg_t1 t1 @@ -527,6 +532,7 @@ GROUP BY t2.b ORDER BY t2.b; Sort Key: t2.b -> Hash Right Semi Join Output: t2.b, (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1.b = t2.b) -> Seq Scan on public.eager_agg_t1 t1 Output: t1.a, t1.b, t1.c @@ -537,7 +543,7 @@ GROUP BY t2.b ORDER BY t2.b; Group Key: t2.b -> Seq Scan on public.eager_agg_t2 t2 Output: t2.a, t2.b, t2.c -(18 rows) +(19 rows) SELECT t2.b, count(*) FROM eager_agg_t2 t2 @@ -596,6 +602,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1.x -> Hash Join Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2.y = t1.x) -> Seq Scan on public.eager_agg_tab2_p1 t2 Output: t2.y @@ -611,6 +618,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_1.x -> Hash Join Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_1.y = t1_1.x) -> Seq Scan on public.eager_agg_tab2_p2 t2_1 Output: t2_1.y @@ -626,6 +634,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Hash Join Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_2.y = t1_2.x) -> Seq Scan on public.eager_agg_tab2_p3 t2_2 Output: t2_2.y @@ -636,7 +645,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Seq Scan on public.eager_agg_tab1_p3 t1_2 Output: t1_2.x, t1_2.y -(49 rows) +(52 rows) SELECT t1.x, sum(t1.y), count(*) FROM eager_agg_tab1 t1 @@ -678,6 +687,7 @@ GROUP BY t2.y ORDER BY t2.y; Group Key: t2.y -> Hash Join Output: t2.y, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2.y = t1.x) -> Seq Scan on public.eager_agg_tab2_p1 t2 Output: t2.y @@ -693,6 +703,7 @@ GROUP BY t2.y ORDER BY t2.y; Group Key: t2_1.y -> Hash Join Output: t2_1.y, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_1.y = t1_1.x) -> Seq Scan on public.eager_agg_tab2_p2 t2_1 Output: t2_1.y @@ -708,6 +719,7 @@ GROUP BY t2.y ORDER BY t2.y; Group Key: t2_2.y -> Hash Join Output: t2_2.y, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_2.y = t1_2.x) -> Seq Scan on public.eager_agg_tab2_p3 t2_2 Output: t2_2.y @@ -718,7 +730,7 @@ GROUP BY t2.y ORDER BY t2.y; Group Key: t1_2.x -> Seq Scan on public.eager_agg_tab1_p3 t1_2 Output: t1_2.y, t1_2.x -(49 rows) +(52 rows) SELECT t2.y, sum(t1.y), count(*) FROM eager_agg_tab1 t1 @@ -762,6 +774,7 @@ GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; -> Append -> Hash Join Output: t2.x, (PARTIAL sum(t1.x)), (PARTIAL count(*)), (PARTIAL avg(t1.x)) + Inner Unique: true Hash Cond: (t2.y = t1.x) -> Seq Scan on public.eager_agg_tab2_p1 t2 Output: t2.x, t2.y @@ -774,6 +787,7 @@ GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; Output: t1.x -> Hash Join Output: t2_1.x, (PARTIAL sum(t1_1.x)), (PARTIAL count(*)), (PARTIAL avg(t1_1.x)) + Inner Unique: true Hash Cond: (t2_1.y = t1_1.x) -> Seq Scan on public.eager_agg_tab2_p2 t2_1 Output: t2_1.x, t2_1.y @@ -786,6 +800,7 @@ GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; Output: t1_1.x -> Hash Join Output: t2_2.x, (PARTIAL sum(t1_2.x)), (PARTIAL count(*)), (PARTIAL avg(t1_2.x)) + Inner Unique: true Hash Cond: (t2_2.y = t1_2.x) -> Seq Scan on public.eager_agg_tab2_p3 t2_2 Output: t2_2.x, t2_2.y @@ -796,7 +811,7 @@ GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; Group Key: t1_2.x -> Seq Scan on public.eager_agg_tab1_p3 t1_2 Output: t1_2.x -(44 rows) +(47 rows) SELECT t2.x, sum(t1.x), count(*) FROM eager_agg_tab1 t1 @@ -1063,6 +1078,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1.x -> Hash Join Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2.y = t1.x) -> Seq Scan on public.eager_agg_tab2_p1 t2 Output: t2.y @@ -1078,6 +1094,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_1.x -> Hash Join Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_1.y = t1_1.x) -> Seq Scan on public.eager_agg_tab2_p2 t2_1 Output: t2_1.y @@ -1093,6 +1110,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Hash Join Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t2_2.y = t1_2.x) -> Seq Scan on public.eager_agg_tab2_p3 t2_2 Output: t2_2.y @@ -1103,7 +1121,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Seq Scan on public.eager_agg_tab1_p3 t1_2 Output: t1_2.x, t1_2.y -(49 rows) +(52 rows) SELECT t1.x, sum(t1.y), count(*) FROM eager_agg_tab1 t1 @@ -1163,6 +1181,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1.x -> Hash Join Output: t1.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1.x = t2.x) -> Seq Scan on public.eager_agg_tab_ml_p1 t1 Output: t1.x @@ -1178,6 +1197,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_1.x -> Hash Join Output: t1_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_1.x = t2_1.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 Output: t1_1.x @@ -1193,6 +1213,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Hash Join Output: t1_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_2.x = t2_2.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 Output: t1_2.x @@ -1208,6 +1229,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_3.x -> Hash Join Output: t1_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_3.x = t2_3.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 Output: t1_3.x @@ -1223,6 +1245,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_4.x -> Hash Join Output: t1_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_4.x = t2_4.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 Output: t1_4.x @@ -1233,7 +1256,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t2_4.x -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 Output: t2_4.y, t2_4.x -(79 rows) +(84 rows) SELECT t1.x, sum(t2.y), count(*) FROM eager_agg_tab_ml t1 @@ -1291,6 +1314,7 @@ GROUP BY t1.y ORDER BY t1.y; -> Append -> Hash Join Output: t1.y, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1.x = t2.x) -> Seq Scan on public.eager_agg_tab_ml_p1 t1 Output: t1.y, t1.x @@ -1303,6 +1327,7 @@ GROUP BY t1.y ORDER BY t1.y; Output: t2.y, t2.x -> Hash Join Output: t1_1.y, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_1.x = t2_1.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 Output: t1_1.y, t1_1.x @@ -1315,6 +1340,7 @@ GROUP BY t1.y ORDER BY t1.y; Output: t2_1.y, t2_1.x -> Hash Join Output: t1_2.y, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_2.x = t2_2.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 Output: t1_2.y, t1_2.x @@ -1327,6 +1353,7 @@ GROUP BY t1.y ORDER BY t1.y; Output: t2_2.y, t2_2.x -> Hash Join Output: t1_3.y, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_3.x = t2_3.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 Output: t1_3.y, t1_3.x @@ -1339,6 +1366,7 @@ GROUP BY t1.y ORDER BY t1.y; Output: t2_3.y, t2_3.x -> Hash Join Output: t1_4.y, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_4.x = t2_4.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 Output: t1_4.y, t1_4.x @@ -1349,7 +1377,7 @@ GROUP BY t1.y ORDER BY t1.y; Group Key: t2_4.x -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 Output: t2_4.y, t2_4.x -(67 rows) +(72 rows) SELECT t1.y, sum(t2.y), count(*) FROM eager_agg_tab_ml t1 @@ -1725,6 +1753,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1.x -> Hash Join Output: t1.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1.x = t2.x) -> Seq Scan on public.eager_agg_tab_ml_p1 t1 Output: t1.x @@ -1740,6 +1769,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_1.x -> Hash Join Output: t1_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_1.x = t2_1.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 Output: t1_1.x @@ -1755,6 +1785,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_2.x -> Hash Join Output: t1_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_2.x = t2_2.x) -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 Output: t1_2.x @@ -1770,6 +1801,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_3.x -> Hash Join Output: t1_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_3.x = t2_3.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 Output: t1_3.x @@ -1785,6 +1817,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t1_4.x -> Hash Join Output: t1_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Inner Unique: true Hash Cond: (t1_4.x = t2_4.x) -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 Output: t1_4.x @@ -1795,7 +1828,7 @@ GROUP BY t1.x ORDER BY t1.x; Group Key: t2_4.x -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 Output: t2_4.y, t2_4.x -(79 rows) +(84 rows) SELECT t1.x, sum(t2.y), count(*) FROM eager_agg_tab_ml t1 diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index a1eec5eeb0b..70944b6cf9e 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -2848,13 +2848,13 @@ order by x.thousand desc, x.twothousand; Sort Key: x.thousand DESC, x.twothousand Presorted Key: x.thousand -> Merge Join - Merge Cond: (y.thousand = x.thousand) - -> Partial GroupAggregate - Group Key: y.thousand - -> Index Only Scan Backward using tenk1_thous_tenthous on tenk1 y + Merge Cond: (x.thousand = y.thousand) -> Sort Sort Key: x.thousand DESC -> Seq Scan on tenk1 x + -> Partial GroupAggregate + Group Key: y.thousand + -> Index Only Scan Backward using tenk1_thous_tenthous on tenk1 y (13 rows) reset enable_hashagg; diff --git a/src/test/regress/expected/uniquekeys.out b/src/test/regress/expected/uniquekeys.out index c4dd9256a72..60b7a46777d 100644 --- a/src/test/regress/expected/uniquekeys.out +++ b/src/test/regress/expected/uniquekeys.out @@ -675,6 +675,63 @@ select uk_pt1.b from uk_pt1 left join (uk_pt2 join uk_pt3 on uk_pt2.a = uk_pt3.a reset enable_hashjoin; reset enable_partitionwise_join; -- +-- Eager aggregation +-- +-- Partial aggregation keeps one row per group, so the grouped relation is +-- distinct over its grouping expressions, which can prove a join above it +-- inner-unique. +-- +create table uk_ea1 (id int primary key, x int, val int); +create table uk_ea2 (id int, y int); +insert into uk_ea1 select i, i % 10, i from generate_series(1, 1000) i; +insert into uk_ea2 select i % 100, i from generate_series(1, 1000) i; +analyze uk_ea1, uk_ea2; +-- uk_ea2 is grouped by the join column, so the join sees a unique inner side +explain (verbose, costs off) +select uk_ea1.x, sum(uk_ea2.y) from uk_ea1 join uk_ea2 on uk_ea1.id = uk_ea2.id + group by uk_ea1.x order by 1; + QUERY PLAN +-------------------------------------------------------------------- + Sort + Output: uk_ea1.x, (sum(uk_ea2.y)) + Sort Key: uk_ea1.x + -> Finalize HashAggregate + Output: uk_ea1.x, sum(uk_ea2.y) + Group Key: uk_ea1.x + -> Merge Join + Output: uk_ea1.x, (PARTIAL sum(uk_ea2.y)) + Inner Unique: true + Merge Cond: (uk_ea1.id = uk_ea2.id) + -> Index Scan using uk_ea1_pkey on public.uk_ea1 + Output: uk_ea1.id, uk_ea1.x, uk_ea1.val + -> Sort + Output: uk_ea2.id, (PARTIAL sum(uk_ea2.y)) + Sort Key: uk_ea2.id + -> Partial HashAggregate + Output: uk_ea2.id, PARTIAL sum(uk_ea2.y) + Group Key: uk_ea2.id + -> Seq Scan on public.uk_ea2 + Output: uk_ea2.id, uk_ea2.y +(20 rows) + +select uk_ea1.x, sum(uk_ea2.y) from uk_ea1 join uk_ea2 on uk_ea1.id = uk_ea2.id + group by uk_ea1.x order by 1; + x | sum +---+------- + 0 | 45000 + 1 | 49600 + 2 | 49700 + 3 | 49800 + 4 | 49900 + 5 | 50000 + 6 | 50100 + 7 | 50200 + 8 | 50300 + 9 | 50400 +(10 rows) + +drop table uk_ea1, uk_ea2; +-- -- Result correctness: steps that must not be removed -- -- Cases where an over-strong key would drop a step that is really needed, and diff --git a/src/test/regress/sql/uniquekeys.sql b/src/test/regress/sql/uniquekeys.sql index 58e8a4bd9a5..d22779a9498 100644 --- a/src/test/regress/sql/uniquekeys.sql +++ b/src/test/regress/sql/uniquekeys.sql @@ -209,6 +209,28 @@ select uk_pt1.b from uk_pt1 left join (uk_pt2 join uk_pt3 on uk_pt2.a = uk_pt3.a reset enable_hashjoin; reset enable_partitionwise_join; +-- +-- Eager aggregation +-- +-- Partial aggregation keeps one row per group, so the grouped relation is +-- distinct over its grouping expressions, which can prove a join above it +-- inner-unique. +-- +create table uk_ea1 (id int primary key, x int, val int); +create table uk_ea2 (id int, y int); +insert into uk_ea1 select i, i % 10, i from generate_series(1, 1000) i; +insert into uk_ea2 select i % 100, i from generate_series(1, 1000) i; +analyze uk_ea1, uk_ea2; + +-- uk_ea2 is grouped by the join column, so the join sees a unique inner side +explain (verbose, costs off) +select uk_ea1.x, sum(uk_ea2.y) from uk_ea1 join uk_ea2 on uk_ea1.id = uk_ea2.id + group by uk_ea1.x order by 1; +select uk_ea1.x, sum(uk_ea2.y) from uk_ea1 join uk_ea2 on uk_ea1.id = uk_ea2.id + group by uk_ea1.x order by 1; + +drop table uk_ea1, uk_ea2; + -- -- Result correctness: steps that must not be removed -- -- 2.37.1 (Apple Git-137.1)