Skip to content

[CALCITE-7755] Support IEJoin for inequality joins - #5233

Open
zzwqqq wants to merge 1 commit into
apache:mainfrom
zzwqqq:iejoin
Open

[CALCITE-7755] Support IEJoin for inequality joins#5233
zzwqqq wants to merge 1 commit into
apache:mainfrom
zzwqqq:iejoin

Conversation

@zzwqqq

@zzwqqq zzwqqq commented Aug 30, 2026

Copy link
Copy Markdown
Member

Jira Link

CALCITE-7755

Changes Proposed

IEJoin is an algorithm for joins with two inequality predicates, described in “Lightning Fast and Space Efficient Inequality Joins,” PVLDB 8(13), 2015.

This change adds an IEJoin implementation for the Enumerable convention. It applies to inner joins with at least two inequality predicates between fields from the two inputs. IEJoin handles the first two predicates; any remaining predicates are evaluated by an EnumerableCalc. Other join conditions are left to the existing rules.

Planner and execution tests are included.

@zzwqqq
zzwqqq marked this pull request as draft August 30, 2026 09:30
@zzwqqq
zzwqqq marked this pull request as ready for review September 1, 2026 15:18
*/
package org.apache.calcite.linq4j;

/** Comparison operator applied to the left and right keys of an inequality

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.

I think this class is fully generic, maybe it can find uses outside of joins. So it's enough to characterize it as "comparison operator".

That being said, is there a benefit reusing the kind?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! I wanted to use SqlKind at first, but that would create a circular dependency. I just realized we already have ExpressionType.

(leftPoint, rightPoint) -> leftPoint.name + ":" + rightPoint.name);
}

private static List<String> nestedLoop(List<Point> leftRows,

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.

I assume this computes the expected values. A little JavaDoc would help

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, that’s right. Added Javadoc, thanks.

actual, is(expected));
}

private static Enumerable<String> ieJoin(List<Point> left, List<Point> right,

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.

I assume this computes the result using the new enumerator.
Add JavaDoc

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I assume this computes the result using the new enumerator. Add JavaDoc

Yes. Added Javadoc, thanks.

private @Nullable TResult current;
private boolean hasCurrent;

IEJoinEnumerator(Enumerable<TLeft> left, Enumerable<TRight> right,

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.

This needs some JavaDoc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks, added Javadoc.

implements Enumerator<TResult> {
private final List<TLeft> leftRows = new ArrayList<>();
private final List<TRight> rightRows = new ArrayList<>();
private final List<Entry<TKey1, TKey2>> firstOrder;

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.

some of these fields are not obvious, can you please document them?

@zzwqqq zzwqqq Sep 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

some of these fields are not obvious, can you please document them?

Thanks, I've added comments for these fields.

@mihaibudiu

Copy link
Copy Markdown
Contributor

I don't think you can assume that people who review this code have read the papers; try to add enough information to make the code self-explanatory as much as possible.

@zzwqqq

zzwqqq commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

I don't think you can assume that people who review this code have read the papers; try to add enough information to make the code self-explanatory as much as possible.

Thanks for reviewing! I've added a short explanation and an example to walk through the algorithm.

@mihaibudiu mihaibudiu 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.

Why doesn't this work for other types, like DOUBLE?


/** Creates an EnumerableIEJoin.
*
* <p>Use {@link #create} unless you know what you're doing. */

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.

Is this comment still relevant, or is it just a copy-paste from other implementations?
Usually you would write this on a constructor if the other method creates some other important invariants.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. I've removed the comment.

outputRows = RelMdUtil.addEpsilon(outputRows);
}
final double inputRows = leftRows + rightRows;
// IEJoin sorts the union twice, scans it once, then emits the result.

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.

why would you sort something twice?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For l.x < r.x AND l.y > r.y, one sort is by x and the other by y. I've updated the comment to make that clear.

return planner.getCostFactory().makeCost(cost, 0, 0);
}

@Override public Result implement(EnumerableRelImplementor implementor,

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.

A sequence of comments showing the equivalent generated Java code would make this more readable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. I've added examples of the generated Java next to the selectors, comparators, and final ieJoin call.

left.getCluster().getTypeFactory(), leftType, rightType)
&& (SqlTypeUtil.isBoolean(leftType)
|| (SqlTypeUtil.isExactNumeric(leftType)
&& !SqlTypeName.UNSIGNED_TYPES.contains(typeName))

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.

Is this because Java does not support unsigned types natively?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! Ordinary unsigned comparisons fail during code generation because overloads such as SqlFunctions.lt(org.joou.UInteger, org.joou.UInteger) are missing. I'd prefer to address this in a separate change.

RexProgram.create(ieJoin.getRowType(),
rexBuilder.identityProjects(ieJoin.getRowType()), residual,
ieJoin.getRowType(), rexBuilder);
return EnumerableCalc.create(ieJoin, program);

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.

So the left-over conjunctions are applied in a subsequent filter - and yet, this is still more efficient than the original join?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, this rule adds a Calc above IEJoin to check the remaining conditions. IEJoin finds candidates through sorting and bitmap scans. Whether this is faster depends on how many pairs pass the first two conditions. Thanks!

new JavaCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
Util.getDefaultCharset(), Collator.PRIMARY);

@Test void ieJoin() {

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.

It's not easy to review these tests.
You can perhaps add a comment to each explaining why you think the output is correct (i.e., has been validated using a different implementation), or why you think the plan has the produced shape.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've replaced the fixed 600 with a count from a simple Java nested loop over the same data. I've also added comments for the cases.

* entries are sorted by each key. A right entry satisfies predicate 1 exactly
* when it follows a left entry in {@code firstOrder}, and predicate 2 exactly
* when it precedes that left entry in {@code secondOrder}. Sort directions and
* tie-breaking depend on the operators; see {@link #entryComparator}.

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.

I see, so you sort twice, once by each compared field?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, that's what I meant: one sort for each comparison. Each has its own sort direction and ordering of equal keys.

* entries are sorted by each key. A right entry satisfies predicate 1 exactly
* when it follows a left entry in {@code firstOrder}, and predicate 2 exactly
* when it precedes that left entry in {@code secondOrder}. Sort directions and
* tie-breaking depend on the operators; see {@link #entryComparator}.

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.

what happens if the same compared field is used in both comparisons?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! Both comparisons can use the same field. The current implementation still sorts twice and doesn't reuse the ordering. I've added tests for this case.

@sonarqubecloud

Copy link
Copy Markdown

@zzwqqq

zzwqqq commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Why doesn't this work for other types, like DOUBLE?

Thanks! I haven't checked every excluded type yet. For DOUBLE, sorting and SQL comparisons disagree on NaN and signed zero. Unsigned comparisons also fail during code generation. Those are the issues I've confirmed so far.

@mihaibudiu mihaibudiu added the LGTM-will-merge-soon Overall PR looks OK. Only minor things left. label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

LGTM-will-merge-soon Overall PR looks OK. Only minor things left.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants