Skip to content

ORCA: don't push LOJ ON-pred onto its own outer in PushThruOuterChild#1836

Open
yjhjstz wants to merge 1 commit into
apache:mainfrom
yjhjstz:fix/orca-loj-bool-on-pred-pushdown
Open

ORCA: don't push LOJ ON-pred onto its own outer in PushThruOuterChild#1836
yjhjstz wants to merge 1 commit into
apache:mainfrom
yjhjstz:fix/orca-loj-bool-on-pred-pushdown

Conversation

@yjhjstz

@yjhjstz yjhjstz commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Fix a wrong-result bug in ORCA where a LEFT JOIN's own ON predicate ends up duplicated as a scan filter on the join's outer relation, discarding outer rows
that LOJ semantics require to be null-padded.

Repro

CREATE TABLE x(c1 boolean);
CREATE TABLE y1(c1 boolean);
CREATE TABLE y2(c1 boolean);
INSERT INTO x  VALUES (true),(false),(false);
INSERT INTO y1 VALUES (true);
INSERT INTO y2 VALUES (true);

SET optimizer = on;
SELECT x.c1, y2.c1 AS y2c1
  FROM x LEFT JOIN y1 ON x.c1
         LEFT JOIN y2 ON x.c1
 WHERE y2.c1 IS NULL;
Optimizer Result Seq Scan on x
Postgres planner 2 rows (correct) no scan filter
ORCA (before fix) 0 rows Filter: c1

Trigger: two or more chained LEFT JOINs whose ON-clauses use the same boolean column from the outer relation, with a WHERE on top.

Root cause

CNormalizer::PushThruOuterChild is invoked from PushThruSelect with a predicate that happens to be (or contain) the LOJ's own ON predicate.
SplitConjunct / FPushable accept it as pushable to the outer relation, because FPushable only checks that the predicate's columns are a subset of the
outer's output columns — an LOJ ON-pred that references only outer-side columns trivially satisfies that.

Consequence: ORCA wraps the outer with Select(outer, on_pred), producing LOJ(Select(x, c1), inner, x.c1). The LOJ's ON-pred is preserved as Join Filter, but a redundant Filter: c1 is also planted on the outer scan, which discards outer rows that don't satisfy the ON-pred — exactly the rows LOJ
must null-pad and keep.

Fixes #ISSUE_Number

What does this PR do?

Type of Change

  • Bug fix (non-breaking change)
  • New feature (non-breaking change)
  • Breaking change (fix or feature with breaking changes)
  • Documentation update

Breaking Changes

Test Plan

  • Unit tests added/updated
  • Integration tests added/updated
  • Passed make installcheck
  • Passed make -C src/test installcheck-cbdb-parallel

Impact

Performance:

User-facing changes:

Dependencies:

Checklist

Additional Context

CI Skip Instructions


When CNormalizer::PushThruOuterChild is invoked from PushThruSelect with a
predicate that happens to be (or contain) the LOJ's own ON predicate, the
SplitConjunct / FPushable structural check would classify it as pushable to
the outer relation -- because FPushable only verifies that the predicate's
columns are a subset of the outer's output columns, and an LOJ ON-pred that
references only outer-side columns trivially satisfies that.

The consequence: the ON-pred is duplicated as a scan filter on the outer
relation, which silently violates LOJ semantics. Outer rows that don't
satisfy the ON-pred (and would have been null-padded in the result) get
discarded at the scan, so a downstream "WHERE inner.col IS NULL" can no
longer match them.

Trigger pattern: two or more chained LEFT JOINs whose ON-clauses use the
same boolean column from the outer relation, with a WHERE on top.

Fix: at the entry of PushThruOuterChild, strip from the incoming conjunct
any conjunct that structurally matches a conjunct of the LOJ's own ON
predicate before handing it to SplitConjunct. The stripped conjunct is
already enforced by the join itself for matching rows and must not become
a filter on the outer for non-matching rows.

Add a minimal regression case to bfv_joins covering the two-LOJ-on-same-
boolean-column pattern with a WHERE on the inner side.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant