[RFC PATCH] Skip LATERAL check in join_clause_is_movable_to() during OR-clause restriction extraction

From: g l <orangegrove(at)live(dot)com>
To: "pgsql-hackers(at)postgresql(dot)org" <pgsql-hackers(at)postgresql(dot)org>
Subject: [RFC PATCH] Skip LATERAL check in join_clause_is_movable_to() during OR-clause restriction extraction
Date: 2026-10-26 16:47:26
Message-ID: SY7P300MB0879AC304A89E6CCCC277796DA642@SY7P300MB0879.AUSP300.PROD.OUTLOOK.COM
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi hackers,

I am investigating a case where join_clause_is_movable_to() appears to be too restrictive when it is called from extract_restriction_or_clauses().

join_clause_is_movable_to() is used by several callers, including match_join_clauses_to_index() and extract_restriction_or_clauses().
It performs some check, the 4th one is:

/*
* Clause must not use any rels with LATERAL references to this rel
*/
if (bms_overlap(baserel->lateral_referencers, rinfo->clause_relids))
return false;

I believe this check is necessary when the function is used by match_join_clauses_to_index(): in that case the clause can be used for a parameterized scan, and the relation containing the lateral reference cannot simply be placed on the outer side of a NestLoop with the target relation.

However, the situation appears to be different when the function is called from extract_restriction_or_clauses(). In that case, the clause is not being used to construct such a parameterized path. Instead, extract_restriction_or_clauses() is trying to derive restriction clauses from an OR-clause and push those restrictions to the relevant base relation.

Therefore, I am wondering whether the lateral-reference check is unnecessarily restrictive in this caller. Removing the check only for this use of join_clause_is_movable_to() allows additional restriction
clauses to be extracted and can result in substantially better plans.

I tested this with the following example. I created:

CREATE TABLE a(a1 int, a2 int, a3 int, a4 int);
CREATE TABLE b(b1 int, b2 int, b3 int, b4 int);

Table A contains 10,000 rows and table B contains 30,000 rows, with random integers and NULLs populated into the columns (The SQL commands I used for populating data and the full output of 'EXPLAIN ANALYZE' commands are in the attached file TEST.txt).

I tested with this query:

EXPLAIN ANALYZE
SELECT *
FROM A,
LATERAL (
SELECT sum(b1) x, sum(b2) y, b3, b4
FROM B
WHERE b3+b4 < a1+a2
GROUP BY b3,b4
) I
WHERE (b3<a3 and (a4=32 or a4=33))
OR (b3>a3-999 and a4=18)
OR (x-y<a3+a4 and a1-a2=a4);

With the current code, the planner does not extract a restriction for table A. The lateral subquery is therefore executed once for each of the 10,000 rows of table A:

-> Seq Scan on A
rows=10000

-> Subquery Scan on I
loops=10000

The execution time is "356292.545 ms"

After relaxing the 4th check when join_clause_is_movable_to() is called from extract_restriction_or_clauses(), the planner extracts:

((a4 = 32) OR (a4 = 33) OR (a4 = 18) OR ((a1 - a2) = a4))

and the plan becomes:

-> Seq Scan on A
rows=7

-> Subquery Scan on I
loops=7

The execution time drops to "369.580 ms"

The query returned 50923 rows in both versions.

I also tested a 'LEFT JOIN LATERAL' case:

EXPLAIN ANALYZE
SELECT *
FROM a
LEFT JOIN LATERAL (
SELECT sum(b1) x,sum(b2) y,b3,b4
FROM b GROUP BY b3,b4 HAVING b3+b4<>a3+a4
) ON a1+a3>x+b3
WHERE (a1=coalesce(x,30) and a2-a4>a1+a3)
OR (a2=coalesce(y-b3,41) and a2+5=a4+a3);

With the current code, subquery is executed 10,000 times, and the execution time is "631284.853 ms"

After the relaxation, the planner derives a restriction on table A:

Filter: (((a2 - a4) > (a1 + a3)) OR ((a2 + 5) = (a4 + a3)))

subquery is executed only 370 times, and the execution time drops to "22444.571 ms"

Again, both versions return 213 rows.

For both examples, I materialized the results produced before and after the change and compared them using bidirectional 'EXCEPT ALL', similar to this:

(SELECT * FROM before EXCEPT ALL SELECT * FROM after)
UNION ALL
(SELECT * FROM after EXCEPT ALL SELECT * FROM before);

In both cases this returned zero rows, so I did not observe any result difference in these test cases.

I also ran make check-world with the patch applied, all tests that were executed passed.

The attached patch makes the lateral-reference check caller-specific: it is retained for the existing uses where it is needed, but skipped when join_clause_is_movable_to() is used by
extract_restriction_or_clauses().

I would especially appreciate feedback on the correctness of this caller-specific relaxation, and in particular whether the fourth check is required for restriction extraction for some reason that I have overlooked.

Thanks, Geng Lei

Attachment Content-Type Size
0001-Skip-LATERAL-check-in-join_clause_is_movable_to-duri.patch application/octet-stream 5.3 KB
TEST.txt text/plain 9.9 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Pavel Stehule 2026-10-26 16:58:55 Re: PL/pgSQL: fix validation of anycompatiblemultirange
Previous Message Tom Lane 2026-10-26 16:20:08 Re: Let's drop support for named POSIX semaphores