From 0c0f682bf88b33e8209f4b872114082c270e2816 Mon Sep 17 00:00:00 2001 From: Iliia Khaprov Date: Sun, 18 Oct 2026 21:10:58 +0200 Subject: [PATCH] Remove useless ORDER BY from scalar subqueries If a subquery is expected to return at most one row, sorting is useless. It can also be costly: the Sort reads its whole input first, so a subquery that yields many rows runs to completion before "more than one row returned by a subquery" is raised. Keep the ORDER BY if LIMIT, OFFSET or DISTINCT ON depend on it, rows are locked, or a sort key is volatile. --- src/backend/optimizer/plan/subselect.c | 45 +++++++++++++ src/test/regress/expected/subselect.out | 89 +++++++++++++++++++++++++ src/test/regress/sql/subselect.sql | 26 ++++++++ 3 files changed, 160 insertions(+) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index cac2aaca2e8..d463b6f3294 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -93,6 +93,7 @@ static bool contain_outer_selfref_walker(Node *node, Index *depth); static void inline_cte(PlannerInfo *root, CommonTableExpr *cte); static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); static bool simplify_EXISTS_query(PlannerInfo *root, Query *query); +static void remove_scalar_sublink_ordering(Query *query); static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, Node **testexpr, List **paramIds); static Node *replace_correlation_vars_mutator(Node *node, PlannerInfo *root); @@ -189,6 +190,12 @@ make_subplan(PlannerInfo *root, Query *orig_subquery, if (subLinkType == EXISTS_SUBLINK) simple_exists = simplify_EXISTS_query(root, subquery); + /* Sorting is useless if the subquery returns at most one row */ + if (subLinkType == EXPR_SUBLINK || + subLinkType == MULTIEXPR_SUBLINK || + subLinkType == ROWCOMPARE_SUBLINK) + remove_scalar_sublink_ordering(subquery); + /* * For an EXISTS subplan, tell lower-level planner to expect that only the * first tuple will be retrieved. For ALL and ANY subplans, we will be @@ -1494,6 +1501,44 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, return result; } +/* + * remove_scalar_sublink_ordering: drop a useless ORDER BY from the subquery + * of an EXPR, MULTIEXPR or ROWCOMPARE sublink + * + * If a subquery is expected to return at most one row, sorting is useless, + * and a Sort reads all of its input before returning anything. Keep the + * ORDER BY if it can change which rows are returned, if rows are locked, or + * if a sort key is volatile. + */ +static void +remove_scalar_sublink_ordering(Query *query) +{ + ListCell *lc; + + if (query->sortClause == NIL) + return; + + if (query->commandType != CMD_SELECT || + query->setOperations || + query->hasModifyingCTE || + query->hasDistinctOn || + query->limitCount || + query->limitOffset || + query->rowMarks) + return; + + foreach(lc, query->sortClause) + { + SortGroupClause *sgc = (SortGroupClause *) lfirst(lc); + Node *expr = get_sortgroupclause_expr(sgc, query->targetList); + + if (contain_volatile_functions(expr)) + return; + } + + query->sortClause = NIL; +} + /* * simplify_EXISTS_query: remove any useless stuff in an EXISTS's subquery * diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index b81923f2e74..b3539370f34 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -1744,3 +1744,92 @@ select * from x for update; Output: subselect_tbl.f1, subselect_tbl.f2, subselect_tbl.f3 (2 rows) +-- +-- Useless ORDER BY in scalar subqueries +-- +create temp table scalar_sort (a int); +insert into scalar_sort select generate_series(1, 5); +-- Sort removed +explain (costs off) +select (select a from scalar_sort where a > 4 order by a); + QUERY PLAN +--------------------------------- + Result + InitPlan 1 (returns $0) + -> Seq Scan on scalar_sort + Filter: (a > 4) +(4 rows) + +select (select a from scalar_sort where a > 4 order by a); + a +--- + 5 +(1 row) + +explain (costs off) +update scalar_sort set a = (select a from scalar_sort s where s.a > 4 order by a desc); + QUERY PLAN +----------------------------------- + Update on scalar_sort + InitPlan 1 (returns $0) + -> Seq Scan on scalar_sort s + Filter: (a > 4) + -> Seq Scan on scalar_sort +(5 rows) + +-- fails without sorting first +select (select x.a from scalar_sort x, generate_series(1, 100000) order by 1); +ERROR: more than one row returned by a subquery used as an expression +-- sort expressions are still evaluated +select (select a from scalar_sort where a = 2 order by 1 / (a - 2)); +ERROR: division by zero +-- Sort kept +explain (costs off) +select (select a from scalar_sort order by a desc limit 1); + QUERY PLAN +---------------------------------------------- + Result + InitPlan 1 (returns $0) + -> Limit + -> Sort + Sort Key: scalar_sort.a DESC + -> Seq Scan on scalar_sort +(6 rows) + +explain (costs off) +select (select distinct on (a > 0) a from scalar_sort order by a > 0, a desc); + QUERY PLAN +--------------------------------------------------------------------- + Result + InitPlan 1 (returns $0) + -> Unique + -> Sort + Sort Key: ((scalar_sort.a > 0)), scalar_sort.a DESC + -> Seq Scan on scalar_sort +(6 rows) + +explain (costs off) +select (select a from scalar_sort order by random()); + QUERY PLAN +--------------------------------------- + Result + InitPlan 1 (returns $0) + -> Sort + Sort Key: (random()) + -> Seq Scan on scalar_sort +(5 rows) + +explain (costs off) +select (select a from scalar_sort where a > 4 order by a for update); + QUERY PLAN +--------------------------------------------- + Result + InitPlan 1 (returns $1) + -> LockRows + -> Sort + Sort Key: scalar_sort.a + -> Seq Scan on scalar_sort + Filter: (a > 4) +(7 rows) + +drop table scalar_sort; diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index cce8ebdb3d9..f83bab0fd2a 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -898,3 +898,29 @@ select * from (with x as (select 2 as y) select * from x) ss; explain (verbose, costs off) with x as (select * from subselect_tbl) select * from x for update; + +-- +-- Useless ORDER BY in scalar subqueries +-- +create temp table scalar_sort (a int); +insert into scalar_sort select generate_series(1, 5); +-- Sort removed +explain (costs off) +select (select a from scalar_sort where a > 4 order by a); +select (select a from scalar_sort where a > 4 order by a); +explain (costs off) +update scalar_sort set a = (select a from scalar_sort s where s.a > 4 order by a desc); +-- fails without sorting first +select (select x.a from scalar_sort x, generate_series(1, 100000) order by 1); +-- sort expressions are still evaluated +select (select a from scalar_sort where a = 2 order by 1 / (a - 2)); +-- Sort kept +explain (costs off) +select (select a from scalar_sort order by a desc limit 1); +explain (costs off) +select (select distinct on (a > 0) a from scalar_sort order by a > 0, a desc); +explain (costs off) +select (select a from scalar_sort order by random()); +explain (costs off) +select (select a from scalar_sort where a > 4 order by a for update); +drop table scalar_sort; -- 2.53.0