This is an automated email from the ASF dual-hosted git repository.
gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 0a8bf83 Bad plan for table-lookup-lookup join with filter on first
lookup and outer limit (#9773)
0a8bf83 is described below
commit 0a8bf83bc589fb899a7d96e426682708c636f0eb
Author: Maytas Monsereenusorn <[email protected]>
AuthorDate: Thu May 14 13:56:40 2020 -1000
Bad plan for table-lookup-lookup join with filter on first lookup and outer
limit (#9773)
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* Bad plan for table-lookup-lookup join with filter on first lookup and
outer limit
* address comments
* address comments
* fix checkstyle
* address comments
* address comments
---
docs/querying/datasource.md | 5 +
docs/querying/sql.md | 11 +-
.../apache/druid/sql/calcite/planner/Rules.java | 6 +-
.../druid/sql/calcite/rule/DruidJoinRule.java | 21 +-
.../rule/FilterJoinExcludePushToChildRule.java | 295 +++++++++++++++
.../apache/druid/sql/calcite/CalciteQueryTest.java | 403 ++++++++++++++++++++-
6 files changed, 730 insertions(+), 11 deletions(-)
diff --git a/docs/querying/datasource.md b/docs/querying/datasource.md
index c522f5f..66cc905 100644
--- a/docs/querying/datasource.md
+++ b/docs/querying/datasource.md
@@ -332,12 +332,17 @@ perform best if `d.field` is a string.
4. As of Druid {{DRUIDVERSION}}, the join operator must evaluate the condition
for each row. In the future, we expect
to implement both early and deferred condition evaluation, which we expect to
improve performance considerably for
common use cases.
+5. Currently, Druid does not support pushing down predicates (condition and
filter) past a Join (i.e. into
+Join's children). Druid only supports pushing predicates into the join if they
originated from
+above the join. Hence, the location of predicates and filters in your Druid
SQL is very important.
+Also, as a result of this, comma joins should be avoided.
#### Future work for joins
Joins are an area of active development in Druid. The following features are
missing today but may appear in
future versions:
+- Reordering of predicates and filters (pushing up and/or pushing down) to get
the most performant plan.
- Preloaded dimension tables that are wider than lookups (i.e. supporting more
than a single key and single value).
- RIGHT OUTER and FULL OUTER joins. Currently, they are partially implemented.
Queries will run but results will not
always be correct.
diff --git a/docs/querying/sql.md b/docs/querying/sql.md
index d7d2c78..80f8a39 100644
--- a/docs/querying/sql.md
+++ b/docs/querying/sql.md
@@ -528,13 +528,18 @@ the way you write the filter.
subqueries generated by conditions on mismatched types, and implicit
subqueries generated by conditions that use
expressions to refer to the right-hand side.
-3. Read through the [Query execution](query-execution.md) page to understand
how various types of native queries
+3. Currently, Druid does not support pushing down predicates (condition and
filter) past a Join (i.e. into
+Join's children). Druid only supports pushing predicates into the join if they
originated from
+above the join. Hence, the location of predicates and filters in your Druid
SQL is very important.
+Also, as a result of this, comma joins should be avoided.
+
+4. Read through the [Query execution](query-execution.md) page to understand
how various types of native queries
will be executed.
-4. Be careful when interpreting EXPLAIN PLAN output, and use request logging
if in doubt. Request logs will show the
+5. Be careful when interpreting EXPLAIN PLAN output, and use request logging
if in doubt. Request logs will show the
exact native query that was run. See the [next
section](#interpreting-explain-plan-output) for more details.
-5. If you encounter a query that could be planned better, feel free to
+6. If you encounter a query that could be planned better, feel free to
[raise an issue on GitHub](https://github.com/apache/druid/issues/new/choose).
A reproducible test case is always
appreciated.
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/Rules.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/Rules.java
index 3ac79aa..acc27c7 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/Rules.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/Rules.java
@@ -42,7 +42,6 @@ import org.apache.calcite.rel.rules.AggregateValuesRule;
import org.apache.calcite.rel.rules.CalcRemoveRule;
import org.apache.calcite.rel.rules.ExchangeRemoveConstantKeysRule;
import org.apache.calcite.rel.rules.FilterAggregateTransposeRule;
-import org.apache.calcite.rel.rules.FilterJoinRule;
import org.apache.calcite.rel.rules.FilterMergeRule;
import org.apache.calcite.rel.rules.FilterProjectTransposeRule;
import org.apache.calcite.rel.rules.FilterTableScanRule;
@@ -75,6 +74,7 @@ import org.apache.druid.sql.calcite.rel.QueryMaker;
import org.apache.druid.sql.calcite.rule.DruidRelToDruidRule;
import org.apache.druid.sql.calcite.rule.DruidRules;
import org.apache.druid.sql.calcite.rule.DruidTableScanRule;
+import org.apache.druid.sql.calcite.rule.FilterJoinExcludePushToChildRule;
import org.apache.druid.sql.calcite.rule.ProjectAggregatePruneUnusedCallRule;
import org.apache.druid.sql.calcite.rule.SortCollapseRule;
@@ -102,7 +102,6 @@ public class Rules
FilterTableScanRule.INSTANCE,
ProjectFilterTransposeRule.INSTANCE,
FilterProjectTransposeRule.INSTANCE,
- FilterJoinRule.FILTER_ON_JOIN,
JoinPushExpressionsRule.INSTANCE,
AggregateCaseToFilterRule.INSTANCE,
FilterAggregateTransposeRule.INSTANCE,
@@ -169,8 +168,6 @@ public class Rules
// 3) JoinCommuteRule (we don't support reordering joins yet).
private static final List<RelOptRule> ABSTRACT_RELATIONAL_RULES =
ImmutableList.of(
- FilterJoinRule.FILTER_ON_JOIN,
- FilterJoinRule.JOIN,
AbstractConverter.ExpandConversionRule.INSTANCE,
AggregateRemoveRule.INSTANCE,
UnionToDistinctRule.INSTANCE,
@@ -243,6 +240,7 @@ public class Rules
}
// Rules that we wrote.
+
rules.add(FilterJoinExcludePushToChildRule.FILTER_ON_JOIN_EXCLUDE_PUSH_TO_CHILD);
rules.add(SortCollapseRule.instance());
rules.add(ProjectAggregatePruneUnusedCallRule.instance());
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidJoinRule.java
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidJoinRule.java
index 380d1e9..cb7fc05 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidJoinRule.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidJoinRule.java
@@ -193,9 +193,24 @@ public class DruidJoinRule extends RelOptRule
final int numLeftFields = leftRowType.getFieldCount();
for (RexNode subCondition : subConditions) {
- if (subCondition.isA(SqlKind.LITERAL)) {
- // Literals are always OK.
- literalSubConditions.add((RexLiteral) subCondition);
+ if (RexUtil.isLiteral(subCondition, true)) {
+ if (subCondition.isA(SqlKind.CAST)) {
+ // This is CAST(literal) which is always OK.
+ // We know that this is CAST(literal) as it passed the check from
RexUtil.isLiteral
+ RexCall call = (RexCall) subCondition;
+ // We have to verify the types of the cast here, because if the
underlying literal and the cast output type
+ // are different, then skipping the cast might change the meaning of
the subcondition.
+ if
(call.getType().getSqlTypeName().equals(call.getOperands().get(0).getType().getSqlTypeName()))
{
+ // If the types are the same, unwrap the cast and use the
underlying literal.
+ literalSubConditions.add((RexLiteral) call.getOperands().get(0));
+ } else {
+ // If the types are not the same, return Optional.empty()
indicating the condition is not supported.
+ return Optional.empty();
+ }
+ } else {
+ // Literals are always OK.
+ literalSubConditions.add((RexLiteral) subCondition);
+ }
continue;
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterJoinExcludePushToChildRule.java
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterJoinExcludePushToChildRule.java
new file mode 100644
index 0000000..ca9ed46
--- /dev/null
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/FilterJoinExcludePushToChildRule.java
@@ -0,0 +1,295 @@
+/*
+ * 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.druid.sql.calcite.rule;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.rules.FilterJoinRule;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.tools.RelBuilderFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is a copy (with modification) of {@link FilterJoinRule}.
Specifically, this class contains a
+ * subset of code from {@link FilterJoinRule} for the codepath involving
{@link FilterJoinRule#FILTER_ON_JOIN}
+ * Everything has been keep as-is from {@link FilterJoinRule} except for the
modification
+ * of {@link #classifyFilters(List, JoinRelType, boolean, List)} method called
in the
+ * {@link #perform(RelOptRuleCall, Filter, Join)} method of this class.
+ * The {@link #classifyFilters(List, JoinRelType, boolean, List)} method is
based of {@link RelOptUtil#classifyFilters}.
+ * The difference is that the modfied method use in thsi class will not not
push filters to the children.
+ * Hence, filters will either stay where they are or are pushed to the join
(if they originated from above the join).
+ *
+ * This modification is needed due to the bug described in
https://github.com/apache/druid/pull/9773
+ * This class and it's modification can be removed, switching back to the
default Rule provided in Calcite's
+ * {@link FilterJoinRule} when https://github.com/apache/druid/issues/9843 is
resolved.
+ */
+
+public abstract class FilterJoinExcludePushToChildRule extends FilterJoinRule
+{
+ /** Copied from {@link FilterJoinRule#NOT_ENUMERABLE} */
+ private static final Predicate NOT_ENUMERABLE = (join, joinType, exp) ->
+ join.getConvention() != EnumerableConvention.INSTANCE;
+
+ /**
+ * Rule that pushes predicates from a Filter into the Join below them.
+ * Similar to {@link FilterJoinRule#FILTER_ON_JOIN} but does not push
predicate to the child
+ */
+ public static final FilterJoinRule FILTER_ON_JOIN_EXCLUDE_PUSH_TO_CHILD =
+ new FilterIntoJoinExcludePushToChildRule(RelFactories.LOGICAL_BUILDER,
NOT_ENUMERABLE);
+
+ FilterJoinExcludePushToChildRule(RelOptRuleOperand operand,
+ String id,
+ boolean smart,
+ RelBuilderFactory relBuilderFactory,
+ Predicate predicate)
+ {
+ super(operand, id, smart, relBuilderFactory, predicate);
+ }
+
+ /**
+ * Rule that tries to push filter expressions into a join
+ * condition. Exlucde pushing into the inputs (child) of the join.
+ */
+ public static class FilterIntoJoinExcludePushToChildRule extends
FilterJoinExcludePushToChildRule
+ {
+ public FilterIntoJoinExcludePushToChildRule(RelBuilderFactory
relBuilderFactory, Predicate predicate)
+ {
+ super(
+ operand(Filter.class,
+ operand(Join.class, RelOptRule.any())),
+ "FilterJoinExcludePushToChildRule:filter", true, relBuilderFactory,
+ predicate);
+ }
+
+ @Override
+ public void onMatch(RelOptRuleCall call)
+ {
+ Filter filter = call.rel(0);
+ Join join = call.rel(1);
+ perform(call, filter, join);
+ }
+ }
+
+ /**
+ * Copied from {@link FilterJoinRule#perform}
+ * The difference is that this method will not not push filters to the
children in classifyFilters
+ */
+ @Override
+ protected void perform(RelOptRuleCall call, Filter filter, Join join)
+ {
+ final List<RexNode> joinFilters =
+ RelOptUtil.conjunctions(join.getCondition());
+ final List<RexNode> origJoinFilters = ImmutableList.copyOf(joinFilters);
+ // If there is only the joinRel,
+ // make sure it does not match a cartesian product joinRel
+ // (with "true" condition), otherwise this rule will be applied
+ // again on the new cartesian product joinRel.
+ if (filter == null && joinFilters.isEmpty()) {
+ return;
+ }
+
+ final List<RexNode> aboveFilters =
+ filter != null
+ ? getConjunctions(filter)
+ : new ArrayList<>();
+ final ImmutableList<RexNode> origAboveFilters =
+ ImmutableList.copyOf(aboveFilters);
+
+ // Simplify Outer Joins
+ JoinRelType joinType = join.getJoinType();
+ if (!origAboveFilters.isEmpty() && join.getJoinType() !=
JoinRelType.INNER) {
+ joinType = RelOptUtil.simplifyJoin(join, origAboveFilters, joinType);
+ }
+
+ final List<RexNode> leftFilters = new ArrayList<>();
+ final List<RexNode> rightFilters = new ArrayList<>();
+
+ // TODO - add logic to derive additional filters. E.g., from
+ // (t1.a = 1 AND t2.a = 2) OR (t1.b = 3 AND t2.b = 4), you can
+ // derive table filters:
+ // (t1.a = 1 OR t1.b = 3)
+ // (t2.a = 2 OR t2.b = 4)
+
+ // Try to push down above filters. These are typically where clause
+ // filters. They can be pushed down if they are not on the NULL
+ // generating side.
+ boolean filterPushed = false;
+ if (classifyFilters(aboveFilters, joinType, true, joinFilters)) {
+ filterPushed = true;
+ }
+
+ // Move join filters up if needed
+ validateJoinFilters(aboveFilters, joinFilters, join, joinType);
+
+ // If no filter got pushed after validate, reset filterPushed flag
+ if (joinFilters.size() == origJoinFilters.size()) {
+ if (Sets.newHashSet(joinFilters)
+ .equals(Sets.newHashSet(origJoinFilters))) {
+ filterPushed = false;
+ }
+ }
+
+ // Try to push down filters in ON clause. A ON clause filter can only be
+ // pushed down if it does not affect the non-matching set, i.e. it is
+ // not on the side which is preserved.
+
+ // Anti-join on conditions can not be pushed into left or right, e.g. for
plan:
+ //
+ // Join(condition=[AND(cond1, $2)], joinType=[anti])
+ // : - prj(f0=[$0], f1=[$1], f2=[$2])
+ // : - prj(f0=[$0])
+ //
+ // The semantic would change if join condition $2 is pushed into left,
+ // that is, the result set may be smaller. The right can not be pushed
+ // into for the same reason.
+ if (joinType != JoinRelType.ANTI && classifyFilters(joinFilters, joinType,
false, joinFilters)) {
+ filterPushed = true;
+ }
+
+ // if nothing actually got pushed and there is nothing leftover,
+ // then this rule is a no-op
+ if ((!filterPushed && joinType == join.getJoinType()) ||
joinFilters.isEmpty()) {
+ return;
+ }
+
+ // create Filters on top of the children if any filters were
+ // pushed to them
+ final RexBuilder rexBuilder = join.getCluster().getRexBuilder();
+ final RelBuilder relBuilder = call.builder();
+ final RelNode leftRel =
+ relBuilder.push(join.getLeft()).filter(leftFilters).build();
+ final RelNode rightRel =
+ relBuilder.push(join.getRight()).filter(rightFilters).build();
+
+ // create the new join node referencing the new children and
+ // containing its new join filters (if there are any)
+ final ImmutableList<RelDataType> fieldTypes =
+ ImmutableList.<RelDataType>builder()
+ .addAll(RelOptUtil.getFieldTypeList(leftRel.getRowType()))
+
.addAll(RelOptUtil.getFieldTypeList(rightRel.getRowType())).build();
+ final RexNode joinFilter =
+ RexUtil.composeConjunction(rexBuilder,
+ RexUtil.fixUp(rexBuilder, joinFilters,
fieldTypes));
+
+ // If nothing actually got pushed and there is nothing leftover,
+ // then this rule is a no-op
+ if (joinFilter.isAlwaysTrue()
+ && leftFilters.isEmpty()
+ && rightFilters.isEmpty()
+ && joinType == join.getJoinType()) {
+ return;
+ }
+
+ RelNode newJoinRel =
+ join.copy(
+ join.getTraitSet(),
+ joinFilter,
+ leftRel,
+ rightRel,
+ joinType,
+ join.isSemiJoinDone());
+ call.getPlanner().onCopy(join, newJoinRel);
+ if (!leftFilters.isEmpty()) {
+ call.getPlanner().onCopy(filter, leftRel);
+ }
+ if (!rightFilters.isEmpty()) {
+ call.getPlanner().onCopy(filter, rightRel);
+ }
+
+ relBuilder.push(newJoinRel);
+
+ // Create a project on top of the join if some of the columns have become
+ // NOT NULL due to the join-type getting stricter.
+ relBuilder.convert(join.getRowType(), false);
+
+ // create a FilterRel on top of the join if needed
+ relBuilder.filter(
+ RexUtil.fixUp(rexBuilder, aboveFilters,
+
RelOptUtil.getFieldTypeList(relBuilder.peek().getRowType())));
+ call.transformTo(relBuilder.build());
+ }
+
+ /**
+ * Copied from {@link FilterJoinRule#getConjunctions}. Method is exactly the
same as original.
+ */
+ private List<RexNode> getConjunctions(Filter filter)
+ {
+ List<RexNode> conjunctions =
RelOptUtil.conjunctions(filter.getCondition());
+ RexBuilder rexBuilder = filter.getCluster().getRexBuilder();
+ for (int i = 0; i < conjunctions.size(); i++) {
+ RexNode node = conjunctions.get(i);
+ if (node instanceof RexCall) {
+ conjunctions.set(i,
+
RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) node, rexBuilder));
+ }
+ }
+ return conjunctions;
+ }
+
+ /**
+ * Copied from {@link RelOptUtil#classifyFilters}
+ * The difference is that this method will not not push filters to the
children.
+ * Hence, filters will either stay where they are or are pushed to the join
(if they originated
+ * from above the join).
+ */
+ private static boolean classifyFilters(List<RexNode> filters,
+ JoinRelType joinType,
+ boolean pushInto,
+ List<RexNode> joinFilters)
+ {
+ final List<RexNode> filtersToRemove = new ArrayList<>();
+ for (RexNode filter : filters) {
+ // Skip pushing the filter to either child. However, if the join
+ // is an inner join, push them to the join if they originated
+ // from above the join
+ if (!joinType.isOuterJoin() && pushInto) {
+ if (!joinFilters.contains(filter)) {
+ joinFilters.add(filter);
+ }
+ filtersToRemove.add(filter);
+ }
+ }
+
+ // Remove filters after the loop, to prevent concurrent modification.
+ if (!filtersToRemove.isEmpty()) {
+ filters.removeAll(filtersToRemove);
+ }
+
+ // Did anything change?
+ return !filtersToRemove.isEmpty();
+ }
+}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index e15424e..81cf68d 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -8387,6 +8387,324 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
}
@Test
+ public void testInnerJoinTableLookupLookupWithFilterWithOuterLimit() throws
Exception
+ {
+ testQuery(
+ "SELECT dim1\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.dim2 = l.k\n"
+ + "INNER JOIN lookup.lookyloo l2 ON foo.dim2 = l2.k\n"
+ + "WHERE l.v = 'xa'\n"
+ + "LIMIT 100\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
+ new LookupDataSource("lookyloo"),
+ "j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_j0.",
+ equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_j0.k")),
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .limit(100)
+ .filters(selector("j0.v", "xa", null))
+ .columns("dim1")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{""},
+ new Object[]{"1"}
+ )
+ );
+ }
+
+ @Test
+ public void testInnerJoinTableLookupLookupWithFilterWithoutLimit() throws
Exception
+ {
+ testQuery(
+ "SELECT dim1\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.dim2 = l.k\n"
+ + "INNER JOIN lookup.lookyloo l2 ON foo.dim2 = l2.k\n"
+ + "WHERE l.v = 'xa'\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
+ new LookupDataSource("lookyloo"),
+ "j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_j0.",
+ equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_j0.k")),
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .filters(selector("j0.v", "xa", null))
+ .columns("dim1")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{""},
+ new Object[]{"1"}
+ )
+ );
+ }
+
+ @Test
+ public void
testInnerJoinTableLookupLookupWithFilterWithOuterLimitWithAllColumns() throws
Exception
+ {
+ testQuery(
+ "SELECT __time, cnt, dim1, dim2, dim3, m1, m2, unique_dim1\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.dim2 = l.k\n"
+ + "INNER JOIN lookup.lookyloo l2 ON foo.dim2 = l2.k\n"
+ + "WHERE l.v = 'xa'\n"
+ + "LIMIT 100\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
+ new LookupDataSource("lookyloo"),
+ "j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_j0.",
+ equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_j0.k")),
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .limit(100)
+ .filters(selector("j0.v", "xa", null))
+ .columns("__time", "cnt", "dim1", "dim2", "dim3", "m1", "m2",
"unique_dim1")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{946684800000L, 1L, "", "a", "[\"a\",\"b\"]", 1.0F,
1.0, "\"AQAAAEAAAA==\""},
+ new Object[]{978307200000L, 1L, "1", "a", "", 4.0F, 4.0,
"\"AQAAAQAAAAFREA==\""}
+ )
+ );
+ }
+
+ @Test
+ public void
testInnerJoinTableLookupLookupWithFilterWithoutLimitWithAllColumns() throws
Exception
+ {
+ testQuery(
+ "SELECT __time, cnt, dim1, dim2, dim3, m1, m2, unique_dim1\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.dim2 = l.k\n"
+ + "INNER JOIN lookup.lookyloo l2 ON foo.dim2 = l2.k\n"
+ + "WHERE l.v = 'xa'\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
+ new LookupDataSource("lookyloo"),
+ "j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_j0.",
+ equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_j0.k")),
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .filters(selector("j0.v", "xa", null))
+ .columns("__time", "cnt", "dim1", "dim2", "dim3", "m1", "m2",
"unique_dim1")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{946684800000L, 1L, "", "a", "[\"a\",\"b\"]", 1.0F,
1.0, "\"AQAAAEAAAA==\""},
+ new Object[]{978307200000L, 1L, "1", "a", "", 4.0F, 4.0,
"\"AQAAAQAAAAFREA==\""}
+ )
+ );
+ }
+
+ @Test
+ public void testManyManyInnerJoinOnManyManyLookup() throws Exception
+ {
+ testQuery(
+ "SELECT dim1\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.dim2 = l.k\n"
+ + "INNER JOIN lookup.lookyloo l2 ON foo.dim2 = l2.k\n"
+ + "INNER JOIN lookup.lookyloo l3 ON foo.dim2 = l3.k\n"
+ + "INNER JOIN lookup.lookyloo l4 ON foo.dim2 = l4.k\n"
+ + "INNER JOIN lookup.lookyloo l5 ON foo.dim2 = l5.k\n"
+ + "INNER JOIN lookup.lookyloo l6 ON foo.dim2 = l6.k\n"
+ + "INNER JOIN lookup.lookyloo l7 ON foo.dim2 = l7.k\n"
+ + "INNER JOIN lookup.lookyloo l8 ON foo.dim2 = l8.k\n"
+ + "INNER JOIN lookup.lookyloo l9 ON foo.dim2 = l9.k\n"
+ + "INNER JOIN lookup.lookyloo l10 ON foo.dim2 = l10.k\n"
+ + "INNER JOIN lookup.lookyloo l11 ON foo.dim2 = l11.k\n"
+ + "INNER JOIN lookup.lookyloo l12 ON foo.dim2 = l12.k\n"
+ + "INNER JOIN lookup.lookyloo l13 ON foo.dim2 = l13.k\n"
+ + "INNER JOIN lookup.lookyloo l14 ON foo.dim2 = l14.k\n"
+ + "INNER JOIN lookup.lookyloo l15 ON foo.dim2 = l15.k\n"
+ + "INNER JOIN lookup.lookyloo l16 ON foo.dim2 = l16.k\n"
+ + "INNER JOIN lookup.lookyloo l17 ON foo.dim2 = l17.k\n"
+ + "INNER JOIN lookup.lookyloo l18 ON foo.dim2 = l18.k\n"
+ + "INNER JOIN lookup.lookyloo l19 ON foo.dim2 = l19.k\n"
+ + "WHERE l.v = 'xa'\n",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+ join(
+
join(
+
join(
+
join(
+
join(
+
join(
+
new TableDataSource(CalciteTests.DATASOURCE1),
+
new LookupDataSource("lookyloo"),
+
"j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("j0.k")),
+
JoinType.INNER
+
),
+
new LookupDataSource("lookyloo"),
+
"_j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_j0.k")),
+
JoinType.INNER
+
),
+
new LookupDataSource("lookyloo"),
+
"__j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("__j0.k")),
+
JoinType.INNER
+
),
+
new LookupDataSource("lookyloo"),
+
"___j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("___j0.k")),
+
JoinType.INNER
+
),
+
new LookupDataSource("lookyloo"),
+
"____j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("____j0.k")),
+
JoinType.INNER
+ ),
+
new LookupDataSource("lookyloo"),
+
"_____j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_____j0.k")),
+
JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+
"______j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("______j0.k")),
+
JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+
"_______j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_______j0.k")),
+
JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+ "________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("________j0.k")),
+ JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+ "_________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_________j0.k")),
+ JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+ "__________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("__________j0.k")),
+ JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+ "___________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("___________j0.k")),
+ JoinType.INNER
+ ),
+ new
LookupDataSource("lookyloo"),
+ "____________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("____________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_____________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_____________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "______________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("______________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_______________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_______________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "________________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("________________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "_________________j0.",
+
equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("_________________j0.k")),
+ JoinType.INNER
+ ),
+ new LookupDataSource("lookyloo"),
+ "__________________j0.",
+ equalsCondition(DruidExpression.fromColumn("dim2"),
DruidExpression.fromColumn("__________________j0.k")),
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .filters(selector("j0.v", "xa", null))
+ .columns("dim1")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{""},
+ new Object[]{"1"}
+ )
+ );
+ }
+
+ @Test
public void testInnerJoinQueryOfLookup() throws Exception
{
// Cannot vectorize the subquery.
@@ -8747,10 +9065,40 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
public void testWhereInSelectNullFromLookup() throws Exception
{
// Regression test for https://github.com/apache/druid/issues/9646.
+ cannotVectorize();
testQuery(
"SELECT * FROM foo where dim1 IN (SELECT NULL FROM lookup.lookyloo)",
- ImmutableList.of(),
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
+ new QueryDataSource(
+ GroupByQuery.builder()
+ .setDataSource(new
LookupDataSource("lookyloo"))
+
.setInterval(querySegmentSpec(Filtration.eternity()))
+ .setVirtualColumns(
+ expressionVirtualColumn("v0",
"null", ValueType.STRING)
+ )
+ .setGranularity(Granularities.ALL)
+ .setDimensions(dimensions(new
DefaultDimensionSpec("v0", "d0")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ "j0.",
+ "0",
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "null", ValueType.STRING)
+ )
+ .columns("__time", "cnt", "dim2", "dim3", "m1", "m2",
"unique_dim1", "v0")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
ImmutableList.of()
);
}
@@ -8789,6 +9137,9 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
+ // This SQL currently does not result in an optimum plan.
+ // Unfortunately, we have disabled pushing down predicates (conditions and
filters) due to https://github.com/apache/druid/pull/9773
+ // Hence, comma join will result in a cross join with filter on outermost
@Test
public void testCommaJoinTableLookupTableMismatchedTypes() throws Exception
{
@@ -8807,6 +9158,56 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
join(
join(
new TableDataSource(CalciteTests.DATASOURCE1),
+ new LookupDataSource("lookyloo"),
+ "j0.",
+ "1",
+ JoinType.INNER
+ ),
+ new QueryDataSource(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE3)
+
.intervals(querySegmentSpec(Filtration.eternity()))
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .columns("cnt")
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ "_j0.",
+ "1",
+ JoinType.INNER
+ )
+ )
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .aggregators(new CountAggregatorFactory("a0"))
+ .filters(and(
+ expressionFilter("(\"cnt\" == CAST(\"j0.k\", 'LONG'))"),
+ expressionFilter("(CAST(\"j0.k\", 'LONG') ==
\"_j0.cnt\")")
+ ))
+ .context(TIMESERIES_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of()
+ );
+ }
+
+ @Test
+ public void testJoinTableLookupTableMismatchedTypesWithoutComma() throws
Exception
+ {
+ // Cannot vectorize JOIN operator.
+ cannotVectorize();
+
+ testQuery(
+ "SELECT COUNT(*)\n"
+ + "FROM foo\n"
+ + "INNER JOIN lookup.lookyloo l ON foo.cnt = l.k\n"
+ + "INNER JOIN numfoo ON l.k = numfoo.cnt\n",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(
+ join(
+ join(
+ new TableDataSource(CalciteTests.DATASOURCE1),
new QueryDataSource(
newScanQueryBuilder()
.dataSource(new
LookupDataSource("lookyloo"))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]