From 97c45a08e041ec2efb1659638ddb6255a6698bfc Mon Sep 17 00:00:00 2001 From: Iliia Khaprov Date: Tue, 20 Oct 2026 21:16:52 +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 7783c7107e0..4158520db53 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -93,6 +93,7 @@ static void inline_cte(PlannerInfo *root, CommonTableExpr *cte); static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); 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); @@ -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 @@ -1657,6 +1664,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 fa4eab3fdf3..66b5ec2ffa1 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -3395,3 +3395,92 @@ SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3))); Seq Scan on onek t (1 row) +-- +-- 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 + -> 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 + -> 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 1).col1 + InitPlan 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 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 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 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 222c7f0bb14..c4dbdceb7bf 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1507,3 +1507,29 @@ SELECT * FROM onek t1, lateral (SELECT * FROM onek t2 WHERE t2.ten IN (values (t -- VtA causes the whole expression to be evaluated as a constant EXPLAIN (COSTS OFF) SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3))); + +-- +-- 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