From f3f771df1f68072f88bb7077ad75e5ab6abfde5a Mon Sep 17 00:00:00 2001 From: Hannu Krosing Date: Wed, 16 Sep 2026 13:22:47 +0000 Subject: [PATCH 2/4] Support inline ORDER BY in DISTINCT ON for SELECT and enable HashAgg This patch introduces the syntax: SELECT DISTINCT ON (keys ORDER BY sort_keys) ... This decouples the tie-breaker sort order used for duplicate elimination from the final output sort order of the query, avoiding the need for subqueries. In addition, this enables Hash Aggregate to execute DISTINCT ON queries with tie-breaking by introducing ReplaceTupleHashEntryIfBetter() and using AllocSetContext for hashed tuples when sort columns are present. --- doc/src/sgml/ref/select.sgml | 37 +++++---- src/backend/executor/execGrouping.c | 80 +++++++++++++++++++ src/backend/executor/nodeAgg.c | 78 ++++++++++++++++-- src/backend/optimizer/plan/createplan.c | 46 +++++++++++ src/backend/optimizer/plan/planner.c | 42 ++++++++-- src/backend/optimizer/util/pathnode.c | 19 +++++ src/backend/parser/analyze.c | 41 +++++++++- src/backend/parser/gram.y | 19 ++++- src/include/executor/executor.h | 6 ++ src/include/executor/nodeAgg.h | 2 + src/include/nodes/parsenodes.h | 3 + src/include/nodes/pathnodes.h | 1 + src/include/nodes/plannodes.h | 7 ++ src/include/optimizer/pathnode.h | 11 +++ .../regress/expected/select_distinct_on.out | 75 +++++++++++++++++ src/test/regress/sql/select_distinct_on.sql | 22 +++++ 16 files changed, 459 insertions(+), 30 deletions(-) diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index 68fb4911769..2a18ed13490 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -1246,30 +1246,39 @@ EXCLUDE NO OTHERS SELECT DISTINCT ON ( expression [, ...] ) + class="parameter">expression [, ...] [ ORDER BY sort_expression [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] ] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. The DISTINCT ON expressions are interpreted using the same rules as for - ORDER BY (see above). Note that the first - row of each set is unpredictable unless ORDER - BY is used to ensure that the desired row appears first. For - example: + ORDER BY (see above). + If the optional inline ORDER BY clause is specified, + it determines which row is kept from each set of duplicates (the first row + according to this sort order). This inline sorting applies only for duplicate + resolution and does not dictate the final output order of the query. + Note that if no inline ORDER BY is used, the first + row of each set is unpredictable unless a global ORDER + BY is used at the end of the query to ensure that the desired row appears first. + For example: -SELECT DISTINCT ON (location) location, time, report - FROM weather_reports - ORDER BY location, time DESC; +SELECT DISTINCT ON (location ORDER BY time DESC) location, time, report + FROM weather_reports; retrieves the most recent weather report for each location. But - if we had not used ORDER BY to force descending order - of time values for each location, we'd have gotten a report from - an unpredictable time for each location. + if we had not used inline ORDER BY time DESC, we'd have gotten a report from + an unpredictable time for each location (unless a global ORDER BY was used). - The DISTINCT ON expression(s) must match the leftmost - ORDER BY expression(s). The ORDER BY clause - will normally contain additional expression(s) that determine the + If a global ORDER BY clause is used at the end of the query + without an inline ORDER BY inside DISTINCT ON, + the DISTINCT ON expression(s) must match the leftmost + ORDER BY expression(s). The global ORDER BY clause + will then normally contain additional expression(s) that determine the desired precedence of rows within each DISTINCT ON group. + If an inline ORDER BY is specified, this matching requirement + is relaxed, and the global ORDER BY can sort the final results + by any columns, independent of the distinct keys. diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c index feee88294aa..a9c45af57b8 100644 --- a/src/backend/executor/execGrouping.c +++ b/src/backend/executor/execGrouping.c @@ -22,6 +22,7 @@ #include "executor/executor.h" #include "miscadmin.h" #include "utils/lsyscache.h" +#include "utils/sortsupport.h" static int TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple tuple2); static inline uint32 TupleHashTableHash_internal(struct tuplehash_hash *tb, @@ -622,3 +623,82 @@ TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple econtext->ecxt_outertuple = slot1; return !ExecQualAndReset(hashtable->cur_eq_func, econtext); } + +/* + * ReplaceTupleHashEntryIfBetter + * + * Compare the new slot with the stored tuple in the entry using the sort keys. + * If the new slot is "better" (comes before in sort order), replace the stored + * tuple in the entry. + * + * Returns true if replaced, false otherwise. + */ +bool +ReplaceTupleHashEntryIfBetter(TupleHashTable hashtable, + TupleHashEntry entry, + TupleTableSlot *newslot, + TupleTableSlot *firstslot, + SortSupport sortKeys, + int numSortCols) +{ + int i; + bool replace = false; + + /* If no sort keys, we shouldn't be here */ + if (numSortCols == 0) + return false; + + /* Retrieve stored tuple and store it in firstslot */ + ExecStoreMinimalTuple(entry->firstTuple, firstslot, false); + + /* Compare sort keys one by one */ + for (i = 0; i < numSortCols; i++) + { + SortSupport skey = &sortKeys[i]; + AttrNumber attno = skey->ssup_attno; + Datum datum1, datum2; + bool isnull1, isnull2; + int compare; + + datum1 = slot_getattr(firstslot, attno, &isnull1); + datum2 = slot_getattr(newslot, attno, &isnull2); + + compare = ApplySortComparator(datum1, isnull1, + datum2, isnull2, + skey); + + if (compare != 0) + { + /* + * ApplySortComparator returns < 0 if datum1 comes BEFORE datum2. + * So if compare > 0, datum2 comes BEFORE datum1, so it is better. + */ + if (compare > 0) + replace = true; + break; /* Found a difference, no need to compare further */ + } + } + + if (replace) + { + MinimalTuple oldtuple = entry->firstTuple; + MinimalTuple newtuple; + MemoryContext oldcxt; + + /* Copy new tuple into the long-lived context */ + oldcxt = MemoryContextSwitchTo(hashtable->tuplescxt); + newtuple = ExecCopySlotMinimalTuple(newslot); + MemoryContextSwitchTo(oldcxt); + + /* Replace in entry */ + entry->firstTuple = newtuple; + + /* Free old tuple */ + pfree(oldtuple); + } + + /* Clear the comparison slot to avoid holding references */ + ExecClearTuple(firstslot); + + return replace; +} diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c index 962cd9c8255..e01dc0ac0bb 100644 --- a/src/backend/executor/nodeAgg.c +++ b/src/backend/executor/nodeAgg.c @@ -277,6 +277,7 @@ #include "utils/memutils_memorychunk.h" #include "utils/syscache.h" #include "utils/tuplesort.h" +#include "utils/sortsupport.h" /* * Control how many partitions are created when spilling HashAgg to @@ -1684,6 +1685,30 @@ find_hash_columns(AggState *aggstate) ExecAllocTableSlot(&estate->es_tupleTable, hashDesc, &TTSOpsMinimalTuple, 0); + if (perhash->aggnode->numSortCols > 0) + { + perhash->hash_firstTupleSlot = + ExecAllocTableSlot(&estate->es_tupleTable, hashDesc, + &TTSOpsMinimalTuple, 0); + + /* Initialize sort support */ + perhash->sortKeys = (SortSupport) palloc0(sizeof(SortSupportData) * perhash->aggnode->numSortCols); + for (i = 0; i < perhash->aggnode->numSortCols; i++) + { + SortSupport skey = &perhash->sortKeys[i]; + + skey->ssup_collation = perhash->aggnode->sortCollations[i]; + skey->ssup_nulls_first = perhash->aggnode->sortNullsFirst[i]; + skey->ssup_attno = perhash->aggnode->sortColIdx[i]; + PrepareSortSupportFromOrderingOp(perhash->aggnode->sortOperators[i], skey); + } + } + else + { + perhash->hash_firstTupleSlot = NULL; + perhash->sortKeys = NULL; + } + list_free(hashTlist); bms_free(colnos); } @@ -1996,7 +2021,25 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions) static void hash_create_memory(AggState *aggstate) { + Agg *node = (Agg *) aggstate->ss.ps.plan; Size maxBlockSize = ALLOCSET_DEFAULT_MAXSIZE; + bool use_allocset = false; + ListCell *lc; + + if (node->numSortCols > 0) + use_allocset = true; + else + { + foreach(lc, node->chain) + { + Agg *chained_node = lfirst_node(Agg, lc); + if (chained_node->numSortCols > 0) + { + use_allocset = true; + break; + } + } + } /* * The hashcontext's per-tuple memory will be used for byref transition @@ -2040,11 +2083,20 @@ hash_create_memory(AggState *aggstate) /* and no smaller than ALLOCSET_DEFAULT_INITSIZE */ maxBlockSize = Max(maxBlockSize, ALLOCSET_DEFAULT_INITSIZE); - aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt, - "HashAgg hashed tuples", - ALLOCSET_DEFAULT_MINSIZE, - ALLOCSET_DEFAULT_INITSIZE, - maxBlockSize); + if (use_allocset) + { + aggstate->hash_tuplescxt = AllocSetContextCreate(aggstate->ss.ps.state->es_query_cxt, + "HashAgg hashed tuples", + ALLOCSET_DEFAULT_SIZES); + } + else + { + aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt, + "HashAgg hashed tuples", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + maxBlockSize); + } } @@ -2178,6 +2230,18 @@ initialize_hash_entry(AggState *aggstate, TupleHashTable hashtable, * for each grouping set, making the refilling of the hash table very * efficient. */ +static void +replace_hash_entry_if_better(AggState *aggstate, AggStatePerHash perhash, + TupleHashEntry entry, TupleTableSlot *newslot) +{ + ReplaceTupleHashEntryIfBetter(perhash->hashtable, + entry, + newslot, + perhash->hash_firstTupleSlot, + perhash->sortKeys, + perhash->aggnode->numSortCols); +} + static void lookup_hash_entries(AggState *aggstate) { @@ -2210,6 +2274,8 @@ lookup_hash_entries(AggState *aggstate) { if (isnew) initialize_hash_entry(aggstate, hashtable, entry); + else if (perhash->aggnode->numSortCols > 0) + replace_hash_entry_if_better(aggstate, perhash, entry, hashslot); pergroup[setno] = TupleHashEntryGetAdditional(hashtable, entry); } else @@ -2770,6 +2836,8 @@ agg_refill_hash_table(AggState *aggstate) { if (isnew) initialize_hash_entry(aggstate, hashtable, entry); + else if (perhash->aggnode->numSortCols > 0) + replace_hash_entry_if_better(aggstate, perhash, entry, hashslot); aggstate->hash_pergroup[batch->setno] = TupleHashEntryGetAdditional(hashtable, entry); advance_aggregates(aggstate); } diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 02a888c5996..4524a6e11f0 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -2193,6 +2193,52 @@ create_agg_plan(PlannerInfo *root, AggPath *best_path) best_path->transitionSpace, subplan); + /* Extract sort keys for inline DISTINCT ON ORDER BY */ + if (best_path->distinctSortClause) + { + List *sortcls = best_path->distinctSortClause; + List *sub_tlist = subplan->targetlist; + ListCell *l; + int numsortkeys; + AttrNumber *sortColIdx; + Oid *sortOperators; + Oid *collations; + bool *nullsFirst; + + numsortkeys = list_length(sortcls); + sortColIdx = (AttrNumber *) palloc(numsortkeys * sizeof(AttrNumber)); + sortOperators = (Oid *) palloc(numsortkeys * sizeof(Oid)); + collations = (Oid *) palloc(numsortkeys * sizeof(Oid)); + nullsFirst = (bool *) palloc(numsortkeys * sizeof(bool)); + + numsortkeys = 0; + foreach(l, sortcls) + { + SortGroupClause *sortcl = (SortGroupClause *) lfirst(l); + TargetEntry *tle = get_sortgroupclause_tle(sortcl, sub_tlist); + + sortColIdx[numsortkeys] = tle->resno; + sortOperators[numsortkeys] = sortcl->sortop; + collations[numsortkeys] = exprCollation((Node *) tle->expr); + nullsFirst[numsortkeys] = sortcl->nulls_first; + numsortkeys++; + } + + plan->numSortCols = numsortkeys; + plan->sortColIdx = sortColIdx; + plan->sortOperators = sortOperators; + plan->sortCollations = collations; + plan->sortNullsFirst = nullsFirst; + } + else + { + plan->numSortCols = 0; + plan->sortColIdx = NULL; + plan->sortOperators = NULL; + plan->sortCollations = NULL; + plan->sortNullsFirst = NULL; + } + copy_generic_path_info(&plan->plan, (Path *) best_path); return plan; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 50d5a140375..99a9ca9ba9d 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -3825,16 +3825,46 @@ standard_qp_callback(PlannerInfo *root, void *extra) /* Make a copy since pathkey processing can modify the list */ root->processed_distinctClause = list_copy(parse->distinctClause); - root->distinct_pathkeys = + + if (parse->distinctSortClause) + { + /* We have DISTINCT ON with ORDER BY */ + List *temp_distinct_clause = list_copy(parse->distinctSortClause); + bool temp_sortable; + root->distinct_pathkeys = + make_pathkeys_for_sortclauses_extended(root, + &temp_distinct_clause, + tlist, + true, + false, + &sortable, + false); + if (!sortable) + root->distinct_pathkeys = NIL; + + /* We ALSO need to remove redundant keys from processed_distinctClause */ make_pathkeys_for_sortclauses_extended(root, &root->processed_distinctClause, tlist, true, false, - &sortable, + &temp_sortable, false); - if (!sortable) - root->distinct_pathkeys = NIL; + } + else + { + /* Standard DISTINCT or DISTINCT ON without ORDER BY */ + root->distinct_pathkeys = + make_pathkeys_for_sortclauses_extended(root, + &root->processed_distinctClause, + tlist, + true, + false, + &sortable, + false); + if (!sortable) + root->distinct_pathkeys = NIL; + } } else root->distinct_pathkeys = NIL; @@ -5223,7 +5253,7 @@ create_partial_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, add_partial_path(partial_distinct_rel, (Path *) create_unique_path(root, partial_distinct_rel, sorted_path, - list_length(root->distinct_pathkeys), + list_length(root->processed_distinctClause), numDistinctRows)); } } @@ -5417,7 +5447,7 @@ create_final_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, add_path(distinct_rel, (Path *) create_unique_path(root, distinct_rel, sorted_path, - list_length(root->distinct_pathkeys), + list_length(root->processed_distinctClause), numDistinctRows)); } } diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 2ba31765ca5..d1d129c2f23 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3077,6 +3077,24 @@ create_agg_path(PlannerInfo *root, List *qual, const AggClauseCosts *aggcosts, double numGroups) +{ + return create_agg_path_ext(root, rel, subpath, target, + aggstrategy, aggsplit, groupClause, + qual, aggcosts, numGroups, NIL); +} + +AggPath * +create_agg_path_ext(PlannerInfo *root, + RelOptInfo *rel, + Path *subpath, + PathTarget *target, + AggStrategy aggstrategy, + AggSplit aggsplit, + List *groupClause, + List *qual, + const AggClauseCosts *aggcosts, + double numGroups, + List *distinctSortClause) { AggPath *pathnode = makeNode(AggPath); @@ -3115,6 +3133,7 @@ create_agg_path(PlannerInfo *root, pathnode->numGroups = numGroups; pathnode->transitionSpace = aggcosts ? aggcosts->transitionSpace : 0; pathnode->groupClause = groupClause; + pathnode->distinctSortClause = distinctSortClause; pathnode->qual = qual; cost_agg(&pathnode->path, root, diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index e89f4684ade..27e088107a4 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1738,6 +1738,33 @@ count_rowexpr_columns(ParseState *pstate, Node *expr) * Note: this covers only cases with no set operations and no VALUES lists; * see below for the other cases. */ +static List * +prepend_distinct_to_sortby(List *distinctClause, List *distinctSortClause) +{ + List *result = list_copy(distinctSortClause); + ListCell *lc; + List *prepended = NIL; + + /* If distinctClause is empty or has NULL (SELECT DISTINCT), do nothing */ + if (distinctClause == NIL || linitial(distinctClause) == NULL) + return distinctSortClause; + + foreach(lc, distinctClause) + { + Node *key = (Node *) lfirst(lc); + SortBy *sb = makeNode(SortBy); + + sb->node = key; + sb->sortby_dir = SORTBY_DEFAULT; + sb->sortby_nulls = SORTBY_NULLS_DEFAULT; + sb->useOp = NIL; + sb->location = -1; + prepended = lappend(prepended, sb); + } + + return list_concat(prepended, result); +} + static Query * transformSelectStmt(ParseState *pstate, SelectStmt *stmt, SelectStmtPassthrough *passthru) @@ -1745,6 +1772,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, Query *qry = makeNode(Query); Node *qual; ListCell *l; + List *distinctSortClause = NIL; qry->commandType = CMD_SELECT; @@ -1817,6 +1845,17 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, false /* allow SQL92 rules */ ); qry->groupDistinct = stmt->groupDistinct; + if (stmt->distinctSortClause) + { + List *full_sortby = prepend_distinct_to_sortby(stmt->distinctClause, stmt->distinctSortClause); + distinctSortClause = transformSortClause(pstate, + full_sortby, + &qry->targetList, + EXPR_KIND_ORDER_BY, + false); + } + qry->distinctSortClause = distinctSortClause; + if (stmt->distinctClause == NIL) { qry->distinctClause = NIL; @@ -1837,7 +1876,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, qry->distinctClause = transformDistinctOnClause(pstate, stmt->distinctClause, &qry->targetList, - qry->sortClause); + distinctSortClause ? distinctSortClause : qry->sortClause); qry->hasDistinctOn = true; } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c025eaaaa4e..a005940374c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -13719,7 +13719,8 @@ simple_select: { SelectStmt *n = makeNode(SelectStmt); - n->distinctClause = $2; + n->distinctClause = linitial($2); + n->distinctSortClause = lsecond($2); n->targetList = $3; n->intoClause = $4; n->fromClause = $5; @@ -13973,8 +13974,9 @@ set_quantifier: * should be placed in the DISTINCT list during parsetree analysis. */ distinct_clause: - DISTINCT { $$ = list_make1(NIL); } - | DISTINCT ON '(' expr_list ')' { $$ = $4; } + DISTINCT { $$ = list_make2(list_make1(NIL), NIL); } + | DISTINCT ON '(' expr_list ')' { $$ = list_make2($4, NIL); } + | DISTINCT ON '(' expr_list sort_clause ')' { $$ = list_make2($4, $5); } ; opt_all_clause: @@ -18734,7 +18736,16 @@ PLpgSQL_Expr: opt_distinct_clause opt_target_list { SelectStmt *n = makeNode(SelectStmt); - n->distinctClause = $1; + if ($1) + { + n->distinctClause = linitial($1); + n->distinctSortClause = lsecond($1); + } + else + { + n->distinctClause = NIL; + n->distinctSortClause = NIL; + } n->targetList = $2; n->fromClause = $3; n->whereClause = $4; diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 190e8a4897a..4bce1abc3e0 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -153,6 +153,12 @@ extern uint32 TupleHashTableHash(TupleHashTable hashtable, extern TupleHashEntry LookupTupleHashEntryHash(TupleHashTable hashtable, TupleTableSlot *slot, bool *isnew, uint32 hash); +extern bool ReplaceTupleHashEntryIfBetter(TupleHashTable hashtable, + TupleHashEntry entry, + TupleTableSlot *newslot, + TupleTableSlot *firstslot, + SortSupport sortKeys, + int numSortCols); extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable, TupleTableSlot *slot, ExprState *eqcomp, diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h index 1e1be9666ae..e9f0cddec32 100644 --- a/src/include/executor/nodeAgg.h +++ b/src/include/executor/nodeAgg.h @@ -319,6 +319,8 @@ typedef struct AggStatePerHashData AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */ AttrNumber *hashGrpColIdxHash; /* indices in hash table tuples */ Agg *aggnode; /* original Agg node, for numGroups etc. */ + TupleTableSlot *hash_firstTupleSlot; /* slot for comparing stored tuples */ + SortSupport sortKeys; /* sort support for distinct ON comparisons */ } AggStatePerHashData; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a0ab2b885e8..a8ed100eb02 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -229,6 +229,8 @@ typedef struct Query List *distinctClause; /* a list of SortGroupClause's */ + List *distinctSortClause; /* a list of SortGroupClause's for inline DISTINCT ON ORDER BY */ + List *sortClause; /* a list of SortGroupClause's */ Node *limitOffset; /* # of result tuples to skip (int8 expr) */ @@ -2346,6 +2348,7 @@ typedef struct SelectStmt struct SelectStmt *larg; /* left child */ struct SelectStmt *rarg; /* right child */ /* Eventually add fields for CORRESPONDING spec here */ + List *distinctSortClause; /* inline DISTINCT ON ORDER BY clause (list of SortBy's) */ } SelectStmt; diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index c48e656ce80..aebaaa60304 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -2596,6 +2596,7 @@ typedef struct AggPath Cardinality numGroups; /* estimated number of groups in input */ uint64 transitionSpace; /* for pass-by-ref transition data */ List *groupClause; /* a list of SortGroupClause's */ + List *distinctSortClause; /* a list of SortGroupClause's for inline DISTINCT ON ORDER BY */ List *qual; /* quals (HAVING quals), if any */ } AggPath; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 2fe6b61afaf..accdd1f5098 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1244,6 +1244,13 @@ typedef struct Agg /* grouping sets to use */ List *groupingSets; + /* sort keys for inline DISTINCT ON ORDER BY (if any) */ + int numSortCols; + AttrNumber *sortColIdx pg_node_attr(array_size(numSortCols)); + Oid *sortOperators pg_node_attr(array_size(numSortCols)); + Oid *sortCollations pg_node_attr(array_size(numSortCols)); + bool *sortNullsFirst pg_node_attr(array_size(numSortCols)); + /* chained Agg/Sort nodes */ List *chain; } Agg; diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index e8db321f92b..85c4d4fe9d3 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -269,6 +269,17 @@ extern AggPath *create_agg_path(PlannerInfo *root, List *qual, const AggClauseCosts *aggcosts, double numGroups); +extern AggPath *create_agg_path_ext(PlannerInfo *root, + RelOptInfo *rel, + Path *subpath, + PathTarget *target, + AggStrategy aggstrategy, + AggSplit aggsplit, + List *groupClause, + List *qual, + const AggClauseCosts *aggcosts, + double numGroups, + List *distinctSortClause); extern GroupingSetsPath *create_groupingsets_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, diff --git a/src/test/regress/expected/select_distinct_on.out b/src/test/regress/expected/select_distinct_on.out index 75b1e7d300f..4c24a8b7fdb 100644 --- a/src/test/regress/expected/select_distinct_on.out +++ b/src/test/regress/expected/select_distinct_on.out @@ -246,3 +246,78 @@ SELECT DISTINCT ON (y, x) x, y FROM (select * from distinct_on_tbl order by x, z RESET enable_hashagg; DROP TABLE distinct_on_tbl; +-- +-- Test SELECT DISTINCT ON with inline ORDER BY +-- +CREATE TABLE distinct_inline_tbl (a int, b int); +INSERT INTO distinct_inline_tbl VALUES (1, 10), (1, 20), (2, 5), (2, 15); +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; + QUERY PLAN +--------------------------------------------- + Unique + -> Sort + Sort Key: a, b DESC + -> Seq Scan on distinct_inline_tbl +(4 rows) + +SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; + a | b +---+---- + 1 | 20 + 2 | 15 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; + QUERY PLAN +--------------------------------------------- + Unique + -> Sort + Sort Key: a, b + -> Seq Scan on distinct_inline_tbl +(4 rows) + +SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; + a | b +---+---- + 1 | 10 + 2 | 5 +(2 rows) + +-- Test with HashAgg +SET enable_sort TO OFF; +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl; + QUERY PLAN +--------------------------------------------- + Unique + -> Sort + Disabled: true + Sort Key: a, b DESC + -> Seq Scan on distinct_inline_tbl +(5 rows) + +SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; + a | b +---+---- + 1 | 20 + 2 | 15 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl; + QUERY PLAN +--------------------------------------------- + Unique + -> Sort + Disabled: true + Sort Key: a, b + -> Seq Scan on distinct_inline_tbl +(5 rows) + +SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; + a | b +---+---- + 1 | 10 + 2 | 5 +(2 rows) + +RESET enable_sort; +DROP TABLE distinct_inline_tbl; diff --git a/src/test/regress/sql/select_distinct_on.sql b/src/test/regress/sql/select_distinct_on.sql index 8680749e49a..df844119938 100644 --- a/src/test/regress/sql/select_distinct_on.sql +++ b/src/test/regress/sql/select_distinct_on.sql @@ -82,3 +82,25 @@ SELECT DISTINCT ON (y, x) x, y FROM (select * from distinct_on_tbl order by x, z RESET enable_hashagg; DROP TABLE distinct_on_tbl; + +-- +-- Test SELECT DISTINCT ON with inline ORDER BY +-- +CREATE TABLE distinct_inline_tbl (a int, b int); +INSERT INTO distinct_inline_tbl VALUES (1, 10), (1, 20), (2, 5), (2, 15); + +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; +SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; +SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; + +-- Test with HashAgg +SET enable_sort TO OFF; +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl; +SELECT DISTINCT ON (a ORDER BY b DESC) a, b FROM distinct_inline_tbl ORDER BY a; +EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl; +SELECT DISTINCT ON (a ORDER BY b ASC) a, b FROM distinct_inline_tbl ORDER BY a; +RESET enable_sort; + +DROP TABLE distinct_inline_tbl; + -- 2.55.0.1082.g2b9226bbc0-goog