From 01527bdae8842b3bcbac84d0b403bb4882a6334e Mon Sep 17 00:00:00 2001 From: zhenglong li Date: Wed, 28 Oct 2026 15:47:12 +0800 Subject: [PATCH v5 1/2] Ignore sort direction when matching GROUP BY keys to input pathkeys get_useful_group_keys_orderings() tries to reorder the GROUP BY keys to match the ordering of the input path, so that a presorted input such as an index scan can be used for the grouping step without an extra sort. However, both group_keys_reorder_by_pathkeys() and the GROUP BY check in truncate_useless_pathkeys() match pathkeys by pointer, which requires the input ordering to agree with the GROUP BY keys in sort direction and NULLS ordering as well. That is more than grouping needs. Grouping only requires equal values to be adjacent, which any ordering by the same equivalence class and operator family guarantees, whichever its direction. The sort direction of a GROUP BY key is merely taken from a matching ORDER BY item by the parser, to make it likely that one sort can serve both steps. So when the ORDER BY sorts in a direction the input path doesn't provide, as in CREATE INDEX ON t (b, c); SELECT b, c, count(*) FROM t GROUP BY b, c ORDER BY b, c DESC; the grouping keys are b ASC, c DESC, and the index ordering b ASC, c ASC is useful only for its first key. We then sort the whole input by b, c DESC incrementally, rather than grouping straight off the index and sorting only the (typically far fewer) groups for the ORDER BY. Teach group_keys_reorder_by_pathkeys() to accept an input pathkey that matches a GROUP BY pathkey in equivalence class and operator family, regardless of its sort direction and NULLS ordering. In that case the reordered GROUP BY keys use the input pathkey, along with a copy of the GROUP BY clause whose sort operator and NULLS ordering are adjusted to agree with it. Likewise, make truncate_useless_pathkeys() consider such pathkeys useful for GROUP BY, so that they survive to be matched. The aggregate pathkeys that may follow the grouping keys in group_pathkeys are still matched exactly, since an aggregate's ORDER BY does depend on the sort direction. Do the same for the partial aggregation that eager aggregation pushes below a join. generate_grouped_paths() required its input to be sorted exactly by the grouping pathkeys, so once the GROUP BY keys took a direction the input path doesn't provide, the partial aggregation had to sort its whole input, and then lost out to a plan without it, as the final aggregation can now group straight off the input. Partial aggregation compares the grouping keys only for equality, and its output keeps the ordering of its input, so let it take the sort direction from the input path, using the new function adjust_group_pathkeys_to_input(). If the operator family lacks the sort operator for the direction of the input pathkey, which is possible only with an operator class that does not follow the btree rules, stop matching there rather than raise an error, since the GROUP BY keys can still be sorted in their own direction. As before, this ordering is only offered as an additional candidate, so it is chosen only if it is cheaper. All of this is done only if enable_group_by_reordering is on, so that turning it off gives the same plans as before. Adjust the assertions in get_useful_group_keys_orderings() accordingly, as the reordered clauses and pathkeys no longer need to be identical to the original ones, only to cover the same grouping keys. Reviewed-by: Henson Choi --- src/backend/optimizer/path/allpaths.c | 26 +- src/backend/optimizer/path/pathkeys.c | 284 +++++++++++++++++- src/include/optimizer/paths.h | 2 + src/test/regress/expected/aggregates.out | 157 ++++++++++ src/test/regress/expected/eager_aggregate.out | 255 ++++++++++++++++ src/test/regress/sql/aggregates.sql | 70 +++++ src/test/regress/sql/eager_aggregate.sql | 143 +++++++++ 7 files changed, 917 insertions(+), 20 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index d52a6d40505..fcc1deb1be6 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3608,6 +3608,7 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, { Path *input_path = (Path *) lfirst(lc); Path *path; + List *input_group_pathkeys; bool is_sorted; int presorted_keys; @@ -3619,7 +3620,16 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, input_path != cheapest_total_path) continue; - is_sorted = pathkeys_count_contained_in(group_pathkeys, + /* + * Partial aggregation compares the grouping keys only for + * equality, so the input can be sorted on them in any direction. + * Take the direction from the input path where it can provide the + * grouping order. + */ + input_group_pathkeys = + adjust_group_pathkeys_to_input(group_pathkeys, + input_path->pathkeys); + is_sorted = pathkeys_count_contained_in(input_group_pathkeys, input_path->pathkeys, &presorted_keys); @@ -3653,13 +3663,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, path = (Path *) create_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, -1.0); else path = (Path *) create_incremental_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, presorted_keys, -1.0); } @@ -3692,10 +3702,14 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, { Path *input_path = (Path *) lfirst(lc); Path *path; + List *input_group_pathkeys; bool is_sorted; int presorted_keys; - is_sorted = pathkeys_count_contained_in(group_pathkeys, + input_group_pathkeys = + adjust_group_pathkeys_to_input(group_pathkeys, + input_path->pathkeys); + is_sorted = pathkeys_count_contained_in(input_group_pathkeys, input_path->pathkeys, &presorted_keys); @@ -3729,13 +3743,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, path = (Path *) create_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, -1.0); else path = (Path *) create_incremental_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, presorted_keys, -1.0); } diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index d6a305aedac..e63aedb4852 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -37,6 +37,7 @@ static bool matches_boolean_partition_clause(RestrictInfo *rinfo, int partkeycol); static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle); static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey); +static bool pathkey_has_sort_operator(PathKey *pathkey); /**************************************************************************** @@ -353,6 +354,35 @@ pathkeys_contained_in(List *keys1, List *keys2) return false; } +/* + * find_grouping_pathkey + * Find a GROUP BY pathkey that 'pathkey' can stand in for. + * + * Searches the first 'num_groupby_pathkeys' items of 'group_pathkeys' for a + * pathkey with the same equivalence class and operator family as 'pathkey', + * ignoring sort direction and NULLS ordering. Returns NULL if there is none. + * Pathkeys are canonical, so there can be at most one. + */ +static PathKey * +find_grouping_pathkey(List *group_pathkeys, int num_groupby_pathkeys, + PathKey *pathkey) +{ + ListCell *lc; + + foreach(lc, group_pathkeys) + { + PathKey *gpk = (PathKey *) lfirst(lc); + + if (foreach_current_index(lc) >= num_groupby_pathkeys) + break; + if (gpk->pk_eclass == pathkey->pk_eclass && + gpk->pk_opfamily == pathkey->pk_opfamily) + return gpk; + } + + return NULL; +} + /* * group_keys_reorder_by_pathkeys * Reorder GROUP BY pathkeys and clauses to match the input pathkeys. @@ -364,6 +394,13 @@ pathkeys_contained_in(List *keys1, List *keys2) * 'num_groupby_pathkeys' is the number of first '*group_pathkeys' items to * search matching pathkeys. * + * An input pathkey matches a GROUP BY pathkey if both have the same + * equivalence class and operator family, even if they differ in sort + * direction or NULLS ordering. Grouping only requires that equal values be + * adjacent, which the input ordering then guarantees, so in that case we use + * the input pathkey in place of the GROUP BY pathkey and a copy of the GROUP + * BY clause with its sort ordering adjusted to agree with it. + * * Returns the number of GROUP BY keys with a matching pathkey. */ static int @@ -374,6 +411,8 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, List *new_group_pathkeys = NIL, *new_group_clauses = NIL; List *grouping_pathkeys; + List *replaced_pathkeys = NIL, + *replaced_clauses = NIL; ListCell *lc; int n; @@ -403,18 +442,24 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, foreach(lc, pathkeys) { PathKey *pathkey = (PathKey *) lfirst(lc); + PathKey *group_pathkey; SortGroupClause *sgc; + SortGroupClause *new_sgc; /* - * Pathkeys are built in a way that allows simply comparing pointers. - * Give up if we can't find the matching pointer. Also give up if + * Give up if we've already matched all the grouping pathkeys, or if * there is no sortclause reference for some reason. */ if (foreach_current_index(lc) >= num_groupby_pathkeys || - !list_member_ptr(grouping_pathkeys, pathkey) || pathkey->pk_eclass->ec_sortref == 0) break; + /* Give up if there's no grouping pathkey this one can stand in for */ + group_pathkey = find_grouping_pathkey(grouping_pathkeys, + num_groupby_pathkeys, pathkey); + if (group_pathkey == NULL) + break; + /* * Since 1349d27 pathkey coming from underlying node can be in the * root->group_pathkeys but not in the processed_groupClause. So, we @@ -432,23 +477,161 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, */ Assert(OidIsValid(sgc->sortop)); + /* + * If the input pathkey's ordering differs from that of the GROUP BY + * pathkey, make a copy of the clause that agrees with the input + * pathkey. + */ + new_sgc = sgc; + if (pathkey != group_pathkey) + { + bool reverse_sort = (pathkey->pk_cmptype == COMPARE_GT); + Oid sortop = sgc->sortop; + + if (sgc->reverse_sort != reverse_sort) + { + Oid lefttype, + righttype; + + op_input_types(sgc->sortop, &lefttype, &righttype); + sortop = get_opfamily_member_for_cmptype(pathkey->pk_opfamily, + lefttype, + righttype, + pathkey->pk_cmptype); + + /* + * Give up if the operator family lacks the operator to sort + * in the input pathkey's direction. That is possible only + * with an operator class that doesn't follow the btree rules, + * but we needn't fail on it, as the GROUP BY keys can still + * be sorted in their own direction. + */ + if (!OidIsValid(sortop)) + break; + } + + new_sgc = copyObject(sgc); + new_sgc->sortop = sortop; + new_sgc->reverse_sort = reverse_sort; + new_sgc->nulls_first = pathkey->pk_nulls_first; + } + + replaced_pathkeys = lappend(replaced_pathkeys, group_pathkey); + replaced_clauses = lappend(replaced_clauses, sgc); new_group_pathkeys = lappend(new_group_pathkeys, pathkey); - new_group_clauses = lappend(new_group_clauses, sgc); + new_group_clauses = lappend(new_group_clauses, new_sgc); } /* remember the number of pathkeys with a matching GROUP BY key */ n = list_length(new_group_pathkeys); /* append the remaining group pathkeys (will be treated as not sorted) */ - *group_pathkeys = list_concat_unique_ptr(new_group_pathkeys, - *group_pathkeys); - *group_clauses = list_concat_unique_ptr(new_group_clauses, - *group_clauses); + foreach(lc, *group_pathkeys) + { + PathKey *pathkey = (PathKey *) lfirst(lc); + + if (!list_member_ptr(replaced_pathkeys, pathkey)) + new_group_pathkeys = list_append_unique_ptr(new_group_pathkeys, + pathkey); + } + foreach(lc, *group_clauses) + { + SortGroupClause *sgc = (SortGroupClause *) lfirst(lc); + + if (!list_member_ptr(replaced_clauses, sgc)) + new_group_clauses = lappend(new_group_clauses, sgc); + } + *group_pathkeys = new_group_pathkeys; + *group_clauses = new_group_clauses; list_free(grouping_pathkeys); + list_free(replaced_pathkeys); + list_free(replaced_clauses); return n; } +/* + * adjust_group_pathkeys_to_input + * Make grouping pathkeys agree in sort direction with the input pathkeys. + * + * Returns a copy of 'group_pathkeys' in which each leading pathkey that has + * the same equivalence class and operator family as the pathkey at the same + * position of 'pathkeys' is replaced by the latter, even if they differ in + * sort direction or NULLS ordering. Grouping only requires that equal values + * be adjacent, so sorting by the result is as good for grouping as sorting by + * 'group_pathkeys', and an input path sorted by 'pathkeys' may satisfy it + * where it would not satisfy 'group_pathkeys'. + * + * This is meant for grouping steps that don't care about the sort operators + * of their grouping clauses, such as partial aggregation, which compares the + * grouping keys only for equality and leaves the final aggregation to provide + * any ordering the query requires. + * + * As the result may be used to sort the input explicitly, stop at a pathkey + * whose operator family lacks the sort operator for the input pathkey's + * direction. That is possible only with an operator class that doesn't + * follow the btree rules, as an index can provide such an ordering without + * the operator; but we needn't fail on it, as the remaining grouping pathkeys + * can still be sorted in their own direction. + * + * Like the reordering of the GROUP BY keys, this is done only if + * enable_group_by_reordering is on. + */ +List * +adjust_group_pathkeys_to_input(List *group_pathkeys, List *pathkeys) +{ + List *result = list_copy(group_pathkeys); + ListCell *lc1, + *lc2; + + if (!enable_group_by_reordering) + return result; + + forboth(lc1, result, lc2, pathkeys) + { + PathKey *group_pathkey = (PathKey *) lfirst(lc1); + PathKey *pathkey = (PathKey *) lfirst(lc2); + + if (group_pathkey->pk_eclass != pathkey->pk_eclass || + group_pathkey->pk_opfamily != pathkey->pk_opfamily) + break; + if (group_pathkey->pk_cmptype != pathkey->pk_cmptype && + !pathkey_has_sort_operator(pathkey)) + break; + lfirst(lc1) = pathkey; + } + + return result; +} + +/* + * pathkey_has_sort_operator + * Check that an explicit sort by 'pathkey' could be performed. + * + * prepare_sort_from_pathkeys() looks up the sort operator in the pathkey's + * operator family using the nominal type of the equivalence member it sorts + * by. We don't know which member that will be, so require the operator for + * the type of every member. + */ +static bool +pathkey_has_sort_operator(PathKey *pathkey) +{ + ListCell *lc; + + foreach(lc, pathkey->pk_eclass->ec_members) + { + EquivalenceMember *em = (EquivalenceMember *) lfirst(lc); + + if (!OidIsValid(get_opfamily_member_for_cmptype(pathkey->pk_opfamily, + em->em_datatype, + em->em_datatype, + pathkey->pk_cmptype))) + return false; + } + + return true; +} + /* * get_useful_group_keys_orderings * Determine which orderings of GROUP BY keys are potentially interesting. @@ -461,7 +644,9 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, * * - GROUP BY keys as ordered by preprocess_groupclause() to match target * ORDER BY clause (as much as possible), - * - GROUP BY keys reordered to match 'path' ordering (as much as possible). + * - GROUP BY keys reordered to match 'path' ordering (as much as possible), + * taking on the sort direction and NULLS ordering of 'path' where they + * differ from those of the GROUP BY keys. */ List * get_useful_group_keys_orderings(PlannerInfo *root, Path *path) @@ -533,8 +718,39 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path) Assert(list_length(info->clauses) == list_length(pinfo->clauses)); Assert(list_length(info->pathkeys) == list_length(pinfo->pathkeys)); - Assert(list_difference(info->clauses, pinfo->clauses) == NIL); - Assert(list_difference_ptr(info->pathkeys, pinfo->pathkeys) == NIL); + + /* + * The clauses and pathkeys may differ from the original ones in + * sort ordering, but they must cover the same GROUP BY items and + * equivalence classes. + */ + foreach(lc1, info->clauses) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc1); + SortGroupClause *psgc; + + psgc = get_sortgroupref_clause(sgc->tleSortGroupRef, + pinfo->clauses); + Assert(sgc->eqop == psgc->eqop); + } + foreach(lc1, info->pathkeys) + { + PathKey *pk = lfirst_node(PathKey, lc1); + bool found = false; + + foreach(lc2, pinfo->pathkeys) + { + PathKey *ppk = lfirst_node(PathKey, lc2); + + if (ppk->pk_eclass == pk->pk_eclass && + ppk->pk_opfamily == pk->pk_opfamily) + { + found = true; + break; + } + } + Assert(found); + } forboth(lc1, info->clauses, lc2, info->pathkeys) { @@ -542,6 +758,8 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path) PathKey *pk = lfirst_node(PathKey, lc2); Assert(pk->pk_eclass->ec_sortref == sgc->tleSortGroupRef); + Assert(sgc->reverse_sort == (pk->pk_cmptype == COMPARE_GT)); + Assert(sgc->nulls_first == pk->pk_nulls_first); } } } @@ -2190,6 +2408,43 @@ count_common_leading_pathkeys_unordered(List *keys1, List *keys2) return ncommon; } +/* + * count_common_leading_group_pathkeys + * Returns the number of leading PathKeys in 'pathkeys' which are useful + * for GROUP BY. + * + * This is like count_common_leading_pathkeys_unordered() applied to + * root->group_pathkeys, except that a pathkey matching a grouping key in + * everything but sort direction and NULLS ordering counts as useful too, + * unless enable_group_by_reordering is off. + */ +static int +count_common_leading_group_pathkeys(PlannerInfo *root, List *pathkeys) +{ + int ncommon = 0; + + /* No point in searching pathkeys when there are no GROUP BY pathkeys */ + if (root->group_pathkeys == NIL) + return 0; + + if (!enable_group_by_reordering) + return count_common_leading_pathkeys_unordered(root->group_pathkeys, + pathkeys); + + foreach_node(PathKey, pathkey, pathkeys) + { + if (!list_member_ptr(root->group_pathkeys, pathkey) && + find_grouping_pathkey(root->group_pathkeys, + root->num_groupby_pathkeys, + pathkey) == NULL) + break; + + ncommon++; + } + + return ncommon; +} + /* * truncate_useless_pathkeys * Shorten the given PathKey List to just the useful PathKeys. If all @@ -2238,10 +2493,11 @@ truncate_useless_pathkeys(PlannerInfo *root, * Check if these pathkeys are useful for GROUP BY or DISTINCT. The order * of the pathkeys does not matter here as Unique and GroupAggregate for * these operations can take advantage of Paths presorted by any of the - * GROUP BY/DISTINCT pathkeys. + * GROUP BY/DISTINCT pathkeys. For GROUP BY, the sort direction and NULLS + * ordering of the grouping keys don't matter either, as + * group_keys_reorder_by_pathkeys() can make them follow the input path. */ - nuseful2 = count_common_leading_pathkeys_unordered(root->group_pathkeys, - pathkeys); + nuseful2 = count_common_leading_group_pathkeys(root, pathkeys); if (nuseful2 == ntotal) return pathkeys; diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 64f87da1674..a47e4ee54df 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -226,6 +226,8 @@ typedef enum extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2); extern bool pathkeys_contained_in(List *keys1, List *keys2); extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common); +extern List *adjust_group_pathkeys_to_input(List *group_pathkeys, + List *pathkeys); extern List *get_useful_group_keys_orderings(PlannerInfo *root, Path *path); extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys, Relids required_outer, diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 7d07619956f..929b8a07c79 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -3428,6 +3428,163 @@ SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z; -> Index Scan using btg_x_y_idx on btg (8 rows) +-- Utilize the ordering of index scan even if it differs in sort direction +-- from the ordering that the GROUP BY keys took from ORDER BY +CREATE TABLE btg_dir AS + SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y); +ANALYZE btg_dir; +EXPLAIN (COSTS OFF) +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC; + QUERY PLAN +-------------------------------------------------------------- + Incremental Sort + Sort Key: x, y DESC + Presorted Key: x + -> GroupAggregate + Group Key: x, y + -> Index Only Scan using btg_dir_x_y_idx on btg_dir +(6 rows) + +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6; + x | y | count +---+---+------- + 0 | 4 | 50 + 0 | 3 | 50 + 0 | 2 | 50 + 0 | 1 | 50 + 0 | 0 | 50 + 1 | 4 | 50 +(6 rows) + +-- Likewise for a backward index scan that also provides the ordering +-- required by the aggregate +EXPLAIN (COSTS OFF) +SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x; + QUERY PLAN +----------------------------------------------------------------- + GroupAggregate + Group Key: x + -> Index Only Scan Backward using btg_dir_x_y_idx on btg_dir +(3 rows) + +SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x; + x | array_agg +---+------------- + 0 | {4,3,2,1,0} + 1 | {4,3,2,1,0} + 2 | {4,3,2,1,0} + 3 | {4,3,2,1,0} +(4 rows) + +DROP TABLE btg_dir; +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error, though such an operator class is not valid +BEGIN; +CREATE TYPE btg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int + LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_lt); +CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_eq); +CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 btg_color_cmp(btg_color, btg_color); +CREATE TABLE btg_color_tab AS + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c + FROM generate_series(1, 300) AS i; +CREATE INDEX ON btg_color_tab (c); +ANALYZE btg_color_tab; +SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c; + c | count +---+------- + r | 100 + g | 100 + b | 100 +(3 rows) + +ROLLBACK; +-- Utilize the ordering of index scan even if it differs only in NULLS +-- ordering from the GROUP BY key +CREATE TABLE btg_nf AS + SELECT nullif(i % 5, 0) AS x, nullif(i % 7, 3) AS y, i AS w + FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_nf_x_y_idx ON btg_nf(x NULLS FIRST, y); +ANALYZE btg_nf; +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM btg_nf GROUP BY x; + QUERY PLAN +------------------------------------------------------ + GroupAggregate + Group Key: x + -> Index Only Scan using btg_nf_x_y_idx on btg_nf +(3 rows) + +SELECT x, count(*) FROM btg_nf GROUP BY x ORDER BY x; + x | count +---+------- + 1 | 200 + 2 | 200 + 3 | 200 + 4 | 200 + | 200 +(5 rows) + +-- ... but the ordering required by an aggregate must match exactly, so the +-- index is of no use for an aggregate's ORDER BY key that differs in NULLS +-- ordering +EXPLAIN (COSTS OFF) +SELECT w % 2 AS w2, array_agg(DISTINCT x ORDER BY x) FROM btg_nf GROUP BY w2; + QUERY PLAN +-------------------------------- + GroupAggregate + Group Key: ((w % 2)) + -> Sort + Sort Key: ((w % 2)), x + -> Seq Scan on btg_nf + Disabled: true +(6 rows) + +SELECT w % 2 AS w2, array_agg(DISTINCT x ORDER BY x) FROM btg_nf GROUP BY w2; + w2 | array_agg +----+---------------- + 0 | {1,2,3,4,NULL} + 1 | {1,2,3,4,NULL} +(2 rows) + +SELECT x, array_agg(DISTINCT y ORDER BY y DESC NULLS LAST) + FROM btg_nf GROUP BY x ORDER BY x; + x | array_agg +---+-------------------- + 1 | {6,5,4,2,1,0,NULL} + 2 | {6,5,4,2,1,0,NULL} + 3 | {6,5,4,2,1,0,NULL} + 4 | {6,5,4,2,1,0,NULL} + | {6,5,4,2,1,0,NULL} +(5 rows) + +-- ... and neither is it for the GROUP BY key with enable_group_by_reordering +-- off +SET enable_group_by_reordering = off; +EXPLAIN (COSTS OFF) +SELECT x, sum(w) FROM btg_nf GROUP BY x; + QUERY PLAN +-------------------------------- + GroupAggregate + Group Key: x + -> Sort + Sort Key: x + -> Seq Scan on btg_nf + Disabled: true +(6 rows) + +RESET enable_group_by_reordering; +DROP TABLE btg_nf; -- Test the case where the number of incoming subtree path keys is more than -- the number of grouping keys. CREATE INDEX btg_y_x_w_idx ON btg(y, x, w); diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index b420a125e1c..1fe3ffc2885 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -600,6 +600,261 @@ GROUP BY s1.a; DROP TABLE eager_agg_s1; DROP TABLE eager_agg_s2; +-- Ensure the partial aggregation can use an index scan that differs in sort +-- direction from the ordering that the grouping keys took from ORDER BY +CREATE TABLE eager_agg_d1 (a int, b int, c int); +CREATE TABLE eager_agg_d2 (a int); +INSERT INTO eager_agg_d1 + SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i; +INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i; +CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b); +ANALYZE eager_agg_d1, eager_agg_d2; +SET enable_hashagg TO off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Limit + Output: d1.a, d1.b, (sum(d1.c)) + -> Incremental Sort + Output: d1.a, d1.b, (sum(d1.c)) + Sort Key: d1.a, d1.b DESC + Presorted Key: d1.a + -> Finalize GroupAggregate + Output: d1.a, d1.b, sum(d1.c) + Group Key: d1.a, d1.b + -> Merge Join + Output: d1.a, d1.b, (PARTIAL sum(d1.c)) + Merge Cond: (d1.a = d2.a) + -> Partial GroupAggregate + Output: d1.a, d1.b, PARTIAL sum(d1.c) + Group Key: d1.a, d1.b + -> Index Scan using eager_agg_d1_a_b_idx on public.eager_agg_d1 d1 + Output: d1.a, d1.b, d1.c + -> Sort + Output: d2.a + Sort Key: d2.a + -> Seq Scan on public.eager_agg_d2 d2 + Output: d2.a +(22 rows) + +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + a | b | sum +---+----+------- + 0 | 99 | 54900 + 0 | 98 | 54800 + 0 | 97 | 54700 + 0 | 96 | 54600 + 0 | 95 | 54500 +(5 rows) + +-- ... but not with enable_group_by_reordering off +SET enable_group_by_reordering TO off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Limit + Output: d1.a, d1.b, (sum(d1.c)) + -> Finalize GroupAggregate + Output: d1.a, d1.b, sum(d1.c) + Group Key: d1.a, d1.b + -> Nested Loop + Output: d1.a, d1.b, (PARTIAL sum(d1.c)) + Join Filter: (d1.a = d2.a) + -> Partial GroupAggregate + Output: d1.a, d1.b, PARTIAL sum(d1.c) + Group Key: d1.a, d1.b + -> Incremental Sort + Output: d1.a, d1.b, d1.c + Sort Key: d1.a, d1.b DESC + Presorted Key: d1.a + -> Index Scan using eager_agg_d1_a_b_idx on public.eager_agg_d1 d1 + Output: d1.a, d1.b, d1.c + -> Materialize + Output: d2.a + -> Seq Scan on public.eager_agg_d2 d2 + Output: d2.a +(21 rows) + +RESET enable_group_by_reordering; +-- Likewise for partial aggregation below a parallel join, with an index +-- that differs from the grouping key in both sort direction and NULLS +-- ordering +DROP INDEX eager_agg_d1_a_b_idx; +CREATE INDEX eager_agg_d1_a_idx ON eager_agg_d1 (a DESC NULLS LAST); +SET enable_nestloop TO off; +SET enable_hashjoin TO off; +SET parallel_setup_cost TO 0; +SET parallel_tuple_cost TO 0; +SET min_parallel_table_scan_size TO 0; +SET min_parallel_index_scan_size TO 0; +SET max_parallel_workers_per_gather TO 2; +EXPLAIN (COSTS OFF) +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a; + QUERY PLAN +----------------------------------------------------------------------------------------- + Finalize GroupAggregate + Group Key: d1.a + -> Gather Merge + Workers Planned: 2 + -> Merge Join + Merge Cond: (d1.a = d2.a) + -> Partial GroupAggregate + Group Key: d1.a + -> Parallel Index Scan using eager_agg_d1_a_idx on eager_agg_d1 d1 + -> Sort + Sort Key: d2.a DESC NULLS LAST + -> Seq Scan on eager_agg_d2 d2 +(12 rows) + +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a ORDER BY d1.a; + a | sum +---+--------- + 0 | 5005000 + 1 | 4996000 + 2 | 4997000 + 3 | 4998000 + 4 | 4999000 + 5 | 5000000 + 6 | 5001000 + 7 | 5002000 + 8 | 5003000 + 9 | 5004000 +(10 rows) + +-- ... but not with enable_group_by_reordering off +SET enable_group_by_reordering TO off; +EXPLAIN (COSTS OFF) +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a; + QUERY PLAN +-------------------------------------------------------------------- + Finalize GroupAggregate + Group Key: d1.a + -> Gather Merge + Workers Planned: 2 + -> Merge Join + Merge Cond: (d1.a = d2.a) + -> Partial GroupAggregate + Group Key: d1.a + -> Sort + Sort Key: d1.a + -> Parallel Seq Scan on eager_agg_d1 d1 + -> Sort + Sort Key: d2.a + -> Seq Scan on eager_agg_d2 d2 +(14 rows) + +RESET enable_group_by_reordering; +RESET enable_nestloop; +RESET enable_hashjoin; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET min_parallel_index_scan_size; +RESET max_parallel_workers_per_gather; +RESET enable_hashagg; +DROP TABLE eager_agg_d1; +DROP TABLE eager_agg_d2; +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error in the partial aggregation, though such an operator +-- class is not valid +CREATE TYPE eager_agg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color) + RETURNS int LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_lt); +CREATE OPERATOR === (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_eq); +-- FUNCTION 4 is needed for the grouping key to be usable in eager aggregation +CREATE OPERATOR CLASS eager_agg_color_ops DEFAULT FOR TYPE eager_agg_color + USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 eager_agg_color_cmp(eager_agg_color, eager_agg_color), + FUNCTION 4 btequalimage(oid); +CREATE TABLE eager_agg_c1 (c eager_agg_color, d int, x int); +CREATE TABLE eager_agg_c2 (k int); +INSERT INTO eager_agg_c1 + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::eager_agg_color, i % 50, i + FROM generate_series(1, 3000) i; +INSERT INTO eager_agg_c2 SELECT generate_series(0, 49); +CREATE INDEX ON eager_agg_c1 (c DESC) INCLUDE (d, x); +VACUUM ANALYZE eager_agg_c1, eager_agg_c2; +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_index_scan_size = 0; +SET enable_hashagg = off; +EXPLAIN (COSTS OFF) +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Finalize GroupAggregate + Group Key: c1.c + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: c1.c + -> Hash Join + Hash Cond: (c1.d = c2.k) + -> Partial GroupAggregate + Group Key: c1.c, c1.d + -> Sort + Sort Key: c1.c, c1.d + -> Parallel Index Only Scan using eager_agg_c1_c_d_x_idx on eager_agg_c1 c1 + -> Hash + -> Seq Scan on eager_agg_c2 c2 +(15 rows) + +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + c | sum +---+--------- + r | 1501500 + g | 1499500 + b | 1500500 +(3 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_index_scan_size; +RESET enable_hashagg; +DROP TABLE eager_agg_c1; +DROP TABLE eager_agg_c2; +DROP OPERATOR CLASS eager_agg_color_ops USING btree; +DROP OPERATOR FAMILY eager_agg_color_ops USING btree; +DROP OPERATOR <<< (eager_agg_color, eager_agg_color); +DROP OPERATOR === (eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color); +DROP TYPE eager_agg_color; DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 91f8342166f..5437017d4b5 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -1457,6 +1457,76 @@ SELECT count(*) FROM btg GROUP BY w, x, z, y ORDER BY y, x, z, w; EXPLAIN (COSTS OFF) SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z; +-- Utilize the ordering of index scan even if it differs in sort direction +-- from the ordering that the GROUP BY keys took from ORDER BY +CREATE TABLE btg_dir AS + SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y); +ANALYZE btg_dir; +EXPLAIN (COSTS OFF) +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC; +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6; + +-- Likewise for a backward index scan that also provides the ordering +-- required by the aggregate +EXPLAIN (COSTS OFF) +SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x; +SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x; +DROP TABLE btg_dir; + +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error, though such an operator class is not valid +BEGIN; +CREATE TYPE btg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int + LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_lt); +CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_eq); +CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 btg_color_cmp(btg_color, btg_color); +CREATE TABLE btg_color_tab AS + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c + FROM generate_series(1, 300) AS i; +CREATE INDEX ON btg_color_tab (c); +ANALYZE btg_color_tab; +SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c; +ROLLBACK; + +-- Utilize the ordering of index scan even if it differs only in NULLS +-- ordering from the GROUP BY key +CREATE TABLE btg_nf AS + SELECT nullif(i % 5, 0) AS x, nullif(i % 7, 3) AS y, i AS w + FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_nf_x_y_idx ON btg_nf(x NULLS FIRST, y); +ANALYZE btg_nf; +EXPLAIN (COSTS OFF) +SELECT x, count(*) FROM btg_nf GROUP BY x; +SELECT x, count(*) FROM btg_nf GROUP BY x ORDER BY x; + +-- ... but the ordering required by an aggregate must match exactly, so the +-- index is of no use for an aggregate's ORDER BY key that differs in NULLS +-- ordering +EXPLAIN (COSTS OFF) +SELECT w % 2 AS w2, array_agg(DISTINCT x ORDER BY x) FROM btg_nf GROUP BY w2; +SELECT w % 2 AS w2, array_agg(DISTINCT x ORDER BY x) FROM btg_nf GROUP BY w2; +SELECT x, array_agg(DISTINCT y ORDER BY y DESC NULLS LAST) + FROM btg_nf GROUP BY x ORDER BY x; + +-- ... and neither is it for the GROUP BY key with enable_group_by_reordering +-- off +SET enable_group_by_reordering = off; +EXPLAIN (COSTS OFF) +SELECT x, sum(w) FROM btg_nf GROUP BY x; +RESET enable_group_by_reordering; +DROP TABLE btg_nf; + -- Test the case where the number of incoming subtree path keys is more than -- the number of grouping keys. CREATE INDEX btg_y_x_w_idx ON btg(y, x, w); diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql index 7e3623aa6fd..2be4ac84d8b 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -226,6 +226,149 @@ GROUP BY s1.a; DROP TABLE eager_agg_s1; DROP TABLE eager_agg_s2; +-- Ensure the partial aggregation can use an index scan that differs in sort +-- direction from the ordering that the grouping keys took from ORDER BY +CREATE TABLE eager_agg_d1 (a int, b int, c int); +CREATE TABLE eager_agg_d2 (a int); +INSERT INTO eager_agg_d1 + SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i; +INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i; +CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b); +ANALYZE eager_agg_d1, eager_agg_d2; + +SET enable_hashagg TO off; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + +-- ... but not with enable_group_by_reordering off +SET enable_group_by_reordering TO off; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + +RESET enable_group_by_reordering; + +-- Likewise for partial aggregation below a parallel join, with an index +-- that differs from the grouping key in both sort direction and NULLS +-- ordering +DROP INDEX eager_agg_d1_a_b_idx; +CREATE INDEX eager_agg_d1_a_idx ON eager_agg_d1 (a DESC NULLS LAST); + +SET enable_nestloop TO off; +SET enable_hashjoin TO off; +SET parallel_setup_cost TO 0; +SET parallel_tuple_cost TO 0; +SET min_parallel_table_scan_size TO 0; +SET min_parallel_index_scan_size TO 0; +SET max_parallel_workers_per_gather TO 2; + +EXPLAIN (COSTS OFF) +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a; + +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a ORDER BY d1.a; + +-- ... but not with enable_group_by_reordering off +SET enable_group_by_reordering TO off; + +EXPLAIN (COSTS OFF) +SELECT d1.a, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a; + +RESET enable_group_by_reordering; +RESET enable_nestloop; +RESET enable_hashjoin; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET min_parallel_index_scan_size; +RESET max_parallel_workers_per_gather; + +RESET enable_hashagg; + +DROP TABLE eager_agg_d1; +DROP TABLE eager_agg_d2; + +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error in the partial aggregation, though such an operator +-- class is not valid +CREATE TYPE eager_agg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color) + RETURNS int LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_lt); +CREATE OPERATOR === (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_eq); +-- FUNCTION 4 is needed for the grouping key to be usable in eager aggregation +CREATE OPERATOR CLASS eager_agg_color_ops DEFAULT FOR TYPE eager_agg_color + USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 eager_agg_color_cmp(eager_agg_color, eager_agg_color), + FUNCTION 4 btequalimage(oid); +CREATE TABLE eager_agg_c1 (c eager_agg_color, d int, x int); +CREATE TABLE eager_agg_c2 (k int); +INSERT INTO eager_agg_c1 + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::eager_agg_color, i % 50, i + FROM generate_series(1, 3000) i; +INSERT INTO eager_agg_c2 SELECT generate_series(0, 49); +CREATE INDEX ON eager_agg_c1 (c DESC) INCLUDE (d, x); +VACUUM ANALYZE eager_agg_c1, eager_agg_c2; + + +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_index_scan_size = 0; +SET enable_hashagg = off; + +EXPLAIN (COSTS OFF) +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_index_scan_size; +RESET enable_hashagg; + +DROP TABLE eager_agg_c1; +DROP TABLE eager_agg_c2; +DROP OPERATOR CLASS eager_agg_color_ops USING btree; +DROP OPERATOR FAMILY eager_agg_color_ops USING btree; +DROP OPERATOR <<< (eager_agg_color, eager_agg_color); +DROP OPERATOR === (eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color); +DROP TYPE eager_agg_color; + DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; -- 2.50.1 (Apple Git-155)