Hi,

The attached patch set adds an optimisation that intensifies the usage of the sorted NestLoop trick to avoid sorts higher in the query tree.

This employs the idea that it is not so rare a case when an ORDER BY query (or GROUP BY or DISTINCT operator) can benefit from a pre-sorted path even if the outer subtree doesn't have an index on the sort expression. It is especially beneficial when a LIMIT clause and an OUTER JOIN are present - combined with tuple-bound propagation through the outer side of such a join, LIMIT can reach the Sort below the join and turn it into a bounded top-N heapsort.

Let me explain the problem with a typical schema of a sales database. It has a 'products' table and description tables for each category (see the SQL script in the attachment). A front page provides the top-N products based on popularity or other criteria. The query looks like multiple OUTER JOINs of the main 'products' table with tables for each category. At the top, non-null fields are combined into a JSON descriptor -- something like the following:

  SELECT p.id, p.name, p.category, p.popularity,
    json_strip_nulls(json_build_object(
      'warranty', e.warranty_months,
      'voltage',  e.voltage,
      'size',     c.size,
      'color',    c.color,
      'expiry',   f.expiry_days,
      'organic',  f.organic
    )) AS properties
  FROM products p
    LEFT JOIN electronics_props e ON e.product_id = p.id
    LEFT JOIN clothing_props    c ON c.product_id = p.id
    LEFT JOIN food_props        f ON f.product_id = p.id
  ORDER BY p.popularity DESC
  LIMIT 10;

AFAICS, this pattern is common in sales systems, CRM and ERP workloads - anywhere you query "top-N items with details" over a normalised schema with LEFT JOINs to optional property tables.

There is usually no need to join all the tables and sort afterwards; pre-sorting the outer relation might be more beneficial. This subject has already been discussed [1,2], but here we try to solve the case in general. There was an argument that the user might just add an index. But sometimes it 1) might not be reasonable (like in the example) and 2) is impossible if the data source is not a plain table but a FunctionScan, SubqueryScan, etc. The most important source I consider here is an Append over foreign tables.

There is a sketchy patch set in the attachment that outlines the solution. It consists of the following parts:

1. Executor part, the ExecSetTupleBound extension: allows recursion into the outer side of a join to bound the underlying subtree. It is crucial because we lack a proper hook in this subsystem, which makes the extensible implementation of such a feature very complex. 2. Planner part, modification of the joinpath.c::match_unsorted_outer. In the case of NestLoop, where its outer side is mentioned in the query_pathkeys prefix, and no sort on this prefix yet exists, Postgres tries to build such a sorted path and proposes a JOIN with sorted output.
3. Adjustment of regression tests.

Open issues
1. Scope: the optimisation targets a specific pattern (ORDER BY + LIMIT over joins, and NestLoop). One may argue that it is too narrow to justify core code. That's true, but I see that such a 'Sort pushdown' might enable the optimiser to create more effective MergeJoins, aggregates, etc. You may check the regression tests diff for insights. 2. Cost estimation: the Sort node is costed with an estimated tuple bound from LIMIT in case of OUTER JOIN, which can, in some cases, make the pre-sorted path look cheaper than it actually is. I don't see an actual problem here, since it's about in-memory sorting, which shouldn't be too expensive anyway.

Feedback and review welcome.

References
[1] "Possible optimisation: push down SORT and LIMIT nodes", 2018
https://www.postgresql.org/message-id/E9FA92C2921F31408041863B74EE4C2001AEF33492@CCPMAILDAG03.cantab.local

[2] "Wasteful nested loop join when there is `limit` in the query", 2025
https://www.postgresql.org/message-id/CAAdwFAwm6HwXM_cuPWZBxrxX4E7pBdVg=kcvdsp6q9ume3h...@mail.gmail.com

--
regards, Andrei Lepikhov,
pgEdge
From 361a1878dfaded538e6148ddabcef7e976ce42bf Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <[email protected]>
Date: Thu, 2 Apr 2026 15:35:16 +0200
Subject: [PATCH v0 1/5] Extend the ExecSetTupleBound to LEFT JOIN outer side

---
 src/backend/executor/execProcnode.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/backend/executor/execProcnode.c 
b/src/backend/executor/execProcnode.c
index d35976925ae..c3152f3e3ca 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -978,6 +978,29 @@ ExecSetTupleBound(int64 tuples_needed, PlanState 
*child_node)
 
                ExecSetTupleBound(tuples_needed, outerPlanState(child_node));
        }
+       else if (IsA(child_node, NestLoopState))
+       {
+               /*
+                * For a NestLoop, the bound can be propagated to the side that
+                * drives the output row count:
+                *
+                * - LEFT JOIN: every outer row produces at least one output row
+                *   (with NULLs if no match), so bound the outer side.
+                * - RIGHT JOIN: every inner row produces at least one output 
row,
+                *   so bound the inner side.
+                *
+                * We do not propagate for INNER, ANTI, or FULL joins, since the
+                * number of output rows can be less than the driving side's 
count
+                * (INNER/ANTI may discard rows, FULL may expand both sides).
+                */
+               NestLoopState  *nlstate = (NestLoopState *) child_node;
+               JoinType                jointype = nlstate->js.jointype;
+
+               if (jointype == JOIN_LEFT)
+                       ExecSetTupleBound(tuples_needed, 
outerPlanState(child_node));
+               else if (jointype == JOIN_RIGHT)
+                       ExecSetTupleBound(tuples_needed, 
innerPlanState(child_node));
+       }
 
        /*
         * In principle we could descend through any plan node type that is
-- 
2.53.0

From 32f23607b65b2e0292113a9291c1ca17e6818bc8 Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <[email protected]>
Date: Thu, 2 Apr 2026 18:38:49 +0200
Subject: [PATCH v0 2/5] Try pre-sorted outer path for a JOIN.

Check whether the outer rel can be sorted on a useful prefix of
query_pathkeys, and if no such path already exists, build one with
create_sort_path() and submit it to try_nestloop_path().

For LEFT JOIN, every outer row produces at least one output row, so
root->limit_tuples translates directly
to a bound on the outer sort and is passed to cost_sort() to activate
the bounded heap-sort cost model.  For INNER, SEMI, and ANTI joins,
outer rows may be discarded by the join condition, so -1.0 is used and a
conservative full-sort cost estimate is produced instead.

The get_cheapest_path_for_pathkeys() guard ensures no redundant work is
done for base relations whose sorted path already exist.
---
 src/backend/optimizer/path/joinpath.c | 110 ++++++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/src/backend/optimizer/path/joinpath.c 
b/src/backend/optimizer/path/joinpath.c
index 713283a73aa..7d9c2b9c7cb 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -2004,6 +2004,116 @@ match_unsorted_outer(PlannerInfo *root,
                                                                 false);
        }
 
+       /*
+        * If the query has ORDER BY (possibly with LIMIT) and nestloop is
+        * applicable, consider pre-sorting the outer relation on the query
+        * pathkeys.  The pre-sorted scan logic in set_plain_rel_pathlist()
+        * handles plain base relations; here we extend that to join rels as
+        * outer sides, covering ORDER BY keys that span multiple tables.
+        * For base relations the sorted path is already in outerrel->pathlist
+        * and was tried in the loop above; the get_cheapest_path_for_pathkeys
+        * guard below prevents redundant work in that case.
+        *
+        * When root->limit_tuples is set, create_sort_path() uses the bounded
+        * heap-sort cost model (N*log2(2K) instead of N*log2(N)), giving an
+        * accurate startup-cost estimate that feeds into fractional path
+        * comparison correctly.
+        */
+       if (nestjoinOK && root->query_pathkeys != NIL)
+       {
+               List       *useful_pathkeys = NIL;
+               ListCell   *lc;
+
+               foreach(lc, root->query_pathkeys)
+               {
+                       PathKey                    *pathkey = (PathKey *) 
lfirst(lc);
+                       EquivalenceClass   *ec = pathkey->pk_eclass;
+
+                       if (!relation_can_be_sorted_early(root, outerrel, ec, 
false))
+                               break;
+
+                       useful_pathkeys = lappend(useful_pathkeys, pathkey);
+               }
+
+               /*
+                * Only proceed if we found useful pathkeys and the outer rel 
does not
+                * already have a path satisfying them — if it does, the 
foreach loop
+                * above already considered it.
+                */
+               if (useful_pathkeys != NIL &&
+                       get_cheapest_path_for_pathkeys(outerrel->pathlist,
+                                                                               
   useful_pathkeys,
+                                                                               
   outerrel->lateral_relids,
+                                                                               
   TOTAL_COST, false) == NULL)
+               {
+                       Path       *outerpath =
+                                       
get_cheapest_path_for_pathkeys(outerrel->pathlist, NIL,
+                                                                               
                   outerrel->lateral_relids,
+                                                                               
                   TOTAL_COST, false);
+
+                       if (outerpath != NULL && !PATH_PARAM_BY_REL(outerpath, 
innerrel))
+                       {
+                               Path       *sorted_outer;
+                               List       *merge_pathkeys;
+
+                               /*
+                                * For LEFT JOIN every outer row produces at 
least one output
+                                * row (NULL-extended if unmatched), so the 
LIMIT bound on join
+                                * output safely bounds the outer side — pass 
it for an
+                                * accurate top-N heap-sort cost estimate.  For 
INNER, SEMI,
+                                * and ANTI, outer rows can be discarded by the 
join condition,
+                                * so we conservatively model a full sort.
+                                */
+                               sorted_outer = (Path *)
+                                       create_sort_path(root, outerrel, 
outerpath,
+                                                                        
useful_pathkeys,
+                                                                        
(jointype == JOIN_LEFT) ?
+                                                                        
root->limit_tuples : -1.0);
+
+                               merge_pathkeys = build_join_pathkeys(root, 
joinrel, jointype,
+                                                                               
                         sorted_outer->pathkeys);
+
+                               /*
+                                * cheapest_parameterized_paths always includes 
the
+                                * cheapest-total unparameterized path, so no 
need to
+                                * try inner_cheapest_total separately.
+                                */
+                               foreach(lc, 
innerrel->cheapest_parameterized_paths)
+                               {
+                                       Path       *innerpath = (Path *) 
lfirst(lc);
+                                       Path       *mpath;
+
+                                       try_nestloop_path(root, joinrel,
+                                                                         
sorted_outer, innerpath,
+                                                                         
merge_pathkeys,
+                                                                         
jointype,
+                                                                         
PGS_NESTLOOP_PLAIN,
+                                                                         
extra);
+
+                                       mpath = get_memoize_path(root, 
innerrel, outerrel,
+                                                                               
         innerpath, sorted_outer, jointype,
+                                                                               
         extra);
+                                       if (mpath != NULL)
+                                               try_nestloop_path(root, joinrel,
+                                                                               
  sorted_outer, mpath,
+                                                                               
  merge_pathkeys,
+                                                                               
  jointype,
+                                                                               
  PGS_NESTLOOP_MEMOIZE,
+                                                                               
  extra);
+                               }
+
+                               /* Also consider materialized form of the 
cheapest inner path */
+                               if (matpath != NULL)
+                                       try_nestloop_path(root, joinrel,
+                                                                         
sorted_outer, matpath,
+                                                                         
merge_pathkeys,
+                                                                         
jointype,
+                                                                         
PGS_NESTLOOP_MATERIALIZE,
+                                                                         
extra);
+                       }
+               }
+       }
+
        /*
         * Consider partial nestloop and mergejoin plan if outerrel has any
         * partial path and the joinrel is parallel-safe.  However, we can't
-- 
2.53.0

From 4d43f8e2d5d22c4e72784065e8a356d17eb85dd2 Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <[email protected]>
Date: Thu, 2 Apr 2026 19:26:34 +0200
Subject: [PATCH v0 3/5] Adjust query plans changed

---
 .../postgres_fdw/expected/postgres_fdw.out    |  40 +++---
 .../regress/expected/collate.icu.utf8.out     |  22 +--
 src/test/regress/expected/create_am.out       |   4 +-
 src/test/regress/expected/eager_aggregate.out | 120 ++++++----------
 .../regress/expected/incremental_sort.out     |  52 ++++---
 src/test/regress/expected/join.out            |  42 +++---
 src/test/regress/expected/oid8.out            |  28 ++--
 src/test/regress/expected/partition_join.out  | 131 ++++++++++--------
 src/test/regress/expected/pg_lsn.out          |  28 ++--
 src/test/regress/expected/tablesample.out     |   8 +-
 src/test/regress/sql/create_am.sql            |   4 +-
 11 files changed, 228 insertions(+), 251 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out 
b/contrib/postgres_fdw/expected/postgres_fdw.out
index ac34a1acacb..a6f2bb13e24 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2242,21 +2242,21 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON 
(t1.c8 = t2.c8) ORDER BY t1.
 -------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1
-   ->  Sort
+   ->  Incremental Sort
          Output: t1.c1, t2.c1
          Sort Key: t1.c1, t2.c1
-         ->  Merge Left Join
+         Presorted Key: t1.c1
+         ->  Nested Loop Left Join
                Output: t1.c1, t2.c1
-               Merge Cond: (t1.c8 = t2.c8)
+               Join Filter: (t1.c8 = t2.c8)
                ->  Sort
                      Output: t1.c1, t1.c8
-                     Sort Key: t1.c8
+                     Sort Key: t1.c1
                      ->  Foreign Scan on public.ft1 t1
                            Output: t1.c1, t1.c8
                            Remote SQL: SELECT "C 1", c8 FROM "S 1"."T 1"
-               ->  Sort
+               ->  Materialize
                      Output: t2.c1, t2.c8
-                     Sort Key: t2.c8
                      ->  Foreign Scan on public.ft2 t2
                            Output: t2.c1, t2.c8
                            Remote SQL: SELECT "C 1", c8 FROM "S 1"."T 1"
@@ -3836,24 +3836,24 @@ select sum(c2) * (random() <= 1)::int as sum from ft1 
order by 1;
 set enable_hashagg to false;
 explain (verbose, costs off)
 select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum 
from ft2 t2 group by t2.c1) qry where t1.c2 * 2 = qry.sum and t1.c2 < 3 and 
t1."C 1" < 100 order by 1;
-                                              QUERY PLAN                       
                       
-------------------------------------------------------------------------------------------------------
- Sort
+                                           QUERY PLAN                          
                 
+------------------------------------------------------------------------------------------------
+ Nested Loop
    Output: t1.c2, qry.sum
-   Sort Key: t1.c2
-   ->  Nested Loop
-         Output: t1.c2, qry.sum
+   ->  Sort
+         Output: t1.c2, t1."C 1"
+         Sort Key: t1.c2
          ->  Index Scan using t1_pkey on "S 1"."T 1" t1
-               Output: t1."C 1", t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, 
t1.c8
+               Output: t1.c2, t1."C 1"
                Index Cond: (t1."C 1" < 100)
                Filter: (t1.c2 < 3)
-         ->  Subquery Scan on qry
-               Output: qry.sum, t2.c1
-               Filter: ((t1.c2 * 2) = qry.sum)
-               ->  Foreign Scan
-                     Output: (sum((t2.c1 + t1."C 1"))), t2.c1
-                     Relations: Aggregate on (public.ft2 t2)
-                     Remote SQL: SELECT sum(("C 1" + $1::integer)), "C 1" FROM 
"S 1"."T 1" GROUP BY 2
+   ->  Subquery Scan on qry
+         Output: qry.sum, t2.c1
+         Filter: ((t1.c2 * 2) = qry.sum)
+         ->  Foreign Scan
+               Output: (sum((t2.c1 + t1."C 1"))), t2.c1
+               Relations: Aggregate on (public.ft2 t2)
+               Remote SQL: SELECT sum(("C 1" + $1::integer)), "C 1" FROM "S 
1"."T 1" GROUP BY 2
 (16 rows)
 
 select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum 
from ft2 t2 group by t2.c1) qry where t1.c2 * 2 = qry.sum and t1.c2 < 3 and 
t1."C 1" < 100 order by 1;
diff --git a/src/test/regress/expected/collate.icu.utf8.out 
b/src/test/regress/expected/collate.icu.utf8.out
index d170e7da066..405bd36fed8 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -2623,26 +2623,30 @@ INSERT INTO pagg_tab6 (b, c) SELECT substr('cdCD', (i % 
4) + 1 , 1), substr('cdC
 ANALYZE pagg_tab6;
 EXPLAIN (COSTS OFF)
 SELECT t1.c, count(t2.c) FROM pagg_tab5 t1 JOIN pagg_tab6 t2 ON t1.c = t2.c 
AND t1.c = t2.b GROUP BY 1 ORDER BY t1.c COLLATE "C";
-                      QUERY PLAN                       
--------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Sort
    Sort Key: t1.c COLLATE "C"
    ->  Append
-         ->  HashAggregate
+         ->  GroupAggregate
                Group Key: t1.c
                ->  Nested Loop
                      Join Filter: (t1.c = t2.c)
-                     ->  Seq Scan on pagg_tab6_p1 t2
-                           Filter: (c = b)
+                     ->  Sort
+                           Sort Key: t2.c COLLATE case_insensitive
+                           ->  Seq Scan on pagg_tab6_p1 t2
+                                 Filter: (c = b)
                      ->  Seq Scan on pagg_tab5_p1 t1
-         ->  HashAggregate
+         ->  GroupAggregate
                Group Key: t1_1.c
                ->  Nested Loop
                      Join Filter: (t1_1.c = t2_1.c)
-                     ->  Seq Scan on pagg_tab6_p2 t2_1
-                           Filter: (c = b)
+                     ->  Sort
+                           Sort Key: t2_1.c COLLATE case_insensitive
+                           ->  Seq Scan on pagg_tab6_p2 t2_1
+                                 Filter: (c = b)
                      ->  Seq Scan on pagg_tab5_p2 t1_1
-(17 rows)
+(21 rows)
 
 SELECT t1.c, count(t2.c) FROM pagg_tab5 t1 JOIN pagg_tab6 t2 ON t1.c = t2.c 
AND t1.c = t2.b GROUP BY 1 ORDER BY t1.c COLLATE "C";
  c | count 
diff --git a/src/test/regress/expected/create_am.out 
b/src/test/regress/expected/create_am.out
index c1a95157251..05efb03272c 100644
--- a/src/test/regress/expected/create_am.out
+++ b/src/test/regress/expected/create_am.out
@@ -201,9 +201,9 @@ SELECT
     pc.relkind,
     pa.amname,
     CASE WHEN relkind = 't' THEN
-        (SELECT 'toast for ' || relname::regclass FROM pg_class pcm WHERE 
pcm.reltoastrelid = pc.oid)
+        (SELECT 'toast for ' || pcm.oid::regclass FROM pg_class pcm WHERE 
pcm.reltoastrelid = pc.oid)
     ELSE
-        relname::regclass::text
+        pc.oid::regclass::text
     END COLLATE "C" AS relname
 FROM pg_class AS pc,
     pg_am AS pa
diff --git a/src/test/regress/expected/eager_aggregate.out 
b/src/test/regress/expected/eager_aggregate.out
index 5ac966186f7..58fbace2769 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -808,92 +808,52 @@ GROUP BY t3.y ORDER BY t3.y;
  Finalize GroupAggregate
    Output: t3.y, sum((t2.y + t3.y))
    Group Key: t3.y
-   ->  Sort
+   ->  Nested Loop
          Output: t3.y, (PARTIAL sum((t2.y + t3.y)))
-         Sort Key: t3.y
-         ->  Append
-               ->  Hash Join
-                     Output: t3.y, (PARTIAL sum((t2.y + t3.y)))
-                     Hash Cond: (t2.x = t1.x)
-                     ->  Partial GroupAggregate
-                           Output: t2.x, t3.y, t3.x, PARTIAL sum((t2.y + t3.y))
-                           Group Key: t2.x, t3.y, t3.x
-                           ->  Incremental Sort
+         Join Filter: (t1.x = t2.x)
+         ->  Sort
+               Output: t2.x, t3.y, t3.x, (PARTIAL sum((t2.y + t3.y)))
+               Sort Key: t3.y
+               ->  Partial GroupAggregate
+                     Output: t2.x, t3.y, t3.x, PARTIAL sum((t2.y + t3.y))
+                     Group Key: t2.x, t3.y, t3.x
+                     ->  Incremental Sort
+                           Output: t2.y, t2.x, t3.y, t3.x
+                           Sort Key: t2.x, t3.y
+                           Presorted Key: t2.x
+                           ->  Merge Join
                                  Output: t2.y, t2.x, t3.y, t3.x
-                                 Sort Key: t2.x, t3.y
-                                 Presorted Key: t2.x
-                                 ->  Merge Join
-                                       Output: t2.y, t2.x, t3.y, t3.x
-                                       Merge Cond: (t2.x = t3.x)
-                                       ->  Sort
-                                             Output: t2.y, t2.x
-                                             Sort Key: t2.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p1 t2
-                                                   Output: t2.y, t2.x
-                                       ->  Sort
-                                             Output: t3.y, t3.x
-                                             Sort Key: t3.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p1 t3
-                                                   Output: t3.y, t3.x
-                     ->  Hash
-                           Output: t1.x
-                           ->  Seq Scan on public.eager_agg_tab1_p1 t1
-                                 Output: t1.x
-               ->  Hash Join
-                     Output: t3_1.y, (PARTIAL sum((t2_1.y + t3_1.y)))
-                     Hash Cond: (t2_1.x = t1_1.x)
-                     ->  Partial GroupAggregate
-                           Output: t2_1.x, t3_1.y, t3_1.x, PARTIAL sum((t2_1.y 
+ t3_1.y))
-                           Group Key: t2_1.x, t3_1.y, t3_1.x
-                           ->  Incremental Sort
-                                 Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x
-                                 Sort Key: t2_1.x, t3_1.y
-                                 Presorted Key: t2_1.x
-                                 ->  Merge Join
-                                       Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x
-                                       Merge Cond: (t2_1.x = t3_1.x)
-                                       ->  Sort
-                                             Output: t2_1.y, t2_1.x
-                                             Sort Key: t2_1.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p2 t2_1
+                                 Merge Cond: (t2.x = t3.x)
+                                 ->  Sort
+                                       Output: t2.y, t2.x
+                                       Sort Key: t2.x
+                                       ->  Append
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p1 t2_1
                                                    Output: t2_1.y, t2_1.x
-                                       ->  Sort
-                                             Output: t3_1.y, t3_1.x
-                                             Sort Key: t3_1.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p2 t3_1
-                                                   Output: t3_1.y, t3_1.x
-                     ->  Hash
-                           Output: t1_1.x
-                           ->  Seq Scan on public.eager_agg_tab1_p2 t1_1
-                                 Output: t1_1.x
-               ->  Hash Join
-                     Output: t3_2.y, (PARTIAL sum((t2_2.y + t3_2.y)))
-                     Hash Cond: (t2_2.x = t1_2.x)
-                     ->  Partial GroupAggregate
-                           Output: t2_2.x, t3_2.y, t3_2.x, PARTIAL sum((t2_2.y 
+ t3_2.y))
-                           Group Key: t2_2.x, t3_2.y, t3_2.x
-                           ->  Incremental Sort
-                                 Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x
-                                 Sort Key: t2_2.x, t3_2.y
-                                 Presorted Key: t2_2.x
-                                 ->  Merge Join
-                                       Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x
-                                       Merge Cond: (t2_2.x = t3_2.x)
-                                       ->  Sort
-                                             Output: t2_2.y, t2_2.x
-                                             Sort Key: t2_2.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p3 t2_2
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p2 t2_2
                                                    Output: t2_2.y, t2_2.x
-                                       ->  Sort
-                                             Output: t3_2.y, t3_2.x
-                                             Sort Key: t3_2.x
-                                             ->  Seq Scan on 
public.eager_agg_tab1_p3 t3_2
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p3 t2_3
+                                                   Output: t2_3.y, t2_3.x
+                                 ->  Sort
+                                       Output: t3.y, t3.x
+                                       Sort Key: t3.x
+                                       ->  Append
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p1 t3_1
+                                                   Output: t3_1.y, t3_1.x
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p2 t3_2
                                                    Output: t3_2.y, t3_2.x
-                     ->  Hash
+                                             ->  Seq Scan on 
public.eager_agg_tab1_p3 t3_3
+                                                   Output: t3_3.y, t3_3.x
+         ->  Materialize
+               Output: t1.x
+               ->  Append
+                     ->  Seq Scan on public.eager_agg_tab1_p1 t1_1
+                           Output: t1_1.x
+                     ->  Seq Scan on public.eager_agg_tab1_p2 t1_2
                            Output: t1_2.x
-                           ->  Seq Scan on public.eager_agg_tab1_p3 t1_2
-                                 Output: t1_2.x
-(88 rows)
+                     ->  Seq Scan on public.eager_agg_tab1_p3 t1_3
+                           Output: t1_3.x
+(48 rows)
 
 SELECT t3.y, sum(t2.y + t3.y)
   FROM eager_agg_tab1 t1
diff --git a/src/test/regress/expected/incremental_sort.out 
b/src/test/regress/expected/incremental_sort.out
index 1e6e020fea8..0eaa8d75ba2 100644
--- a/src/test/regress/expected/incremental_sort.out
+++ b/src/test/regress/expected/incremental_sort.out
@@ -1610,16 +1610,16 @@ from tenk1 t, generate_series(1, 1000);
                                    QUERY PLAN                                  
  
 
---------------------------------------------------------------------------------
  Unique
-   ->  Sort
-         Sort Key: t.unique1, ((SubPlan expr_1))
-         ->  Gather
-               Workers Planned: 2
-               ->  Nested Loop
+   ->  Nested Loop
+         ->  Sort
+               Sort Key: t.unique1, ((SubPlan expr_1))
+               ->  Gather
+                     Workers Planned: 2
                      ->  Parallel Index Only Scan using tenk1_unique1 on tenk1 
t
-                     ->  Function Scan on generate_series
-               SubPlan expr_1
-                 ->  Index Only Scan using tenk1_unique1 on tenk1
-                       Index Cond: (unique1 = t.unique1)
+                     SubPlan expr_1
+                       ->  Index Only Scan using tenk1_unique1 on tenk1
+                             Index Cond: (unique1 = t.unique1)
+         ->  Function Scan on generate_series
 (11 rows)
 
 explain (costs off) select
@@ -1629,16 +1629,16 @@ from tenk1 t, generate_series(1, 1000)
 order by 1, 2;
                                 QUERY PLAN                                 
 ---------------------------------------------------------------------------
- Sort
-   Sort Key: t.unique1, ((SubPlan expr_1))
-   ->  Gather
-         Workers Planned: 2
-         ->  Nested Loop
+ Nested Loop
+   ->  Sort
+         Sort Key: t.unique1, ((SubPlan expr_1))
+         ->  Gather
+               Workers Planned: 2
                ->  Parallel Index Only Scan using tenk1_unique1 on tenk1 t
-               ->  Function Scan on generate_series
-         SubPlan expr_1
-           ->  Index Only Scan using tenk1_unique1 on tenk1
-                 Index Cond: (unique1 = t.unique1)
+               SubPlan expr_1
+                 ->  Index Only Scan using tenk1_unique1 on tenk1
+                       Index Cond: (unique1 = t.unique1)
+   ->  Function Scan on generate_series
 (10 rows)
 
 -- Parallel sort but with expression not available until the upper rel.
@@ -1708,21 +1708,19 @@ explain (costs off)
 select * from
   (select * from tenk1 order by four) t1 join tenk1 t2 on t1.four = t2.four 
and t1.two = t2.two
 order by t1.four, t1.two limit 1;
-                              QUERY PLAN                               
------------------------------------------------------------------------
+                               QUERY PLAN                               
+------------------------------------------------------------------------
  Limit
-   ->  Merge Join
-         Merge Cond: ((tenk1.four = t2.four) AND (tenk1.two = t2.two))
-         ->  Incremental Sort
+   ->  Nested Loop
+         Join Filter: ((tenk1.four = t2.four) AND (tenk1.two = t2.two))
+         ->  Sort
                Sort Key: tenk1.four, tenk1.two
-               Presorted Key: tenk1.four
                ->  Sort
                      Sort Key: tenk1.four
                      ->  Seq Scan on tenk1
-         ->  Sort
-               Sort Key: t2.four, t2.two
+         ->  Materialize
                ->  Seq Scan on tenk1 t2
-(12 rows)
+(10 rows)
 
 --
 -- Test incremental sort for Append/MergeAppend
diff --git a/src/test/regress/expected/join.out 
b/src/test/regress/expected/join.out
index 84872c6f04e..753ef99ae6b 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -9400,29 +9400,29 @@ select t1.b, ss.phv from join_ut1 t1 left join lateral
               (select t2.a as t2a, t3.a t3a, least(t1.a, t2.a, t3.a) phv
                                          from join_pt1 t2 join join_ut1 t3 on 
t2.a = t3.b) ss
               on t1.a = ss.t2a order by t1.a;
-                             QUERY PLAN                             
---------------------------------------------------------------------
- Sort
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Nested Loop Left Join
    Output: t1.b, (LEAST(t1.a, t2.a, t3.a)), t1.a
-   Sort Key: t1.a
-   ->  Nested Loop Left Join
-         Output: t1.b, (LEAST(t1.a, t2.a, t3.a)), t1.a
+   ->  Sort
+         Output: t1.b, t1.a
+         Sort Key: t1.a
          ->  Seq Scan on public.join_ut1 t1
-               Output: t1.a, t1.b, t1.c
-         ->  Hash Join
-               Output: t2.a, LEAST(t1.a, t2.a, t3.a)
-               Hash Cond: (t3.b = t2.a)
-               ->  Seq Scan on public.join_ut1 t3
-                     Output: t3.a, t3.b, t3.c
-               ->  Hash
-                     Output: t2.a
-                     ->  Append
-                           ->  Seq Scan on public.join_pt1p1p1 t2_1
-                                 Output: t2_1.a
-                                 Filter: (t1.a = t2_1.a)
-                           ->  Seq Scan on public.join_pt1p2 t2_2
-                                 Output: t2_2.a
-                                 Filter: (t1.a = t2_2.a)
+               Output: t1.b, t1.a
+   ->  Hash Join
+         Output: t2.a, LEAST(t1.a, t2.a, t3.a)
+         Hash Cond: (t3.b = t2.a)
+         ->  Seq Scan on public.join_ut1 t3
+               Output: t3.a, t3.b, t3.c
+         ->  Hash
+               Output: t2.a
+               ->  Append
+                     ->  Seq Scan on public.join_pt1p1p1 t2_1
+                           Output: t2_1.a
+                           Filter: (t1.a = t2_1.a)
+                     ->  Seq Scan on public.join_pt1p2 t2_2
+                           Output: t2_2.a
+                           Filter: (t1.a = t2_2.a)
 (21 rows)
 
 select t1.b, ss.phv from join_ut1 t1 left join lateral
diff --git a/src/test/regress/expected/oid8.out 
b/src/test/regress/expected/oid8.out
index 2e114f1ce70..7bfa8a9d826 100644
--- a/src/test/regress/expected/oid8.out
+++ b/src/test/regress/expected/oid8.out
@@ -217,21 +217,19 @@ SELECT DISTINCT (i || '000000000000' || j)::oid8 f
        generate_series(1, 5) k
   WHERE i <= 10 AND j > 0 AND j <= 10
   ORDER BY f;
-                                    QUERY PLAN                                 
    
------------------------------------------------------------------------------------
- Sort
-   Sort Key: (((((i.i)::text || '000000000000'::text) || (j.j)::text))::oid8)
-   ->  HashAggregate
-         Group Key: ((((i.i)::text || '000000000000'::text) || 
(j.j)::text))::oid8
-         ->  Nested Loop
-               ->  Function Scan on generate_series k
-               ->  Materialize
-                     ->  Nested Loop
-                           ->  Function Scan on generate_series j
-                                 Filter: ((j > 0) AND (j <= 10))
-                           ->  Function Scan on generate_series i
-                                 Filter: (i <= 10)
-(12 rows)
+                                        QUERY PLAN                             
           
+------------------------------------------------------------------------------------------
+ Unique
+   ->  Nested Loop
+         ->  Sort
+               Sort Key: (((((i.i)::text || '000000000000'::text) || 
(j.j)::text))::oid8)
+               ->  Nested Loop
+                     ->  Function Scan on generate_series j
+                           Filter: ((j > 0) AND (j <= 10))
+                     ->  Function Scan on generate_series i
+                           Filter: (i <= 10)
+         ->  Function Scan on generate_series k
+(10 rows)
 
 SELECT DISTINCT (i || '000000000000' || j)::oid8 f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/partition_join.out 
b/src/test/regress/expected/partition_join.out
index 38643d41fd7..ccae52e1470 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -2102,53 +2102,60 @@ EXPLAIN (COSTS OFF)
 SELECT * FROM prt1_l t1 LEFT JOIN LATERAL
                          (SELECT t2.a AS t2a, t2.c AS t2c, t2.b AS t2b, t3.b 
AS t3b, least(t1.a,t2.a,t3.b) FROM prt1_l t2 JOIN prt2_l t3 ON (t2.a = t3.b AND 
t2.c = t3.c)) ss
                          ON t1.a = ss.t2a AND t1.c = ss.t2c WHERE t1.b = 0 
ORDER BY t1.a;
-                                          QUERY PLAN                           
                
------------------------------------------------------------------------------------------------
- Sort
+                                       QUERY PLAN                              
          
+-----------------------------------------------------------------------------------------
+ Merge Append
    Sort Key: t1.a
-   ->  Append
-         ->  Nested Loop Left Join
+   ->  Nested Loop Left Join
+         ->  Sort
+               Sort Key: t1_1.a
                ->  Seq Scan on prt1_l_p1 t1_1
                      Filter: (b = 0)
-               ->  Hash Join
-                     Hash Cond: ((t3_1.b = t2_1.a) AND ((t3_1.c)::text = 
(t2_1.c)::text))
-                     ->  Seq Scan on prt2_l_p1 t3_1
-                     ->  Hash
-                           ->  Seq Scan on prt1_l_p1 t2_1
-                                 Filter: ((t1_1.a = a) AND ((t1_1.c)::text = 
(c)::text))
-         ->  Nested Loop Left Join
+         ->  Hash Join
+               Hash Cond: ((t3_1.b = t2_1.a) AND ((t3_1.c)::text = 
(t2_1.c)::text))
+               ->  Seq Scan on prt2_l_p1 t3_1
+               ->  Hash
+                     ->  Seq Scan on prt1_l_p1 t2_1
+                           Filter: ((t1_1.a = a) AND ((t1_1.c)::text = 
(c)::text))
+   ->  Nested Loop Left Join
+         ->  Sort
+               Sort Key: t1_2.a
                ->  Seq Scan on prt1_l_p2_p1 t1_2
                      Filter: (b = 0)
-               ->  Hash Join
-                     Hash Cond: ((t3_2.b = t2_2.a) AND ((t3_2.c)::text = 
(t2_2.c)::text))
-                     ->  Seq Scan on prt2_l_p2_p1 t3_2
-                     ->  Hash
-                           ->  Seq Scan on prt1_l_p2_p1 t2_2
-                                 Filter: ((t1_2.a = a) AND ((t1_2.c)::text = 
(c)::text))
-         ->  Nested Loop Left Join
+         ->  Hash Join
+               Hash Cond: ((t3_2.b = t2_2.a) AND ((t3_2.c)::text = 
(t2_2.c)::text))
+               ->  Seq Scan on prt2_l_p2_p1 t3_2
+               ->  Hash
+                     ->  Seq Scan on prt1_l_p2_p1 t2_2
+                           Filter: ((t1_2.a = a) AND ((t1_2.c)::text = 
(c)::text))
+   ->  Nested Loop Left Join
+         ->  Sort
+               Sort Key: t1_3.a
                ->  Seq Scan on prt1_l_p2_p2 t1_3
                      Filter: (b = 0)
-               ->  Hash Join
-                     Hash Cond: ((t3_3.b = t2_3.a) AND ((t3_3.c)::text = 
(t2_3.c)::text))
-                     ->  Seq Scan on prt2_l_p2_p2 t3_3
-                     ->  Hash
-                           ->  Seq Scan on prt1_l_p2_p2 t2_3
-                                 Filter: ((t1_3.a = a) AND ((t1_3.c)::text = 
(c)::text))
-         ->  Nested Loop Left Join
+         ->  Hash Join
+               Hash Cond: ((t3_3.b = t2_3.a) AND ((t3_3.c)::text = 
(t2_3.c)::text))
+               ->  Seq Scan on prt2_l_p2_p2 t3_3
+               ->  Hash
+                     ->  Seq Scan on prt1_l_p2_p2 t2_3
+                           Filter: ((t1_3.a = a) AND ((t1_3.c)::text = 
(c)::text))
+   ->  Nested Loop Left Join
+         ->  Sort
+               Sort Key: t1_4.a
                ->  Seq Scan on prt1_l_p3_p1 t1_4
                      Filter: (b = 0)
-               ->  Hash Join
-                     Hash Cond: ((t3_5.b = t2_5.a) AND ((t3_5.c)::text = 
(t2_5.c)::text))
+         ->  Hash Join
+               Hash Cond: ((t3_5.b = t2_5.a) AND ((t3_5.c)::text = 
(t2_5.c)::text))
+               ->  Append
+                     ->  Seq Scan on prt2_l_p3_p1 t3_5
+                     ->  Seq Scan on prt2_l_p3_p2 t3_6
+               ->  Hash
                      ->  Append
-                           ->  Seq Scan on prt2_l_p3_p1 t3_5
-                           ->  Seq Scan on prt2_l_p3_p2 t3_6
-                     ->  Hash
-                           ->  Append
-                                 ->  Seq Scan on prt1_l_p3_p1 t2_5
-                                       Filter: ((t1_4.a = a) AND 
((t1_4.c)::text = (c)::text))
-                                 ->  Seq Scan on prt1_l_p3_p2 t2_6
-                                       Filter: ((t1_4.a = a) AND 
((t1_4.c)::text = (c)::text))
-(44 rows)
+                           ->  Seq Scan on prt1_l_p3_p1 t2_5
+                                 Filter: ((t1_4.a = a) AND ((t1_4.c)::text = 
(c)::text))
+                           ->  Seq Scan on prt1_l_p3_p2 t2_6
+                                 Filter: ((t1_4.a = a) AND ((t1_4.c)::text = 
(c)::text))
+(51 rows)
 
 SELECT * FROM prt1_l t1 LEFT JOIN LATERAL
                          (SELECT t2.a AS t2a, t2.c AS t2c, t2.b AS t2b, t3.b 
AS t3b, least(t1.a,t2.a,t3.b) FROM prt1_l t2 JOIN prt2_l t3 ON (t2.a = t3.b AND 
t2.c = t3.c)) ss
@@ -5143,36 +5150,46 @@ EXPLAIN (COSTS OFF)
 SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = 
t2.b AND t1.c = t2.c) WHERE ((t1.b >= 100 AND t1.b < 110) OR (t1.b >= 200 AND 
t1.b < 210)) AND ((t2.b >= 100 AND t2.b < 110) OR (t2.b >= 200 AND t2.b < 210)) 
AND t1.c IN ('0004', '0009') ORDER BY t1.a, t1.b;
                                                            QUERY PLAN          
                                                 
 
--------------------------------------------------------------------------------------------------------------------------------
- Sort
+ Merge Append
    Sort Key: t1.a, t1.b
-   ->  Append
-         ->  Hash Join
-               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.b = t2_1.b) AND (t1_1.c 
= t2_1.c))
+   ->  Merge Join
+         Merge Cond: ((t1_1.a = t2_1.a) AND (t1_1.b = t2_1.b) AND (t1_1.c = 
t2_1.c))
+         ->  Sort
+               Sort Key: t1_1.a, t1_1.b, t1_1.c
                ->  Seq Scan on alpha_neg_p1 t1_1
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 
100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
-               ->  Hash
-                     ->  Seq Scan on beta_neg_p1 t2_1
-                           Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) 
AND (b < 210)))
-         ->  Hash Join
-               Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.b = t2_2.b) AND (t1_2.c 
= t2_2.c))
+         ->  Sort
+               Sort Key: t2_1.a, t2_1.b, t2_1.c
+               ->  Seq Scan on beta_neg_p1 t2_1
+                     Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b 
< 210)))
+   ->  Merge Join
+         Merge Cond: ((t1_2.a = t2_2.a) AND (t1_2.b = t2_2.b) AND (t1_2.c = 
t2_2.c))
+         ->  Sort
+               Sort Key: t1_2.a, t1_2.b, t1_2.c
                ->  Seq Scan on alpha_neg_p2 t1_2
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 
100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
-               ->  Hash
-                     ->  Seq Scan on beta_neg_p2 t2_2
-                           Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) 
AND (b < 210)))
-         ->  Nested Loop
-               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.b = t2_3.b) AND 
(t1_3.c = t2_3.c))
-               ->  Seq Scan on alpha_pos_p2 t1_3
-                     Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 
100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
+         ->  Sort
+               Sort Key: t2_2.a, t2_2.b, t2_2.c
+               ->  Seq Scan on beta_neg_p2 t2_2
+                     Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b 
< 210)))
+   ->  Nested Loop
+         Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.b = t2_3.b) AND (t1_3.c = 
t2_3.c))
+         ->  Sort
+               Sort Key: t2_3.a, t2_3.b
                ->  Seq Scan on beta_pos_p2 t2_3
                      Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b 
< 210)))
-         ->  Nested Loop
-               Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.b = t2_4.b) AND 
(t1_4.c = t2_4.c))
-               ->  Seq Scan on alpha_pos_p3 t1_4
+         ->  Materialize
+               ->  Seq Scan on alpha_pos_p2 t1_3
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 
100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
+   ->  Nested Loop
+         Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.b = t2_4.b) AND (t1_4.c = 
t2_4.c))
+         ->  Sort
+               Sort Key: t2_4.a, t2_4.b
                ->  Seq Scan on beta_pos_p3 t2_4
                      Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b 
< 210)))
-(29 rows)
+         ->  Seq Scan on alpha_pos_p3 t1_4
+               Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND 
(b < 110)) OR ((b >= 200) AND (b < 210))))
+(39 rows)
 
 SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = 
t2.b AND t1.c = t2.c) WHERE ((t1.b >= 100 AND t1.b < 110) OR (t1.b >= 200 AND 
t1.b < 210)) AND ((t2.b >= 100 AND t2.b < 110) OR (t2.b >= 200 AND t2.b < 210)) 
AND t1.c IN ('0004', '0009') ORDER BY t1.a, t1.b;
  a  |  b  |  c   | a  |  b  |  c   
diff --git a/src/test/regress/expected/pg_lsn.out 
b/src/test/regress/expected/pg_lsn.out
index 8ab59b2e445..ee014f7514e 100644
--- a/src/test/regress/expected/pg_lsn.out
+++ b/src/test/regress/expected/pg_lsn.out
@@ -142,21 +142,19 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
        generate_series(1, 5) k
   WHERE i <= 10 AND j > 0 AND j <= 10
   ORDER BY f;
-                                QUERY PLAN                                
---------------------------------------------------------------------------
- Sort
-   Sort Key: (((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn)
-   ->  HashAggregate
-         Group Key: ((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn
-         ->  Nested Loop
-               ->  Function Scan on generate_series k
-               ->  Materialize
-                     ->  Nested Loop
-                           ->  Function Scan on generate_series j
-                                 Filter: ((j > 0) AND (j <= 10))
-                           ->  Function Scan on generate_series i
-                                 Filter: (i <= 10)
-(12 rows)
+                                   QUERY PLAN                                  
  
+---------------------------------------------------------------------------------
+ Unique
+   ->  Nested Loop
+         ->  Sort
+               Sort Key: (((((i.i)::text || '/'::text) || 
(j.j)::text))::pg_lsn)
+               ->  Nested Loop
+                     ->  Function Scan on generate_series j
+                           Filter: ((j > 0) AND (j <= 10))
+                     ->  Function Scan on generate_series i
+                           Filter: (i <= 10)
+         ->  Function Scan on generate_series k
+(10 rows)
 
 SELECT DISTINCT (i || '/' || j)::pg_lsn f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/tablesample.out 
b/src/test/regress/expected/tablesample.out
index 9ff4611640c..fe2dd6eef41 100644
--- a/src/test/regress/expected/tablesample.out
+++ b/src/test/regress/expected/tablesample.out
@@ -253,13 +253,15 @@ select pct, count(unique1) from
   group by pct;
                        QUERY PLAN                       
 --------------------------------------------------------
- HashAggregate
+ GroupAggregate
    Group Key: "*VALUES*".column1
    ->  Nested Loop
-         ->  Values Scan on "*VALUES*"
+         ->  Sort
+               Sort Key: "*VALUES*".column1
+               ->  Values Scan on "*VALUES*"
          ->  Sample Scan on tenk1
                Sampling: bernoulli ("*VALUES*".column1)
-(6 rows)
+(8 rows)
 
 select pct, count(unique1) from
   (values (0),(100)) v(pct),
diff --git a/src/test/regress/sql/create_am.sql 
b/src/test/regress/sql/create_am.sql
index 754fe0c694b..0062189eb61 100644
--- a/src/test/regress/sql/create_am.sql
+++ b/src/test/regress/sql/create_am.sql
@@ -146,9 +146,9 @@ SELECT
     pc.relkind,
     pa.amname,
     CASE WHEN relkind = 't' THEN
-        (SELECT 'toast for ' || relname::regclass FROM pg_class pcm WHERE 
pcm.reltoastrelid = pc.oid)
+        (SELECT 'toast for ' || pcm.oid::regclass FROM pg_class pcm WHERE 
pcm.reltoastrelid = pc.oid)
     ELSE
-        relname::regclass::text
+        pc.oid::regclass::text
     END COLLATE "C" AS relname
 FROM pg_class AS pc,
     pg_am AS pa
-- 
2.53.0

-- Real-life example: Top-10 most popular products with type-specific properties
-- This demonstrates the pre-sorted scan optimization in PG18.

DROP TABLE IF EXISTS electronics_props, clothing_props, food_props, products 
CASCADE;

-- Main products table (large)
CREATE TABLE products (
    id          int PRIMARY KEY,
    name        text NOT NULL,
    category    text NOT NULL,  -- 'electronics', 'clothing', 'food', ...
    popularity  int NOT NULL,   -- sales count or rating score
    price       numeric(10,2)
);

-- Type-specific property tables
CREATE TABLE electronics_props (
    product_id  int PRIMARY KEY REFERENCES products(id),
    warranty_months int,
    voltage     text,
    wifi        boolean
);

CREATE TABLE clothing_props (
    product_id  int PRIMARY KEY REFERENCES products(id),
    size        text,
    color       text,
    material    text
);

CREATE TABLE food_props (
    product_id  int PRIMARY KEY REFERENCES products(id),
    expiry_days int,
    organic     boolean,
    allergens   text
);

-- Populate products: 100k rows, ~33k per category
INSERT INTO products
SELECT
    g,
    'Product_' || g,
    CASE (g % 3) WHEN 0 THEN 'electronics'
                 WHEN 1 THEN 'clothing'
                 ELSE 'food' END,
    (random() * 10000)::int,   -- popularity 0..10000
    (random() * 500 + 1)::numeric(10,2)
FROM generate_series(1, 100000) g;

-- Populate property tables for matching categories
INSERT INTO electronics_props
SELECT id, (random()*36)::int, '220V', (random() > 0.5)
FROM products WHERE category = 'electronics';

INSERT INTO clothing_props
SELECT id,
    (ARRAY['S','M','L','XL'])[1 + (random()*3)::int],
    (ARRAY['red','blue','black','white'])[1 + (random()*3)::int],
    (ARRAY['cotton','polyester','wool'])[1 + (random()*2)::int]
FROM products WHERE category = 'clothing';

INSERT INTO food_props
SELECT id, (random()*365)::int, (random() > 0.5),
    (ARRAY['nuts','gluten','dairy',NULL])[1 + (random()*3)::int]
FROM products WHERE category = 'food';

VACUUM ANALYZE;

-- The query: top-10 most popular products with all their properties as JSON
EXPLAIN (ANALYZE, COSTS OFF)
SELECT
    p.id,
    p.name,
    p.category,
    p.popularity,
    p.price,
    json_strip_nulls(json_build_object(
        'warranty_months', e.warranty_months,
        'voltage',         e.voltage,
        'wifi',            e.wifi,
        'size',            c.size,
        'color',           c.color,
        'material',        c.material,
        'expiry_days',     f.expiry_days,
        'organic',         f.organic,
        'allergens',       f.allergens
    )) AS properties
FROM products p
    LEFT JOIN electronics_props e ON e.product_id = p.id
    LEFT JOIN clothing_props    c ON c.product_id = p.id
    LEFT JOIN food_props        f ON f.product_id = p.id
ORDER BY p.popularity DESC
LIMIT 10;

/*
 Limit (actual time=43.924..44.176 rows=10.00 loops=1)
   Buffers: shared hit=940
   ->  Nested Loop Left Join (actual time=43.922..44.172 rows=10.00 loops=1)
         Buffers: shared hit=940
         ->  Nested Loop Left Join (actual time=43.865..43.980 rows=10.00 
loops=1)
               Buffers: shared hit=918
               ->  Nested Loop Left Join (actual time=43.851..43.903 rows=10.00 
loops=1)
                     Buffers: shared hit=892
                     ->  Sort (actual time=43.825..43.826 rows=10.00 loops=1)
                           Sort Key: p.popularity DESC
                           Sort Method: top-N heapsort  Memory: 26kB
                           Buffers: shared hit=870
                           ->  Seq Scan on products p (actual time=0.016..9.847 
rows=100000.00 loops=1)
                                 Buffers: shared hit=870
                     ->  Index Scan using electronics_props_pkey on 
electronics_props e (actual time=0.006..0.006 rows=0.20 loops=10)
                           Index Cond: (product_id = p.id)
                           Index Searches: 10
                           Buffers: shared hit=22
               ->  Index Scan using clothing_props_pkey on clothing_props c 
(actual time=0.007..0.007 rows=0.60 loops=10)
                     Index Cond: (product_id = p.id)
                     Index Searches: 10
                     Buffers: shared hit=26
         ->  Index Scan using food_props_pkey on food_props f (actual 
time=0.006..0.006 rows=0.20 loops=10)
               Index Cond: (product_id = p.id)
               Index Searches: 10
               Buffers: shared hit=22
 Planning:
   Buffers: shared hit=36
 Planning Time: 2.150 ms
 Execution Time: 44.305 ms
(30 rows)

 Limit (actual time=813.491..813.495 rows=10.00 loops=1)
   Buffers: shared hit=1442
   ->  Sort (actual time=813.489..813.491 rows=10.00 loops=1)
         Sort Key: p.popularity DESC
         Sort Method: top-N heapsort  Memory: 27kB
         Buffers: shared hit=1442
         ->  Hash Left Join (actual time=73.483..772.134 rows=100000.00 loops=1)
               Hash Cond: (p.id = f.product_id)
               Buffers: shared hit=1442
               ->  Hash Left Join (actual time=54.732..128.615 rows=100000.00 
loops=1)
                     Hash Cond: (p.id = c.product_id)
                     Buffers: shared hit=1261
                     ->  Hash Left Join (actual time=32.330..79.951 
rows=100000.00 loops=1)
                           Hash Cond: (p.id = e.product_id)
                           Buffers: shared hit=1051
                           ->  Seq Scan on products p (actual time=0.036..6.171 
rows=100000.00 loops=1)
                                 Buffers: shared hit=870
                           ->  Hash (actual time=32.189..32.189 rows=33333.00 
loops=1)
                                 Buckets: 65536  Batches: 1  Memory Usage: 
2075kB
                                 Buffers: shared hit=181
                                 ->  Seq Scan on electronics_props e (actual 
time=0.028..12.981 rows=33333.00 loops=1)
                                       Buffers: shared hit=181
                     ->  Hash (actual time=22.354..22.354 rows=33334.00 loops=1)
                           Buckets: 65536  Batches: 1  Memory Usage: 2247kB
                           Buffers: shared hit=210
                           ->  Seq Scan on clothing_props c (actual 
time=0.018..8.800 rows=33334.00 loops=1)
                                 Buffers: shared hit=210
               ->  Hash (actual time=18.680..18.681 rows=33333.00 loops=1)
                     Buckets: 65536  Batches: 1  Memory Usage: 2054kB
                     Buffers: shared hit=181
                     ->  Seq Scan on food_props f (actual time=0.008..7.573 
rows=33333.00 loops=1)
                           Buffers: shared hit=181
 Planning:
   Buffers: shared hit=73
 Planning Time: 3.966 ms
 Execution Time: 813.815 ms
(36 rows)

 */

Reply via email to