[CALCITE-1656] Improve cost function in DruidQuery to encourage early column 
pruning (Nishant Bangarwa)

The cost function now includes:
 * Number of fields being queried
 * Type of DruidQuery generated

Close apache/calcite#382


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

Branch: refs/heads/master
Commit: c90fddfb1c600e6c870558778c50ebfbcd5f1799
Parents: 33f9d0f
Author: Nishant <[email protected]>
Authored: Fri Feb 24 21:40:42 2017 +0530
Committer: Julian Hyde <[email protected]>
Committed: Fri Mar 3 10:13:36 2017 -0800

----------------------------------------------------------------------
 .../apache/calcite/rel/metadata/RelMdUtil.java  | 31 +++++++++++++++
 .../apache/calcite/test/RelMetadataTest.java    | 11 ++++++
 .../calcite/adapter/druid/DruidQuery.java       | 27 ++++++++++++-
 .../org/apache/calcite/test/DruidAdapterIT.java | 41 +++++++++++++++++++-
 4 files changed, 108 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/c90fddfb/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java 
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
index 089f551..7b63ac9 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
@@ -44,6 +44,7 @@ import org.apache.calcite.sql.type.ReturnTypes;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.NumberUtil;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 import java.math.BigDecimal;
@@ -719,6 +720,36 @@ public class RelMdUtil {
         * mq.getSelectivity(child, condition);
   }
 
+  /** Returns a point on a line.
+   *
+   * <p>The result is always a value between {@code minY} and {@code maxY},
+   * even if {@code x} is not between {@code minX} and {@code maxX}.
+   *
+   * <p>Examples:<ul>
+   *   <li>{@code linear(0, 0, 10, 100, 200}} returns 100 because 0 is minX
+   *   <li>{@code linear(5, 0, 10, 100, 200}} returns 150 because 5 is
+   *   mid-way between minX and maxX
+   *   <li>{@code linear(5, 0, 10, 100, 200}} returns 160
+   *   <li>{@code linear(10, 0, 10, 100, 200}} returns 200 because 10 is maxX
+   *   <li>{@code linear(-2, 0, 10, 100, 200}} returns 100 because -2 is
+   *   less than minX and is therefore treated as minX
+   *   <li>{@code linear(12, 0, 10, 100, 200}} returns 100 because 12 is
+   *   greater than maxX and is therefore treated as maxX
+   * </ul>
+   */
+  public static double linear(int x, int minX, int maxX, double minY, double
+      maxY) {
+    Preconditions.checkArgument(minX < maxX);
+    Preconditions.checkArgument(minY < maxY);
+    if (x < minX) {
+      return minY;
+    }
+    if (x > maxX) {
+      return maxY;
+    }
+    return minY + (double) (x - minX) / (double) (maxX - minX) * (maxY - minY);
+  }
+
   //~ Inner Classes ----------------------------------------------------------
 
   /** Visitor that walks over a scalar expression and computes the

http://git-wip-us.apache.org/repos/asf/calcite/blob/c90fddfb/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
----------------------------------------------------------------------
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 54e3b5e..234b666 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -60,6 +60,7 @@ import org.apache.calcite.rel.metadata.MetadataHandler;
 import org.apache.calcite.rel.metadata.ReflectiveRelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelColumnOrigin;
 import org.apache.calcite.rel.metadata.RelMdCollation;
+import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
@@ -1470,6 +1471,16 @@ public class RelMetadataTest extends SqlToRelTestBase {
     assertThat(d, is(dist));
   }
 
+  /** Unit test for {@link RelMdUtil#linear(int, int, int, double, double)}. */
+  @Test public void testLinear() {
+    assertThat(RelMdUtil.linear(0, 0, 10, 100, 200), is(100d));
+    assertThat(RelMdUtil.linear(5, 0, 10, 100, 200), is(150d));
+    assertThat(RelMdUtil.linear(6, 0, 10, 100, 200), is(160d));
+    assertThat(RelMdUtil.linear(10, 0, 10, 100, 200), is(200d));
+    assertThat(RelMdUtil.linear(-2, 0, 10, 100, 200), is(100d));
+    assertThat(RelMdUtil.linear(12, 0, 10, 100, 200), is(200d));
+  }
+
   /**
    * Matcher that succeeds for any collection that, when converted to strings
    * and sorted on those strings, matches the given reference string.

http://git-wip-us.apache.org/repos/asf/calcite/blob/c90fddfb/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
----------------------------------------------------------------------
diff --git 
a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java 
b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
index b76652c..17ddd05 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidQuery.java
@@ -44,6 +44,7 @@ import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
@@ -346,7 +347,31 @@ public class DruidQuery extends AbstractRelNode implements 
BindableRel {
 
   @Override public RelOptCost computeSelfCost(RelOptPlanner planner,
       RelMetadataQuery mq) {
-    return Util.last(rels).computeSelfCost(planner, mq).multiplyBy(.1);
+    return Util.last(rels)
+        .computeSelfCost(planner, mq)
+        // Cost increases with the number of fields queried.
+        // A plan returning 100 or more columns will have 2x the cost of a
+        // plan returning 2 columns.
+        // A plan where all extra columns are pruned will be preferred.
+        .multiplyBy(
+            RelMdUtil.linear(querySpec.fieldNames.size(), 2, 100, 1d, 2d))
+        .multiplyBy(getQueryTypeCostMultiplier());
+  }
+
+  private double getQueryTypeCostMultiplier() {
+    // Cost of Select > GroupBy > Timeseries > TopN
+    switch (querySpec.queryType) {
+    case SELECT:
+      return .1;
+    case GROUP_BY:
+      return .08;
+    case TIMESERIES:
+      return .06;
+    case TOP_N:
+      return .04;
+    default:
+      return .2;
+    }
   }
 
   @Override public void register(RelOptPlanner planner) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/c90fddfb/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java
----------------------------------------------------------------------
diff --git a/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java 
b/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java
index 15cf7e0..cd28362 100644
--- a/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java
+++ b/druid/src/test/java/org/apache/calcite/test/DruidAdapterIT.java
@@ -1335,7 +1335,7 @@ public class DruidAdapterIT {
     final String explain = "EnumerableInterpreter\n"
         + "  BindableAggregate(group=[{}], C=[COUNT()])\n"
         + "    BindableFilter(condition=[AND(>=(/INT(Reinterpret($0), 
86400000), 1997-01-01), <(/INT(Reinterpret($0), 86400000), 1998-01-01), 
OR(AND(>=(/INT(Reinterpret($0), 86400000), 1997-04-01), <(/INT(Reinterpret($0), 
86400000), 1997-05-01)), AND(>=(/INT(Reinterpret($0), 86400000), 1997-06-01), 
<(/INT(Reinterpret($0), 86400000), 1997-07-01))))])\n"
-        + "      DruidQuery(table=[[foodmart, foodmart]], 
intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]])";
+        + "      DruidQuery(table=[[foodmart, foodmart]], 
intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], 
projects=[[$0]])";
     sql(sql)
         .explainContains(explain)
         .returnsUnordered("C=13500");
@@ -1368,6 +1368,45 @@ public class DruidAdapterIT {
     sql(sql, WIKI)
         .returnsCount(9);
   }
+
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-1656";>[CALCITE-1656]
+   * Improve cost function in DruidQuery to encourage early column
+   * pruning</a>. */
+  @Test public void testFieldBasedCostColumnPruning() {
+    // A query where filter cannot be pushed to Druid but
+    // the project can still be pushed in order to prune extra columns.
+    String sql = "select \"countryName\", floor(\"time\" to DAY),\n"
+        + "  cast(count(*) as integer) as c\n"
+        + "from \"wiki\"\n"
+        + "where floor(\"time\" to DAY) >= '1997-01-01 00:00:00'\n"
+        + "and floor(\"time\" to DAY) < '1997-09-01 00:00:00'\n"
+        + "group by \"countryName\", floor(\"time\" TO DAY)\n"
+        + "order by c limit 5";
+
+    String plan = "BindableProject(countryName=[$0], EXPR$1=[$1], 
C=[CAST($2):INTEGER NOT NULL])\n"
+        + "    BindableSort(sort0=[$2], dir0=[ASC], fetch=[5])\n"
+        + "      BindableAggregate(group=[{0, 1}], agg#0=[COUNT()])\n"
+        + "        BindableProject(countryName=[$1], EXPR$1=[FLOOR($0, 
FLAG(DAY))])\n"
+        + "          BindableFilter(condition=[AND(>=(FLOOR($0, FLAG(DAY)), 
1997-01-01 00:00:00), <(FLOOR($0, FLAG(DAY)), 1997-09-01 00:00:00))])\n"
+        + "            DruidQuery(table=[[wiki, wiki]], 
intervals=[[1900-01-09T00:00:00.000/2992-01-10T00:00:00.000]], projects=[[$0, 
$5]])";
+
+    // NOTE: Druid query only has countryName as the dimension
+    // being queried after project is pushed to druid query.
+    String druidQuery = "{\"queryType\":\"select\","
+        + "\"dataSource\":\"wikiticker\","
+        + "\"descending\":false,"
+        + 
"\"intervals\":[\"1900-01-09T00:00:00.000/2992-01-10T00:00:00.000\"],"
+        + "\"dimensions\":[\"countryName\"],"
+        + "\"metrics\":[],"
+        + "\"granularity\":\"all\","
+        + "\"pagingSpec\":{\"threshold\":16384,\"fromNext\":true},"
+        + "\"context\":{\"druid.query.fetch\":false}}";
+    sql(sql, WIKI).explainContains(plan);
+    sql(sql, WIKI).queryContains(druidChecker(druidQuery));
+  }
+
 }
 
 // End DruidAdapterIT.java

Reply via email to