You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2021/03/28 04:13:18 UTC

[calcite] 02/02: [CALCITE-4522] CPU cost of Sort should be lower if sort keys are empty (huangqixiang)

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit f4a5512caeaf087147fec82b7d98ea231b134036
Author: huangqixiang <hu...@kuaishou.mail>
AuthorDate: Wed Mar 10 21:08:25 2021 +0800

    [CALCITE-4522] CPU cost of Sort should be lower if sort keys are empty (huangqixiang)
    
    The model is as follows:
    * If fetch is zero, CPU cost is zero; otherwise,
    * if the sort keys are empty, we don't need to sort, only step over
      the rows, and therefore the CPU cost is
      min(fetch + offset, inputRowCount) * bytesPerRow; otherwise
    * we need to read and sort inputRowCount rows, with at most
      min(fetch + offset, inputRowCount) of them in the sort data
      structure at a time, giving a CPU cost of
      inputRowCount * log(min(fetch + offset, inputRowCount)) * bytesPerRow.
    
    The cost model factors in row width via bytesPerRow, because
    sorts need to move rows around, not just compare them; by making the cost
    higher if rows are wider, we discourage pushing a Project through a Sort.
    We assume that each field is 4 bytes, and we add 3 'virtual fields' to
    represent the per-row overhead. Thus a 1-field row is (3 + 1) * 4 = 16
    bytes; a 5-field row is (3 + 5) * 4 = 32 bytes.
    
    The cost model does not consider a 5-field sort to be more expensive
    than, say, a 2-field sort, because both sorts will compare just one field
    most of the time.
    
    Close apache/calcite#2363
---
 .../adapter/enumerable/EnumerableLimitSort.java    |  32 -----
 .../java/org/apache/calcite/rel/core/Sort.java     |  71 +++++++++-
 .../calcite/rel/metadata/BuiltInMetadata.java      |   1 +
 .../main/java/org/apache/calcite/util/Util.java    |   9 ++
 .../java/org/apache/calcite/test/JdbcTest.java     |   8 +-
 .../org/apache/calcite/test/RelMetadataTest.java   | 151 +++++++++++++++++----
 .../apache/calcite/adapter/tpcds/TpcdsTest.java    |   6 +-
 7 files changed, 209 insertions(+), 69 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
index 28f02a4..0ae9723 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
@@ -20,15 +20,10 @@ import org.apache.calcite.linq4j.tree.BlockBuilder;
 import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.linq4j.tree.Expressions;
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptCost;
-import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Sort;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rex.RexDynamicParam;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.BuiltInMethod;
 import org.apache.calcite.util.Pair;
@@ -128,31 +123,4 @@ public class EnumerableLimitSort extends Sort implements EnumerableRel {
             )));
     return implementor.result(physType, builder.toBlock());
   }
-
-  @Override public @Nullable RelOptCost computeSelfCost(RelOptPlanner planner,
-      RelMetadataQuery mq) {
-    final double rowCount = mq.getRowCount(this.input).doubleValue();
-    double toSort = getValue(this.fetch, rowCount);
-    if (this.offset != null) {
-      toSort += getValue(this.offset, rowCount);
-    }
-    // we need to sort at most rowCount rows
-    toSort = Math.min(rowCount, toSort);
-
-    // we need to process rowCount rows, and for every row
-    // we search the key in a TreeMap with at most toSort entries
-    final double lookup = Math.max(1., Math.log(toSort));
-    final double bytesPerRow = this.getRowType().getFieldCount() * 4.;
-    final double cpu = (rowCount * lookup) * bytesPerRow;
-
-    RelOptCost cost = planner.getCostFactory().makeCost(rowCount, cpu, 0);
-    return cost;
-  }
-
-  private static double getValue(@Nullable RexNode r, double defaultValue) {
-    if (r == null || r instanceof RexDynamicParam) {
-      return defaultValue;
-    }
-    return RexLiteral.intValue(r);
-  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Sort.java b/core/src/main/java/org/apache/calcite/rel/core/Sort.java
index 0ce2089..1faad39 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Sort.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Sort.java
@@ -29,6 +29,7 @@ import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.util.Util;
@@ -122,14 +123,65 @@ public abstract class Sort extends SingleRel {
   public abstract Sort copy(RelTraitSet traitSet, RelNode newInput,
       RelCollation newCollation, @Nullable RexNode offset, @Nullable RexNode fetch);
 
+  /** {@inheritDoc}
+   *
+   * <p>The CPU cost of a Sort has three main cases:
+   *
+   * <ul>
+   * <li>If {@code fetch} is zero, CPU cost is zero; otherwise,
+   *
+   * <li>if the sort keys are empty, we don't need to sort, only step over
+   * the rows, and therefore the CPU cost is
+   * {@code min(fetch + offset, inputRowCount) * bytesPerRow}; otherwise
+   *
+   * <li>we need to read and sort {@code inputRowCount} rows, with at most
+   * {@code min(fetch + offset, inputRowCount)} of them in the sort data
+   * structure at a time, giving a CPU cost of {@code inputRowCount *
+   * log(min(fetch + offset, inputRowCount)) * bytesPerRow}.
+   * </ul>
+   *
+   * <p>The cost model factors in row width via {@code bytesPerRow}, because
+   * sorts need to move rows around, not just compare them; by making the cost
+   * higher if rows are wider, we discourage pushing a Project through a Sort.
+   * We assume that each field is 4 bytes, and we add 3 'virtual fields' to
+   * represent the per-row overhead. Thus a 1-field row is (3 + 1) * 4 = 16
+   * bytes; a 5-field row is (3 + 5) * 4 = 32 bytes.
+   *
+   * <p>The cost model does not consider a 5-field sort to be more expensive
+   * than, say, a 2-field sort, because both sorts will compare just one field
+   * most of the time. */
   @Override public @Nullable RelOptCost computeSelfCost(RelOptPlanner planner,
       RelMetadataQuery mq) {
-    // Higher cost if rows are wider discourages pushing a project through a
-    // sort.
-    final double rowCount = mq.getRowCount(this);
-    final double bytesPerRow = getRowType().getFieldCount() * 4;
-    final double cpu = Util.nLogN(rowCount) * bytesPerRow;
-    return planner.getCostFactory().makeCost(rowCount, cpu, 0);
+    final double offsetValue = Util.first(doubleValue(offset), 0d);
+    assert offsetValue >= 0 : "offset should not be negative:" + offsetValue;
+
+    final double inCount = mq.getRowCount(input);
+    @Nullable Double fetchValue = doubleValue(fetch);
+    final double readCount;
+    if (fetchValue == null) {
+      readCount = inCount;
+    } else if (fetchValue <= 0) {
+      // Case 1. Read zero rows from input, therefore CPU cost is zero.
+      return planner.getCostFactory().makeCost(inCount, 0, 0);
+    } else {
+      readCount = Math.min(inCount, offsetValue + fetchValue);
+    }
+
+    final double bytesPerRow = (3 + getRowType().getFieldCount()) * 4;
+
+    final double cpu;
+    if (collation.getFieldCollations().isEmpty()) {
+      // Case 2. If sort keys are empty, CPU cost is cheaper because we are just
+      // stepping over the first "readCount" rows, rather than sorting all
+      // "inCount" them. (Presumably we are applying FETCH and/or OFFSET,
+      // otherwise this Sort is a no-op.)
+      cpu = readCount * bytesPerRow;
+    } else {
+      // Case 3. Read and sort all "inCount" rows, keeping "readCount" in the
+      // sort data structure at a time.
+      cpu = Util.nLogM(inCount, readCount) * bytesPerRow;
+    }
+    return planner.getCostFactory().makeCost(readCount, cpu, 0);
   }
 
   @Override public RelNode accept(RexShuttle shuttle) {
@@ -193,4 +245,11 @@ public abstract class Sort extends SingleRel {
     pw.itemIf("fetch", fetch, fetch != null);
     return pw;
   }
+
+  /** Returns the double value of a node if it is a literal, otherwise null. */
+  private static @Nullable Double doubleValue(@Nullable RexNode r) {
+    return r instanceof RexLiteral
+        ? ((RexLiteral) r).getValueAs(Double.class)
+        : null;
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java b/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
index 92e242a..614cc15 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/BuiltInMetadata.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.rel.metadata;
 
 import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.volcano.VolcanoPlanner;
 import org.apache.calcite.rel.RelCollation;
diff --git a/core/src/main/java/org/apache/calcite/util/Util.java b/core/src/main/java/org/apache/calcite/util/Util.java
index 15b2fd4..5f77a4d 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -370,6 +370,15 @@ public class Util {
   }
 
   /**
+   * Computes <code>nlog(m)</code> using the natural logarithm (or <code>
+   * n</code> if <code>m &lt; {@link Math#E}</code>, so the result is never
+   * negative.
+   */
+  public static double nLogM(double n, double m) {
+    return (m < Math.E) ? n : (n * Math.log(m));
+  }
+
+  /**
    * Prints an object using reflection. We can handle <code>null</code>;
    * arrays of objects and primitive values; for regular objects, we print all
    * public fields.
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 45b0ab0..96b2829 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -1162,10 +1162,10 @@ public class JdbcTest {
             + "and p.\"brand_name\" = 'Washington'")
         .explainMatches("including all attributes ",
             CalciteAssert.checkMaskedResultContains(""
-                + "EnumerableMergeJoin(condition=[=($0, $38)], joinType=[inner]): rowcount = 7.050660528307499E8, cumulative cost = {7.656040129282498E8 rows, 5.0023949296644424E10 cpu, 0.0 io}\n"
-                + "  EnumerableSort(sort0=[$0], dir0=[ASC]): rowcount = 2.0087351932499997E7, cumulative cost = {4.044858016499999E7 rows, 5.0023896255644424E10 cpu, 0.0 io}\n"
-                + "    EnumerableMergeJoin(condition=[=($2, $8)], joinType=[inner]): rowcount = 2.0087351932499997E7, cumulative cost = {2.0361228232499994E7 rows, 3.232400376004586E7 cpu, 0.0 io}\n"
-                + "      EnumerableSort(sort0=[$2], dir0=[ASC]): rowcount = 86837.0, cumulative cost = {173674.0 rows, 3.168658076004586E7 cpu, 0.0 io}\n"
+                + "EnumerableMergeJoin(condition=[=($0, $38)], joinType=[inner]): rowcount = 7.050660528307499E8, cumulative cost = {7.656040129282498E8 rows, 5.408916992330521E10 cpu, 0.0 io}\n"
+                + "  EnumerableSort(sort0=[$0], dir0=[ASC]): rowcount = 2.0087351932499997E7, cumulative cost = {4.044858016499999E7 rows, 5.408911688230521E10 cpu, 0.0 io}\n"
+                + "    EnumerableMergeJoin(condition=[=($2, $8)], joinType=[inner]): rowcount = 2.0087351932499997E7, cumulative cost = {2.0361228232499994E7 rows, 4.4173907295063056E7 cpu, 0.0 io}\n"
+                + "      EnumerableSort(sort0=[$2], dir0=[ASC]): rowcount = 86837.0, cumulative cost = {173674.0 rows, 4.3536484295063056E7 cpu, 0.0 io}\n"
                 + "        EnumerableTableScan(table=[[foodmart2, sales_fact_1997]]): rowcount = 86837.0, cumulative cost = {86837.0 rows, 86838.0 cpu, 0.0 io}\n"
                 + "      EnumerableCalc(expr#0..28=[{inputs}], expr#29=['San Francisco':VARCHAR(30)], expr#30=[=($t9, $t29)], proj#0..28=[{exprs}], $condition=[$t30]): rowcount = 1542.1499999999999, cumulative cost = {11823.15 rows, 637423.0 cpu, 0.0 io}\n"
                 + "        EnumerableTableScan(table=[[foodmart2, customer]]): rowcount = 10281.0, cumulative cost = {10281.0 rows, 10282.0 cpu, 0.0 io}\n"
diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
index 00edfcf..7d84dbb 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -20,6 +20,7 @@ import org.apache.calcite.adapter.enumerable.EnumerableMergeJoin;
 import org.apache.calcite.config.CalciteSystemProperty;
 import org.apache.calcite.linq4j.tree.Types;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.RelOptTable;
@@ -28,6 +29,7 @@ import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.plan.hep.HepPlanner;
 import org.apache.calcite.plan.hep.HepProgram;
 import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelCollations;
@@ -91,6 +93,7 @@ import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexTableInputRef;
 import org.apache.calcite.rex.RexTableInputRef.RelTableRef;
 import org.apache.calcite.runtime.SqlFunctions;
+import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
@@ -107,6 +110,7 @@ import org.apache.calcite.util.BuiltInMethod;
 import org.apache.calcite.util.Holder;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
+import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -192,6 +196,11 @@ public class RelMetadataTest extends SqlToRelTestBase {
 
   //~ Methods ----------------------------------------------------------------
 
+  /** Creates a tester. */
+  Sql sql(String sql) {
+    return new Sql(tester, sql);
+  }
+
   // ----------------------------------------------------------------------
   // Tests for getPercentageOriginalRows
   // ----------------------------------------------------------------------
@@ -532,6 +541,33 @@ public class RelMetadataTest extends SqlToRelTestBase {
         false);
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-4192">[CALCITE-4192]
+   * RelMdColumnOrigins get the wrong index of group by columns after RelNode
+   * was optimized by AggregateProjectMergeRule rule</a>. */
+  @Test void testColumnOriginAfterAggProjectMergeRule() {
+    final String sql = "select count(ename), SAL from emp group by SAL";
+    final RelNode rel = tester.convertSqlToRel(sql).rel;
+    final HepProgramBuilder programBuilder = HepProgram.builder();
+    programBuilder.addRuleInstance(CoreRules.AGGREGATE_PROJECT_MERGE);
+    final HepPlanner planner = new HepPlanner(programBuilder.build());
+    planner.setRoot(rel);
+    final RelNode optimizedRel = planner.findBestExp();
+
+    Set<RelColumnOrigin> origins = RelMetadataQuery.instance()
+        .getColumnOrigins(optimizedRel, 1);
+    assertThat(origins.size(), equalTo(1));
+
+    RelColumnOrigin columnOrigin = origins.iterator().next();
+    assertThat(columnOrigin.getOriginColumnOrdinal(), equalTo(5));
+    assertThat(columnOrigin.getOriginTable().getRowType().getFieldNames().get(5),
+        equalTo("SAL"));
+  }
+
+  // ----------------------------------------------------------------------
+  // Tests for getRowCount, getMinRowCount, getMaxRowCount
+  // ----------------------------------------------------------------------
+
   private void checkRowCount(String sql, double expected, double expectedMin,
       double expectedMax) {
     RelNode rel = convertSql(sql);
@@ -644,7 +680,6 @@ public class RelMetadataTest extends SqlToRelTestBase {
         0D, 0D); // 0 * 4
   }
 
-
   @Test void testRowCountJoinEmptyEmpty() {
     final String sql = "select * from (select * from emp limit 0) as emp\n"
         + "inner join (select * from dept limit 0) as dept\n"
@@ -808,6 +843,69 @@ public class RelMetadataTest extends SqlToRelTestBase {
     checkRowCount(sql, 1D, 1D, 1D);
   }
 
+  // ----------------------------------------------------------------------
+  // Tests for computeSelfCost.cpu
+  // ----------------------------------------------------------------------
+
+  @Test void testSortCpuCostOffsetLimit() {
+    final String sql = "select ename, deptno from emp\n"
+        + "order by ename limit 5 offset 5";
+    // inputRows = EMP_SIZE = 14
+    // offset + fetch = 5 + 5 = 10
+    // rowBytes = (2 real columns + 3 virtual columns) * 4 bytes per column
+    //   = 5 * 4
+    //   = 20
+    double cpuCost = Util.nLogM(EMP_SIZE, 10) * 5 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "offset + fetch smaller than table size "
+        + "=> cpu cost should be: inputRows * log(offset + fetch) * rowBytes");
+  }
+
+  @Test void testSortCpuCostLimit() {
+    final String sql = "select ename, deptno from emp limit 10";
+    final double cpuCost = 10 * 5 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "no order by clause "
+        + "=> cpu cost should be min(fetch + offset, inputRows) * rowBytes");
+  }
+
+  @Test void testSortCpuCostOffset() {
+    final String sql = "select ename from emp order by ename offset 10";
+    double cpuCost = Util.nLogM(EMP_SIZE, EMP_SIZE) * 4 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "offset smaller than table size "
+        + "=> cpu cost should be: inputRows * log(inputRows) * rowBytes");
+  }
+
+  @Test void testSortCpuCostLargeOffset() {
+    final String sql = "select ename from emp order by ename offset 100";
+    double cpuCost = Util.nLogM(EMP_SIZE, EMP_SIZE) * 4 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "offset larger than table size "
+        + "=> cpu cost should be: inputRows * log(inputRows) * rowBytes");
+  }
+
+  @Test void testSortCpuCostLimit0() {
+    final String sql = "select ename from emp order by ename limit 0";
+    sql(sql).assertCpuCost(is(0d), "fetch zero => cpu cost should be 0");
+  }
+
+  @Test void testSortCpuCostLimit1() {
+    final String sql = "select ename, deptno from emp\n"
+        + "order by ename limit 1";
+    double cpuCost = EMP_SIZE * 5 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "fetch 1 "
+        + "=> cpu cost should be inputRows * rowBytes");
+  }
+
+  @Test void testSortCpuCostLargeLimit() {
+    final String sql = "select ename, deptno from emp\n"
+        + "order by ename limit 10000";
+    double cpuCost = Util.nLogM(EMP_SIZE, EMP_SIZE) * 5 * 4;
+    sql(sql).assertCpuCost(is(cpuCost), "sort limit exceeds table size "
+        + "=> cpu cost should be dominated by table size");
+  }
+
+  // ----------------------------------------------------------------------
+  // Tests for getSelectivity
+  // ----------------------------------------------------------------------
+
   private void checkFilterSelectivity(
       String sql,
       double expected) {
@@ -3271,6 +3369,8 @@ public class RelMetadataTest extends SqlToRelTestBase {
     });
   }
 
+  //~ Inner classes and interfaces -------------------------------------------
+
   /** Custom metadata interface. */
   public interface ColType extends Metadata {
     Method METHOD = Types.lookupMethod(ColType.class, "getColType", int.class);
@@ -3356,8 +3456,7 @@ public class RelMetadataTest extends SqlToRelTestBase {
   /**
    * Dummy rel node used for testing.
    */
-  private class DummyRelNode extends SingleRel {
-
+  private static class DummyRelNode extends SingleRel {
     /**
      * Creates a <code>DummyRelNode</code>.
      */
@@ -3369,8 +3468,8 @@ public class RelMetadataTest extends SqlToRelTestBase {
   /**
    * Mocked catalog reader for registering table with composite keys.
    */
-  private class CompositeKeysCatalogReader extends MockCatalogReaderSimple {
-
+  private static class CompositeKeysCatalogReader
+      extends MockCatalogReaderSimple {
     CompositeKeysCatalogReader(RelDataTypeFactory typeFactory, boolean caseSensitive) {
       super(typeFactory, caseSensitive);
     }
@@ -3390,26 +3489,30 @@ public class RelMetadataTest extends SqlToRelTestBase {
     }
   }
 
-  /** Test case for
-   * <a href="https://issues.apache.org/jira/browse/CALCITE-4192">[CALCITE-4192]
-   * RelMdColumnOrigins get the wrong index of group by columns after RelNode was optimized by
-   * AggregateProjectMergeRule rule</a>. */
-  @Test void testColumnOriginAfterAggProjectMergeRule() {
-    final String sql = "select count(ename), SAL from emp group by SAL";
-    final RelNode rel = tester.convertSqlToRel(sql).rel;
-    final HepProgramBuilder programBuilder = HepProgram.builder();
-    programBuilder.addRuleInstance(CoreRules.AGGREGATE_PROJECT_MERGE);
-    final HepPlanner planner = new HepPlanner(programBuilder.build());
-    planner.setRoot(rel);
-    final RelNode optimizedRel = planner.findBestExp();
+  /** Parameters for a test. */
+  private static class Sql {
+    private final Tester tester;
+    private final String sql;
 
-    Set<RelColumnOrigin> origins = RelMetadataQuery.instance()
-        .getColumnOrigins(optimizedRel, 1);
-    assertThat(origins.size(), equalTo(1));
+    Sql(Tester tester, String sql) {
+      this.tester = tester;
+      this.sql = sql;
+    }
 
-    RelColumnOrigin columnOrigin = origins.iterator().next();
-    assertThat(columnOrigin.getOriginColumnOrdinal(), equalTo(5));
-    assertThat(columnOrigin.getOriginTable().getRowType().getFieldNames().get(5),
-        equalTo("SAL"));
+    Sql assertCpuCost(Matcher<Double> matcher, String reason) {
+      RelNode rel = convertSql(tester, sql);
+      RelOptCost cost = computeRelSelfCost(rel);
+      assertThat(reason + "\n"
+          + "sql:" + sql + "\n"
+          + "plan:" + RelOptUtil.toString(rel, SqlExplainLevel.ALL_ATTRIBUTES),
+          cost.getCpu(), matcher);
+      return this;
+    }
+
+    private static RelOptCost computeRelSelfCost(RelNode rel) {
+      final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
+      RelOptPlanner planner = new VolcanoPlanner();
+      return rel.computeSelfCost(planner, mq);
+    }
   }
 }
diff --git a/plus/src/test/java/org/apache/calcite/adapter/tpcds/TpcdsTest.java b/plus/src/test/java/org/apache/calcite/adapter/tpcds/TpcdsTest.java
index 188d589..5d04be4 100644
--- a/plus/src/test/java/org/apache/calcite/adapter/tpcds/TpcdsTest.java
+++ b/plus/src/test/java/org/apache/calcite/adapter/tpcds/TpcdsTest.java
@@ -220,9 +220,9 @@ class TpcdsTest {
         .withHook(Hook.PROGRAM, handler(true, 2))
         .explainMatches("including all attributes ",
             CalciteAssert.checkMaskedResultContains(""
-                + "EnumerableCalc(expr#0..9=[{inputs}], expr#10=[/($t4, $t3)], expr#11=[CAST($t10):INTEGER NOT NULL], expr#12=[*($t4, $t4)], expr#13=[/($t12, $t3)], expr#14=[-($t5, $t13)], expr#15=[1], expr#16=[=($t3, $t15)], expr#17=[null:BIGINT], expr#18=[-($t3, $t15)], expr#19=[CASE($t16, $t17, $t18)], expr#20=[/($t14, $t19)], expr#21=[0.5:DECIMAL(2, 1)], expr#22=[POWER($t20, $t21)], expr#23=[CAST($t22):INTEGER NOT NULL], expr#24=[/($t23, $t11)], expr#25=[/($t6, $t3)], expr#26=[CAST($ [...]
-                + "  EnumerableLimit(fetch=[100]): rowcount = 100.0, cumulative cost = {1.2435775409784036E28 rows, 2.555295485909236E30 cpu, 0.0 io}\n"
-                + "    EnumerableSort(sort0=[$0], sort1=[$1], sort2=[$2], dir0=[ASC], dir1=[ASC], dir2=[ASC]): rowcount = 5.434029018852197E26, cumulative cost = {1.2435775409784036E28 rows, 2.555295485909236E30 cpu, 0.0 io}\n"
+                + "EnumerableCalc(expr#0..9=[{inputs}], expr#10=[/($t4, $t3)], expr#11=[CAST($t10):INTEGER NOT NULL], expr#12=[*($t4, $t4)], expr#13=[/($t12, $t3)], expr#14=[-($t5, $t13)], expr#15=[1], expr#16=[=($t3, $t15)], expr#17=[null:BIGINT], expr#18=[-($t3, $t15)], expr#19=[CASE($t16, $t17, $t18)], expr#20=[/($t14, $t19)], expr#21=[0.5:DECIMAL(2, 1)], expr#22=[POWER($t20, $t21)], expr#23=[CAST($t22):INTEGER NOT NULL], expr#24=[/($t23, $t11)], expr#25=[/($t6, $t3)], expr#26=[CAST($ [...]
+                + "  EnumerableLimit(fetch=[100]): rowcount = 100.0, cumulative cost = {1.2435775409784036E28 rows, 2.95671738161514E30 cpu, 0.0 io}\n"
+                + "    EnumerableSort(sort0=[$0], sort1=[$1], sort2=[$2], dir0=[ASC], dir1=[ASC], dir2=[ASC]): rowcount = 5.434029018852197E26, cumulative cost = {1.2435775409784036E28 rows, 2.95671738161514E30 cpu, 0.0 io}\n"
                 + "      EnumerableAggregate(group=[{0, 1, 2}], STORE_SALES_QUANTITYCOUNT=[COUNT()], agg#1=[$SUM0($3)], agg#2=[$SUM0($6)], agg#3=[$SUM0($4)], agg#4=[$SUM0($7)], agg#5=[$SUM0($5)], agg#6=[$SUM0($8)]): rowcount = 5.434029018852197E26, cumulative cost = {1.1892372507898816E28 rows, 1.2172225002228922E30 cpu, 0.0 io}\n"
                 + "        EnumerableCalc(expr#0..211=[{inputs}], expr#212=[*($t89, $t89)], expr#213=[*($t140, $t140)], expr#214=[*($t196, $t196)], I_ITEM_ID=[$t58], I_ITEM_DESC=[$t61], S_STATE=[$t24], SS_QUANTITY=[$t89], SR_RETURN_QUANTITY=[$t140], CS_QUANTITY=[$t196], $f6=[$t212], $f7=[$t213], $f8=[$t214]): rowcount = 5.434029018852197E27, cumulative cost = {1.0873492066864028E28 rows, 1.2172225002228922E30 cpu, 0.0 io}\n"
                 + "          EnumerableHashJoin(condition=[AND(=($82, $133), =($81, $132), =($88, $139))], joinType=[inner]): rowcount = 5.434029018852197E27, cumulative cost = {5.439463048011832E27 rows, 1.7776306E7 cpu, 0.0 io}\n"