[CALCITE-7755] Support IEJoin for inequality joins - #5233
Conversation
| */ | ||
| package org.apache.calcite.linq4j; | ||
|
|
||
| /** Comparison operator applied to the left and right keys of an inequality |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
I assume this computes the expected values. A little JavaDoc would help
There was a problem hiding this comment.
Yes, that’s right. Added Javadoc, thanks.
| actual, is(expected)); | ||
| } | ||
|
|
||
| private static Enumerable<String> ieJoin(List<Point> left, List<Point> right, |
There was a problem hiding this comment.
I assume this computes the result using the new enumerator.
Add JavaDoc
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
This needs some 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; |
There was a problem hiding this comment.
some of these fields are not obvious, can you please document them?
There was a problem hiding this comment.
some of these fields are not obvious, can you please document them?
Thanks, I've added comments for these fields.
|
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
left a comment
There was a problem hiding this comment.
Why doesn't this work for other types, like DOUBLE?
|
|
||
| /** Creates an EnumerableIEJoin. | ||
| * | ||
| * <p>Use {@link #create} unless you know what you're doing. */ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
why would you sort something twice?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
A sequence of comments showing the equivalent generated Java code would make this more readable
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Is this because Java does not support unsigned types natively?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
So the left-over conjunctions are applied in a subsequent filter - and yet, this is still more efficient than the original join?
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}. |
There was a problem hiding this comment.
I see, so you sort twice, once by each compared field?
There was a problem hiding this comment.
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}. |
There was a problem hiding this comment.
what happens if the same compared field is used in both comparisons?
There was a problem hiding this comment.
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.
|
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. |



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.