Skip to content

Realize MarkJoin as HashJoin for IN/NOT IN (issue #272) - #323

Merged
zhouqingqing merged 1 commit into
masterfrom
fix-hashjoin-markjoin
Apr 15, 2026
Merged

zhouqingqing merged 1 commit into
masterfrom
fix-hashjoin-markjoin

Conversation

@zhouqingqing

Copy link
Copy Markdown
Owner

Summary

  • Replace nested-loop execution with hash-based execution for IN/NOT IN derived MarkJoin
  • Hash on correlation (residual) keys, evaluate marker predicate per-row for three-valued NULL logic
  • Falls back to nested loop when no hashable residual keys exist; EXISTS/NOT EXISTS still uses nested loop
  • Added subqueryd_hashmj.sql regression tests covering IN, NOT IN, with/without correlation keys

Closes #272

Test plan

  • All 72 existing tests pass
  • New subqueryd_hashmj.sql regression test covers hash path (IN, NOT IN, with extra filters, no-correlation fallback)
  • Updated subqueryd_or.txt expect file (no more loops=3 for hash-executed MarkJoin)

🤖 Generated with Claude Code

Replace nested-loop execution with hash-based execution for IN/NOT IN
derived MarkJoin. Hash on correlation (residual) keys, then evaluate
the marker predicate per-row for three-valued NULL logic. Falls back
to nested loop when no hashable residual keys exist. EXISTS/NOT EXISTS
MarkJoin continues to use nested loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements hash-based execution for IN/NOT IN-derived PhysicMarkJoin to replace nested-loop evaluation when correlation (residual) equi-join keys exist, while retaining nested-loop execution for EXISTS/NOT EXISTS and as a fallback when no hashable residual keys are available.

Changes:

  • Route PhysicMarkJoin.Exec() to a new hash-based execution path for IN/NOT IN mark joins, with a nested-loop fallback when no residual equi-keys can be extracted.
  • Add new regression coverage for the hash path and fallback behavior (subqueryd_hashmj.sql + expected output).
  • Update existing regression expected output (subqueryd_or.txt) to reflect the removal of per-row loop counts under hash execution.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
qpmodel/subquery.cs Adds hash-based execution logic for IN/NOT IN-derived mark joins and introduces a nested-loop fallback helper.
test/regress/sql/subqueryd_hashmj.sql New SQL regression tests exercising hash-based mark join behavior and no-correlation fallback.
test/regress/expect/subqueryd_hashmj.txt Expected output for the new regression test file.
test/regress/expect/subqueryd_or.txt Updates expected plan output (removes loops=3 on right scan due to hash execution).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread qpmodel/subquery.cs
Comment on lines +1874 to +1891
rchild_().Exec(r =>
{
Row combined = new Row(l, r);

if (residualFilter != null)
{
bool boolMarker = marker is true;
fixMarkerValue(n, semi ? boolMarker : !boolMarker);
var flag = residualFilter.Exec(context, combined);
if (!(flag is true))
return;
}

callback(n);
RisEmpty = false;
if (r.ColsHasNull())
RHasNull = true;

if (markerExpr.Exec(context, combined) is true)
marker = true;
});

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

ExecNestedInClause scans the entire right side even after marker becomes true. For IN/NOT IN semantics a single true match is sufficient to determine the final marker (and for NOT IN, a true match definitively makes the predicate false), so this can be short-circuited to avoid unnecessary work on large subqueries (e.g., by checking marker is true at the top of the rchild_().Exec callback and returning early, or using context.stop_ if that’s the established cancellation mechanism).

Copilot uses AI. Check for mistakes.
Comment thread qpmodel/subquery.cs
Comment on lines +1787 to +1790

rchild_().Exec(r =>
{
Row fakeLeft = new Row(lColCount);

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

In ExecHash, fakeLeft is allocated inside the rchild_().Exec callback, so a new all-null Row is created for every right-side row. This is unnecessary allocation/GC pressure since the left placeholder is constant; consider allocating fakeLeft once outside the loop (or avoid the combined row entirely if rightKeys are guaranteed right-only).

Suggested change
rchild_().Exec(r =>
{
Row fakeLeft = new Row(lColCount);
Row fakeLeft = new Row(lColCount);
rchild_().Exec(r =>
{

Copilot uses AI. Check for mistakes.
@zhouqingqing
zhouqingqing merged commit 903d29e into master Apr 15, 2026
7 checks passed
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.

we should realize the MarkJoin as HashJoin

2 participants