From 2dbe042186b64ab2649c26867841dfb20a40486d Mon Sep 17 00:00:00 2001 From: Iliia Khaprov Date: Tue, 20 Oct 2026 21:17:48 +0200 Subject: [PATCH v2] 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, or rows are locked. The sort expressions stay in the targetlist, so they are still evaluated, with their errors and side effects. --- src/backend/optimizer/plan/subselect.c | 35 ++++++++++ src/test/regress/expected/subselect.out | 89 +++++++++++++++++++++++++ src/test/regress/sql/subselect.sql | 26 ++++++++ 3 files changed, 150 insertions(+) diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index cac2aaca2e8..e8aa31254d6 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,34 @@ 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 or if rows are locked. + * Only sortClause is cleared, the sort expressions stay in the targetlist and + * are still evaluated. + */ +static void +remove_scalar_sublink_ordering(Query *query) +{ + if (query->sortClause == NIL) + return; + + if (query->commandType != CMD_SELECT || + query->setOperations || + query->hasModifyingCTE || + query->hasDistinctOn || + query->limitCount || + query->limitOffset || + query->rowMarks) + 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..f50c01b2fdd 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 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 +explain (verbose, costs off) +select (select a from scalar_sort order by random()); + QUERY PLAN +------------------------------------------- + Result + Output: $0 + InitPlan 1 (returns $0) + -> Seq Scan on public.scalar_sort + Output: scalar_sort.a, random() +(5 rows) + +-- 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 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..a25c44bed7e 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 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)); +explain (verbose, costs off) +select (select a from scalar_sort order by random()); +-- 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 where a > 4 order by a for update); +drop table scalar_sort; -- 2.53.0