From babe90a940539e42b473e6e0ffa0e29149d33707 Mon Sep 17 00:00:00 2001 From: Iliia Khaprov Date: Sun, 18 Oct 2026 21:03:37 +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 1b66906ad11..d1ba193299a 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -95,6 +95,7 @@ static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); static bool sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink); static int add_antijoin_rte(Query *parse); 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); @@ -193,6 +194,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 @@ -1833,6 +1840,34 @@ add_antijoin_rte(Query *parse) return list_length(parse->rtable); } +/* + * 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 6379bd6ca3c..ac009df4150 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -4075,3 +4075,92 @@ WHERE COALESCE(t1.id, -1) NOT IN (0 rows) ROLLBACK; +-- +-- 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 expr_1 + -> 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 expr_1 + -> 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: (InitPlan expr_1).col1 + InitPlan expr_1 + -> 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 expr_1 + -> 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 expr_1 + -> 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 expr_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 c6cd275367d..9b7e1bb1c2a 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1790,3 +1790,29 @@ WHERE COALESCE(t1.id, -1) NOT IN (SELECT t1.val FROM not_null_tab t2 WHERE t2.val IS NOT NULL); ROLLBACK; + +-- +-- 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