You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jc...@apache.org on 2015/09/25 21:58:12 UTC

incubator-calcite git commit: [CALCITE-892] Implement SortJoinTransposeRule

Repository: incubator-calcite
Updated Branches:
  refs/heads/master beb3b3bcf -> 0715f5b55


[CALCITE-892] Implement SortJoinTransposeRule

Close apache/incubator-calcite#136


Project: http://git-wip-us.apache.org/repos/asf/incubator-calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-calcite/commit/0715f5b5
Tree: http://git-wip-us.apache.org/repos/asf/incubator-calcite/tree/0715f5b5
Diff: http://git-wip-us.apache.org/repos/asf/incubator-calcite/diff/0715f5b5

Branch: refs/heads/master
Commit: 0715f5b55f363a58e3dd8c20caac0024e19be413
Parents: beb3b3b
Author: Jesus Camacho Rodriguez <jc...@apache.org>
Authored: Fri Sep 25 16:41:44 2015 +0100
Committer: Jesus Camacho Rodriguez <jc...@apache.org>
Committed: Fri Sep 25 20:57:03 2015 +0100

----------------------------------------------------------------------
 .../calcite/prepare/CalcitePrepareImpl.java     |   4 +-
 .../calcite/rel/metadata/RelMdRowCount.java     |  14 +-
 .../rel/rules/SortJoinTransposeRule.java        | 161 +++++++++++++++++++
 .../apache/calcite/test/RelOptRulesTest.java    |  42 +++++
 .../org/apache/calcite/test/RelOptRulesTest.xml |  83 ++++++++++
 core/src/test/resources/sql/join.oq             |  29 ++++
 6 files changed, 331 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
index f761e3c..2a9c3fc 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
@@ -88,6 +88,7 @@ import org.apache.calcite.rel.rules.ProjectMergeRule;
 import org.apache.calcite.rel.rules.ProjectTableScanRule;
 import org.apache.calcite.rel.rules.ProjectWindowTransposeRule;
 import org.apache.calcite.rel.rules.ReduceExpressionsRule;
+import org.apache.calcite.rel.rules.SortJoinTransposeRule;
 import org.apache.calcite.rel.rules.SortProjectTransposeRule;
 import org.apache.calcite.rel.rules.TableScanRule;
 import org.apache.calcite.rel.rules.ValuesReduceRule;
@@ -227,7 +228,8 @@ public class CalcitePrepareImpl implements CalcitePrepare {
           JoinCommuteRule.INSTANCE,
           JoinPushThroughJoinRule.RIGHT,
           JoinPushThroughJoinRule.LEFT,
-          SortProjectTransposeRule.INSTANCE);
+          SortProjectTransposeRule.INSTANCE,
+          SortJoinTransposeRule.INSTANCE);
 
   private static final List<RelOptRule> CONSTANT_REDUCTION_RULES =
       ImmutableList.of(

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
index 568f7ce..cc90395 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
@@ -23,6 +23,7 @@ import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.SemiJoin;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.BuiltInMethod;
 import org.apache.calcite.util.ImmutableBitSet;
@@ -65,7 +66,18 @@ public class RelMdRowCount {
   }
 
   public Double getRowCount(Sort rel) {
-    return RelMetadataQuery.getRowCount(rel.getInput());
+    final Double rowCount = RelMetadataQuery.getRowCount(rel.getInput());
+    if (rowCount != null && rel.fetch != null) {
+      final int offset = rel.offset == null ? 0 : RexLiteral.intValue(rel.offset);
+      final int limit = RexLiteral.intValue(rel.fetch);
+      final Double offsetLimit = new Double(offset + limit);
+      // offsetLimit is smaller than rowCount of the input operator
+      // thus, we return the offsetLimit
+      if (offsetLimit < rowCount) {
+        return offsetLimit;
+      }
+    }
+    return rowCount;
   }
 
   public Double getRowCount(SemiJoin rel) {

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
new file mode 100644
index 0000000..b6d87f5
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.rules;
+
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+
+import com.google.common.collect.ImmutableList;
+
+
+/**
+ * Planner rule that pushes a {@link org.apache.calcite.rel.core.Sort} past a
+ * {@link org.apache.calcite.rel.core.Join}. At the moment, we only consider
+ * left/right outer joins.
+ * However, an extesion for full outer joins for this rule could be envision.
+ * Special attention should be paid to null values for correctness issues.
+ */
+public class SortJoinTransposeRule extends RelOptRule {
+
+  public static final SortJoinTransposeRule INSTANCE =
+      new SortJoinTransposeRule(LogicalSort.class,
+              LogicalJoin.class);
+
+  //~ Constructors -----------------------------------------------------------
+
+  /** Creates a SortJoinTransposeRule. */
+  public SortJoinTransposeRule(Class<? extends Sort> sortClass,
+          Class<? extends Join> joinClass) {
+    super(
+        operand(sortClass,
+            operand(joinClass, any())));
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override
+  public boolean matches(RelOptRuleCall call) {
+    final Sort sort = call.rel(0);
+    final Join join = call.rel(1);
+
+    // 1) If join is not a left or right outer, we bail out
+    // 2) If sort does not consist only of a limit operation,
+    // or any sort column is not part of the input where the
+    // sort is pushed, we bail out
+    if (join.getJoinType() == JoinRelType.LEFT) {
+      if (sort.getCollation() != RelCollations.EMPTY) {
+        for (RelFieldCollation relFieldCollation
+                : sort.getCollation().getFieldCollations()) {
+          if (relFieldCollation.getFieldIndex()
+                  >= join.getLeft().getRowType().getFieldCount()) {
+            return false;
+          }
+        }
+      }
+    } else if (join.getJoinType() == JoinRelType.RIGHT) {
+      if (sort.getCollation() != RelCollations.EMPTY) {
+        for (RelFieldCollation relFieldCollation
+                : sort.getCollation().getFieldCollations()) {
+          if (relFieldCollation.getFieldIndex()
+                  < join.getLeft().getRowType().getFieldCount()) {
+            return false;
+          }
+        }
+      }
+    } else {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public void onMatch(RelOptRuleCall call) {
+    final Sort sort = call.rel(0);
+    final Join join = call.rel(1);
+
+    // We create a new sort operator on the corresponding input
+    final RelNode newLeftInput;
+    final RelNode newRightInput;
+    if (join.getJoinType() == JoinRelType.LEFT) {
+      // If the input is already sorted and we are not reducing the number of tuples,
+      // we bail out
+      if (checkInputForCollationAndLimit(join.getLeft(), sort.getCollation(),
+          sort.offset, sort.fetch)) {
+        return;
+      }
+      newLeftInput = sort.copy(sort.getTraitSet(), join.getLeft(), sort.getCollation(),
+              sort.offset, sort.fetch);
+      newRightInput = join.getRight();
+    } else {
+      final RelCollation rightCollation = RelCollations.shift(
+          sort.getCollation(), -join.getLeft().getRowType().getFieldCount());
+      // If the input is already sorted and we are not reducing the number of tuples,
+      // we bail out
+      if (checkInputForCollationAndLimit(join.getRight(), rightCollation,
+          sort.offset, sort.fetch)) {
+        return;
+      }
+      newLeftInput = join.getLeft();
+      newRightInput = sort.copy(sort.getTraitSet(), join.getRight(), rightCollation,
+              sort.offset, sort.fetch);
+    }
+    // We copy the join and the top sort operator
+    final RelNode joinCopy = join.copy(join.getTraitSet(), join.getCondition(), newLeftInput,
+            newRightInput, join.getJoinType(), join.isSemiJoinDone());
+    final RelNode sortCopy = sort.copy(sort.getTraitSet(), joinCopy, sort.getCollation(),
+            sort.offset, sort.fetch);
+
+    call.transformTo(sortCopy);
+  }
+
+  /* Checks if an input is already sorted and has less rows than
+   * the sum of offset and limit */
+  private boolean checkInputForCollationAndLimit(RelNode input,
+      RelCollation collation, RexNode offset, RexNode fetch) {
+    // Check if the input is already sorted
+    ImmutableList<RelCollation> inputCollation =
+        RelMetadataQuery.collations(input);
+    final boolean alreadySorted = RelCollations.equal(
+        ImmutableList.of(collation), inputCollation);
+    // Check if we are not reducing the number of tuples
+    boolean alreadySmaller = true;
+    final Double rowCount = RelMetadataQuery.getRowCount(input);
+    if (rowCount != null && fetch != null) {
+      final int offsetVal = offset == null ? 0 : RexLiteral.intValue(offset);
+      final int limit = RexLiteral.intValue(fetch);
+      final Double offsetLimit = new Double(offsetVal + limit);
+      if (offsetLimit < rowCount) {
+        alreadySmaller = false;
+      }
+    }
+    return alreadySorted && alreadySmaller;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 34ff371..7ba514d 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -72,6 +72,8 @@ import org.apache.calcite.rel.rules.SemiJoinJoinTransposeRule;
 import org.apache.calcite.rel.rules.SemiJoinProjectTransposeRule;
 import org.apache.calcite.rel.rules.SemiJoinRemoveRule;
 import org.apache.calcite.rel.rules.SemiJoinRule;
+import org.apache.calcite.rel.rules.SortJoinTransposeRule;
+import org.apache.calcite.rel.rules.SortProjectTransposeRule;
 import org.apache.calcite.rel.rules.TableScanRule;
 import org.apache.calcite.rel.rules.UnionToDistinctRule;
 import org.apache.calcite.rel.rules.ValuesReduceRule;
@@ -1826,6 +1828,46 @@ public class RelOptRulesTest extends RelOptTestBase {
             + " where d.deptno + 10 = e.deptno * 2");
   }
 
+  @Test public void testSortJoinTranspose1() {
+    final HepProgram preProgram = new HepProgramBuilder()
+        .addRuleInstance(SortProjectTransposeRule.INSTANCE)
+        .build();
+    final HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(SortJoinTransposeRule.INSTANCE)
+        .build();
+    final String sql = "select * from sales.emp e left join (\n"
+            + "select * from sales.dept d) using (deptno)\n"
+            + "order by sal limit 10";
+    checkPlanning(tester, preProgram, new HepPlanner(program), sql);
+  }
+
+  @Test public void testSortJoinTranspose2() {
+    final HepProgram preProgram = new HepProgramBuilder()
+        .addRuleInstance(SortProjectTransposeRule.INSTANCE)
+        .build();
+    final HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(SortJoinTransposeRule.INSTANCE)
+        .build();
+    final String sql = "select * from sales.emp e right join (\n"
+            + "select * from sales.dept d) using (deptno)\n"
+            + "order by name";
+    checkPlanning(tester, preProgram, new HepPlanner(program), sql);
+  }
+
+  @Test public void testSortJoinTranspose3() {
+    final HepProgram preProgram = new HepProgramBuilder()
+        .addRuleInstance(SortProjectTransposeRule.INSTANCE)
+        .build();
+    final HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(SortJoinTransposeRule.INSTANCE)
+        .build();
+    // This one cannot be pushed down
+    final String sql = "select * from sales.emp left join (\n"
+            + "select * from sales.dept) using (deptno)\n"
+            + "order by sal, name limit 10";
+    checkPlanning(tester, preProgram, new HepPlanner(program), sql);
+  }
+
 }
 
 // End RelOptRulesTest.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
----------------------------------------------------------------------
diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 07cf79a..0d01e9a 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -4039,4 +4039,87 @@ LogicalProject($f0=[$3], $f1=[$1], $f2=[$2])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testSortJoinTranspose1">
+        <Resource name="sql">
+            <![CDATA[select * from sales.emp e left join (
+select * from sales.dept d) using (deptno)
+order by sal limit 10]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$5], dir0=[ASC], fetch=[10])
+    LogicalJoin(condition=[=($7, $9)], joinType=[left])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(DEPTNO=[$0], NAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$5], dir0=[ASC], fetch=[10])
+    LogicalJoin(condition=[=($7, $9)], joinType=[left])
+      LogicalSort(sort0=[$5], dir0=[ASC], fetch=[10])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(DEPTNO=[$0], NAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testSortJoinTranspose2">
+        <Resource name="sql">
+            <![CDATA[select * from sales.emp e right join (
+select * from sales.dept d) using (deptno)
+order by name]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$10], dir0=[ASC])
+    LogicalJoin(condition=[=($7, $9)], joinType=[right])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(DEPTNO=[$0], NAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$10], dir0=[ASC])
+    LogicalJoin(condition=[=($7, $9)], joinType=[right])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalSort(sort0=[$1], dir0=[ASC])
+        LogicalProject(DEPTNO=[$0], NAME=[$1])
+          LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testSortJoinTranspose3">
+        <Resource name="sql">
+            <![CDATA[select * from sales.emp left join (
+select * from sales.dept) using (deptno)
+order by sal, name limit 10]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$5], sort1=[$10], dir0=[ASC], dir1=[ASC], fetch=[10])
+    LogicalJoin(condition=[=($7, $9)], joinType=[left])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(DEPTNO=[$0], NAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalSort(sort0=[$5], sort1=[$10], dir0=[ASC], dir1=[ASC], fetch=[10])
+    LogicalJoin(condition=[=($7, $9)], joinType=[left])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(DEPTNO=[$0], NAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+        </Resource>
+    </TestCase>
 </Root>

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/0715f5b5/core/src/test/resources/sql/join.oq
----------------------------------------------------------------------
diff --git a/core/src/test/resources/sql/join.oq b/core/src/test/resources/sql/join.oq
index 0c69e2a..43cdad1 100644
--- a/core/src/test/resources/sql/join.oq
+++ b/core/src/test/resources/sql/join.oq
@@ -256,4 +256,33 @@ using (deptno);
 
 !ok
 
+### [CALCITE-892] Implement SortJoinTransposeRule
+### Limit and sort are different Enumerable operators, thus it does not apply
+select * from "scott".emp e left join (
+  select * from "scott".dept d) using (deptno)
+order by sal limit 10;
++-------+--------+----------+------+------------+---------+---------+--------+---------+------------+----------+
+| EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO | DEPTNO0 | DNAME      | LOC      |
++-------+--------+----------+------+------------+---------+---------+--------+---------+------------+----------+
+|  7369 | SMITH  | CLERK    | 7902 | 1980-12-17 |  800.00 |         |     20 |      20 | RESEARCH   | DALLAS   |
+|  7900 | JAMES  | CLERK    | 7698 | 1981-12-03 |  950.00 |         |     30 |      30 | SALES      | CHICAGO  |
+|  7876 | ADAMS  | CLERK    | 7788 | 1987-05-23 | 1100.00 |         |     20 |      20 | RESEARCH   | DALLAS   |
+|  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |      30 | SALES      | CHICAGO  |
+|  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |      30 | SALES      | CHICAGO  |
+|  7934 | MILLER | CLERK    | 7782 | 1982-01-23 | 1300.00 |         |     10 |      10 | ACCOUNTING | NEW YORK |
+|  7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 |    0.00 |     30 |      30 | SALES      | CHICAGO  |
+|  7499 | ALLEN  | SALESMAN | 7698 | 1981-02-20 | 1600.00 |  300.00 |     30 |      30 | SALES      | CHICAGO  |
+|  7782 | CLARK  | MANAGER  | 7839 | 1981-06-09 | 2450.00 |         |     10 |      10 | ACCOUNTING | NEW YORK |
+|  7698 | BLAKE  | MANAGER  | 7839 | 1981-01-05 | 2850.00 |         |     30 |      30 | SALES      | CHICAGO  |
++-------+--------+----------+------+------------+---------+---------+--------+---------+------------+----------+
+(10 rows)
+
+!ok
+EnumerableLimit(fetch=[10])
+  EnumerableSort(sort0=[$5], dir0=[ASC])
+    EnumerableJoin(condition=[=($7, $8)], joinType=[left])
+      EnumerableTableScan(table=[[scott, EMP]])
+      EnumerableTableScan(table=[[scott, DEPT]])
+!plan
+
 # End join.oq