Try a presorted outer path when referenced by an ORDER BY prefix

From: Andrei Lepikhov <lepihov(at)gmail(dot)com>
To: pgsql-hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: Try a presorted outer path when referenced by an ORDER BY prefix
Date: 2026-04-03 11:35:34
Message-ID: 19a9265c-c441-4a43-bc0d-dac533438da0@gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

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=KcVDSP6q9ume3hYpQ@mail.gmail.com

--
regards, Andrei Lepikhov,
pgEdge

Attachment Content-Type Size
v0-0001-Extend-the-ExecSetTupleBound-to-LEFT-JOIN-outer-s.patch text/plain 1.7 KB
v0-0002-Try-pre-sorted-outer-path-for-a-JOIN.patch text/plain 5.1 KB
v0-0003-Adjust-query-plans-changed.patch text/plain 34.7 KB
example_schema.sql text/plain 6.6 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Nazir Bilal Yavuz 2026-04-03 11:37:47 Re: pg_waldump: support decoding of WAL inside tarfile
Previous Message Daniel Gustafsson 2026-04-03 10:27:27 Re: Flaky test in t/100_vacuumdb.pl: ordering assumption not stable under plan changes