Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 200 additions & 62 deletions qpmodel/subquery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1503,7 +1503,7 @@

public class LogicCteProducer : LogicNode
{
internal CteExpr cte_;

Check warning on line 1506 in qpmodel/subquery.cs

View workflow job for this annotation

GitHub Actions / build

Field 'LogicCteProducer.cte_' is never assigned to, and will always have its default value null

Check warning on line 1506 in qpmodel/subquery.cs

View workflow job for this annotation

GitHub Actions / build

Field 'LogicCteProducer.cte_' is never assigned to, and will always have its default value null

Check warning on line 1506 in qpmodel/subquery.cs

View workflow job for this annotation

GitHub Actions / build

Field 'LogicCteProducer.cte_' is never assigned to, and will always have its default value null

Check warning on line 1506 in qpmodel/subquery.cs

View workflow job for this annotation

GitHub Actions / build

Field 'LogicCteProducer.cte_' is never assigned to, and will always have its default value null

public CteInfoEntry cteInfoEntry_;
// represent the id of CTE, it should match the related CteProducer
Expand Down Expand Up @@ -1712,95 +1712,233 @@
public override void Exec(Action<Row> callback)
{
var isDerivedFromInClause = filterHasMarkerBinExpr(logic_.filter_);
Exec(callback, isDerivedFromInClause);
if (isDerivedFromInClause)
ExecHash(callback);
else
ExecNested(callback);
}
public void Exec(Action<Row> callback, bool isDerivedFromInClause)

// Hash-based execution for IN/NOT IN derived MarkJoin (issue #272).
//
// The filter has two parts:
// - Marker BinExpr: the IN-list equality (e.g., a2=b2)
// - Residual: the correlation predicate (e.g., b1=a1)
//
// We hash the RIGHT side on the RESIDUAL equi-join keys (the correlation
// condition), then for each left row probe the hash table and evaluate
// the marker predicate for three-valued IN/NOT IN logic.
void ExecHash(Action<Row> callback)
{
ExecContext context = context_;
var logic = logic_ as LogicMarkJoin;
var filter = logic.filter_;
bool semi = (logic_ is LogicMarkSemiJoin);
bool antisemi = (logic_ is LogicMarkAntiSemiJoin);
bool lIsNull = false; // l represent one row
bool RHasNull = false; // R represent a set of Row
bool RisEmpty = true;
Value marker = false; //false true null
int markerOrdinal = 0;
Debug.Assert(filter != null);

// Decompose the filter into marker predicate and residual.
var andList = filter.FilterToAndList();
var markerExpr = andList.Find(x => x is BinExpr xB && xB.IsMarkerBinExpr()) as BinExpr;
Debug.Assert(markerExpr != null);
andList.Remove(markerExpr);

// Extract equi-join keys from residual predicates for hashing.
var ltabrefs = lchild_().logic_.InclusiveTableRefs();
var leftKeys = new List<Expr>();
var rightKeys = new List<Expr>();
var nonEquiResidual = new List<Expr>();

foreach (var pred in andList)
{
if (pred is BinExpr be && be.op_ == "=")
{
var lrefs = be.lchild_().tableRefs_;
var rrefs = be.rchild_().tableRefs_;
bool lOnLeft = lrefs != null && !lrefs.Except(ltabrefs).Any();
bool rOnLeft = rrefs != null && !rrefs.Except(ltabrefs).Any();

if (lOnLeft && !rOnLeft)
{
leftKeys.Add(be.lchild_());
rightKeys.Add(be.rchild_());
continue;
}
else if (rOnLeft && !lOnLeft)
{
leftKeys.Add(be.rchild_());
rightKeys.Add(be.lchild_());
continue;
}
}
nonEquiResidual.Add(pred);
}

Expr nonEquiFilter = nonEquiResidual.Count > 0 ? FilterHelper.AndListToExpr(nonEquiResidual) : null;
int lColCount = lchild_().logic_.output_.Count;
int rColCount = rchild_().logic_.output_.Count;

// If no hashable residual keys, fall back to nested loop.
if (leftKeys.Count == 0)
{
ExecNestedInClause(callback, markerExpr, andList.Count > 0 ? FilterHelper.AndListToExpr(andList) : null);
return;
}

// Build hash table from right (subquery) side on residual keys.
var hm = new Dictionary<KeyList, List<Row>>();

rchild_().Exec(r =>
{
Row fakeLeft = new Row(lColCount);
Comment on lines +1787 to +1790

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.
Row combined = new Row(fakeLeft, r);
var keys = KeyList.ComputeKeys(context, rightKeys, combined);
if (keys.ColsHasNull())
return; // NULL correlation keys never match
if (hm.TryGetValue(keys, out List<Row> existing))
existing.Add(r);
else
hm.Add(keys, new List<Row> { r });
});

// Probe with left (outer) side.
lchild_().Exec(l =>
{
lIsNull = l.ColsHasNull();
marker = false;
rchild_().Exec(r =>
bool lIsNull = l.ColsHasNull();
Value marker = false;
bool RHasNull = false;
bool RisEmpty = true;

var keys = KeyList.ComputeKeys(context, leftKeys, l);
if (!keys.ColsHasNull() && hm.TryGetValue(keys, out List<Row> matches))
{
Row n = new Row(l, r);
if (isDerivedFromInClause)
// Hash hit on correlation keys — now evaluate marker and non-equi residual
foreach (var r in matches)
{
if (!(r is null))
RHasNull = r.ColsHasNull();
var andList = filter.FilterToAndList();

// SELECT a1 FROM a WHERE a1 = 3 and a2 NOT IN (SELECT b2 FROM b WHERE a1 < b1);
// a1 < b1 will not produce marker
// a2 = b2 will produce marker
// if the markjoin is derived from IN clause, we need to judge if it is a empty
//
if (andList.Count >= 2)
Row combined = new Row(l, r);

// Check non-equi residual first
if (nonEquiFilter != null)
{
var markerExpr = andList.Find(x => x is BinExpr xB && xB.IsMarkerBinExpr());
andList.Remove(markerExpr);
var excludeMarkerExpr = FilterHelper.AndListToExpr(andList);
var flagE = excludeMarkerExpr.Exec(context, n);
var flag = nonEquiFilter.Exec(context, combined);
if (!(flag is true))
continue;
}

if (flagE is true)
RisEmpty = false;
else
return;
RisEmpty = false;
if (r.ColsHasNull())
RHasNull = true;

// there is at least one match, mark true
if (markerExpr.Exec(context, n) is true)
marker = true;
}
else if (filter.Exec(context, n) is true)
marker = true;
}
else if (!(marker is true) && !isDerivedFromInClause)
{
if (filter.Exec(context, n) is true)
// Evaluate the marker (IN-list equality) predicate
if (markerExpr.Exec(context, combined) is true)
{
marker = true;
n = ExecProject(n);
fixMarkerValue(n, semi);
callback(n);
break; // One match is enough for IN
}
}
});
}

// Three-valued NULL logic (same as original nested loop):
if (marker is false && RHasNull)
marker = null;
if (lIsNull && RisEmpty)
marker = false;

Row rr = new Row(rColCount);
Row n = new Row(l, rr);
n = ExecProject(n);

if (isDerivedFromInClause)
if (marker is null)
fixMarkerValue(n, false);
else
{
if (marker is false && RHasNull)
marker = null;
bool boolMarker = marker is true;
fixMarkerValue(n, semi ? boolMarker : !boolMarker);
}

callback(n);
});
}

if (lIsNull && RisEmpty)
marker = false;
// Nested-loop fallback for IN-clause MarkJoin when no hashable keys found.
void ExecNestedInClause(Action<Row> callback, BinExpr markerExpr, Expr residualFilter)
{
ExecContext context = context_;
bool semi = (logic_ is LogicMarkSemiJoin);
int rColCount = rchild_().logic_.output_.Count;

Row r = new Row(rchild_().logic_.output_.Count);
Row n = new Row(l, r);
n = ExecProject(n);
lchild_().Exec(l =>
{
bool lIsNull = l.ColsHasNull();
Value marker = false;
bool RHasNull = false;
bool RisEmpty = true;

markerOrdinal = findMarkerOrdinal();
if (marker is null)
fixMarkerValue(n, false);
else
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;
});
Comment on lines +1874 to +1891

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.

if (marker is false && RHasNull)
marker = null;
if (lIsNull && RisEmpty)
marker = false;

Row rr = new Row(rColCount);
Row n = new Row(l, rr);
n = ExecProject(n);

if (marker is null)
fixMarkerValue(n, false);
else
{
bool boolMarker = marker is true;
fixMarkerValue(n, semi ? boolMarker : !boolMarker);
}
else if (!(marker is true) && !isDerivedFromInClause)

callback(n);
});
}

// Nested-loop execution for EXISTS/NOT EXISTS derived MarkJoin.
void ExecNested(Action<Row> callback)
{
ExecContext context = context_;
var logic = logic_ as LogicMarkJoin;
var filter = logic.filter_;
bool semi = (logic_ is LogicMarkSemiJoin);
Value marker = false;

lchild_().Exec(l =>
{
marker = false;
rchild_().Exec(r =>
{
if (!(marker is true))
{
Row n = new Row(l, r);
if (filter.Exec(context, n) is true)
{
marker = true;
n = ExecProject(n);
fixMarkerValue(n, semi);
callback(n);
}
}
});

if (!(marker is true))
{
Row r = new Row(rchild_().logic_.output_.Count);
Row n = new Row(l, r);
Expand Down
68 changes: 68 additions & 0 deletions test/regress/expect/subqueryd_hashmj.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
select a1 from a where a2 in (select b2 from b where b2 = a1)
PhysicFilter (actual rows=0)
Output: a.a1[0]
Filter: {#marker@1}[1]
-> PhysicMarkJoin Left (actual rows=3)
Output: a.a1[0],{#marker@1}[1]
Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=3)
Output: b.b2[1]


select a1 from a where a2 not in (select b2 from b where b2 = a1)
PhysicFilter (actual rows=3)
Output: a.a1[0]
Filter: {#marker@1}[1]
-> PhysicMarkJoin Left (actual rows=3)
Output: a.a1[0],{#marker@1}[1]
Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=3)
Output: b.b2[1]
0
1
2

select a1 from a where a1 in (select b1 from b where b2 = a2 and b3 > 2)
PhysicFilter (actual rows=2)
Output: a.a1[0]
Filter: {#marker@1}[1]
-> PhysicMarkJoin Left (actual rows=3)
Output: a.a1[0],{#marker@1}[1]
Filter: (a.a1[0]=b.b1[2] and b.b2[3]=a.a2[1])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=2)
Output: b.b1[0],b.b2[1]
Filter: b.b3[2]>2
1
2

select a1 from a where a1 not in (select b1 from b where b2 = a2 and b3 > 2)
PhysicFilter (actual rows=1)
Output: a.a1[0]
Filter: {#marker@1}[1]
-> PhysicMarkJoin Left (actual rows=3)
Output: a.a1[0],{#marker@1}[1]
Filter: (a.a1[0]=b.b1[2] and b.b2[3]=a.a2[1])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=2)
Output: b.b1[0],b.b2[1]
Filter: b.b3[2]>2
0

select a1 from a where a2 in (select b2 from b where b3 > 2)
PhysicScanTable a (actual rows=2)
Output: a.a1[0]
Filter: a.a2[1] in @1
<InSubqueryExpr> cached 1
-> PhysicScanTable b (actual rows=2)
Output: b.b2[1]
Filter: b.b3[2]>2
1
2

4 changes: 2 additions & 2 deletions test/regress/expect/subqueryd_or.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ PhysicFilter (actual rows=1)
Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=3, loops=3)
-> PhysicScanTable b (actual rows=3)
Output: b.b2[1]
2

Expand All @@ -20,7 +20,7 @@ PhysicFilter (actual rows=3)
Filter: (a.a2[1]=b.b2[2] and b.b2[2]=a.a1[0])
-> PhysicScanTable a (actual rows=3)
Output: a.a1[0],a.a2[1]
-> PhysicScanTable b (actual rows=3, loops=3)
-> PhysicScanTable b (actual rows=3)
Output: b.b2[1]
0
1
Expand Down
Loading
Loading