Skip to content

[GLUTEN-12921][VL] Support full outer BroadcastNestedLoopJoin with guarded rewrite - #12922

Open
WangGuangxin wants to merge 6 commits into
apache:mainfrom
WangGuangxin:bnlj
Open

[GLUTEN-12921][VL] Support full outer BroadcastNestedLoopJoin with guarded rewrite#12922
WangGuangxin wants to merge 6 commits into
apache:mainfrom
WangGuangxin:bnlj

Conversation

@WangGuangxin

@WangGuangxin WangGuangxin commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

This PR adds Velox support for BroadcastNestedLoopJoinExec in FULL OUTER JOIN cases by rewriting the Spark physical plan into a composition that Velox already supports, instead of falling back the original
full outer BNLJ.

The rewrite transforms:

  • BroadcastNestedLoopJoinExec(..., FullOuter, ...)

into:

  • one outer BNLJ branch that produces matched rows plus unmatched streamed-side rows
  • one outer BNLJ branch with a synthetic non-null marker column to identify unmatched broadcast-side rows
  • a Union to combine both branches

This avoids introducing native Velox full outer nested-loop join support while preserving full outer join semantics, including null-sensitive cases.

In addition, this PR adds a conservative size-based guard for the rewrite:

  • the rewrite is applied only when both logical join sides have known statistics
  • both sides must be at or below spark.gluten.sql.columnar.broadcastNestedLoopJoin.fullOuterRewriteThreshold

Otherwise, Gluten keeps the original behavior and lets the plan fall back instead of forcing the rewrite on larger joins.

Why are the changes needed?

Velox does not support Spark's FULL OUTER broadcast nested loop join end-to-end today, so Gluten falls back for these queries.

A direct native implementation is not a good short-term option because:

  • the existing Velox/Gluten BNLJ pipeline is built around a broadcast build side
  • full outer semantics require emitting unmatched rows from both sides
  • handling unmatched broadcast-side rows natively would need extra protocol/state beyond the current execution model

A rewrite-based solution is lower risk because it reuses operators already supported by Velox.

However, rewriting every full outer BNLJ into two joins can be more expensive than a single Spark row-based fallback join. To keep the feature safe, this PR only enables the rewrite for small, stats-known joins
and preserves fallback for larger or unknown-size cases.

How was this patch tested?

More UT

Config

A new config is introduced:

  • spark.gluten.sql.columnar.backend.velox.broadcastNLJ.fullOuterRewriteThreshold

It controls the maximum per-side logical plan size for enabling the full outer BNLJ rewrite.

Was this patch authored or co-authored using generative AI tooling?

Yes. Generated-by: GPT-5

Related issue: #12921

@github-actions github-actions Bot added CORE works for Gluten Core VELOX labels Aug 28, 2026
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

2 similar comments
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions github-actions Bot added the DOCS label Aug 29, 2026
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

8 similar comments
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

2 similar comments
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@WangGuangxin

Copy link
Copy Markdown
Contributor Author

@zhztheplayer @zml1206 @philo-he please take a look at this when you are convenient

@philo-he philo-he left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

@zhztheplayer zhztheplayer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WangGuangxin

I see the following code to be added:

injector.injectQueryStagePrepRule(_ => VeloxBroadcastNestedLoopJoinRewriteRule())
injector.injectPre(_ => VeloxBroadcastNestedLoopJoinRewriteRule())

Curious why we need 2 rules here, is it a joined rewrite?

@zml1206 zml1206 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider using ExistenceJoin for the second branch instead of an outer join with a synthetic marker?
The second branch only needs to determine whether each row from the original broadcast side has any match. The current outer-join approach may emit every matching pair and then discard all of them through IsNull(markerAttr). For predicates with a high match rate or many-to-many matches, this can introduce substantial redundant processing.
ExistenceJoin is already supported by the Velox BNLJ path with BuildRight and maps to Velox’s kLeftSemiProject. It emits at most one row per streamed row and can stop searching after the first match. The rewrite could therefore be:

LeftOuter(L, R)
UNION ALL
Filter(!exists, ExistenceJoin(R, L))

followed by projecting NULL for the missing-side columns. This would also eliminate the synthetic marker while preserving the same full-outer-join semantics. Could we use this approach, or add a high-match-cardinality benchmark to compare the two implementations?

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

1 similar comment
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@WangGuangxin

Copy link
Copy Markdown
Contributor Author

@zhztheplayer @zml1206 @philo-he comments resolved. Please take another look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE works for Gluten Core DOCS VELOX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants