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 48d72bbfe7f..29236bf8bc1 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 @@ -210,6 +210,18 @@ public static boolean areColumnsDefinitelyUnique(RelMetadataQuery mq, return b != null && b; } + public static boolean isRelDefinitelyEmpty(RelMetadataQuery mq, + RelNode rel) { + Boolean b = mq.isEmpty(rel); + return b != null && b; + } + + public static boolean isRelDefinitelyNotEmpty(RelMetadataQuery mq, + RelNode rel) { + Boolean b = mq.isEmpty(rel); + return b != null && !b; + } + public static @Nullable Boolean areColumnsUnique(RelMetadataQuery mq, RelNode rel, List columnRefs) { ImmutableBitSet.Builder colMask = ImmutableBitSet.builder(); diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java index b2ea8c23da5..bf8e09b2b2a 100644 --- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java +++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java @@ -293,7 +293,7 @@ public static RelMetadataQuery instance() { * statistic. * * @param rel the relational expression - * @return max row count + * @return min row count */ public @Nullable Double getMinRowCount(RelNode rel) { for (;;) { @@ -305,6 +305,26 @@ public static RelMetadataQuery instance() { } } + /** + * Returns whether the return rows of a given relational expression are empty. + * + * @param relNode the relational expression + * @return true or false depending on whether the return rows are empty, or + * null if not enough information is available to make that determination + */ + public @Nullable Boolean isEmpty(RelNode relNode) { + Double minRowCount = getMinRowCount(relNode); + if (minRowCount != null && minRowCount > 0D) { + return Boolean.FALSE; + } + Double maxRowCount = getMaxRowCount(relNode); + if (maxRowCount != null && maxRowCount <= 0D) { + return Boolean.TRUE; + } + return null; + } + + /** * Returns the * {@link BuiltInMetadata.CumulativeCost#getCumulativeCost()} diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java index a201f8f870f..58a5caf02a0 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java @@ -38,6 +38,7 @@ import org.apache.calcite.rel.core.Union; import org.apache.calcite.rel.core.Values; import org.apache.calcite.rel.logical.LogicalValues; +import org.apache.calcite.rel.metadata.RelMdUtil; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rex.RexDynamicParam; import org.apache.calcite.rex.RexLiteral; @@ -645,8 +646,7 @@ public interface ZeroMaxRowsRuleConfig extends PruneEmptyRule.Config { return new RemoveEmptySingleRule(this) { @Override public boolean matches(RelOptRuleCall call) { RelNode node = call.rel(0); - Double maxRowCount = call.getMetadataQuery().getMaxRowCount(node); - return maxRowCount != null && maxRowCount == 0.0; + return RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), node); } }; } diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java index 3e9473f0b60..4aa31a65f45 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java @@ -27,6 +27,7 @@ import org.apache.calcite.rel.core.Join; import org.apache.calcite.rel.core.JoinRelType; import org.apache.calcite.rel.core.Project; +import org.apache.calcite.rel.metadata.RelMdUtil; import org.apache.calcite.rel.metadata.RelMetadataQuery; import org.apache.calcite.rex.LogicVisitor; import org.apache.calcite.rex.RexCorrelVariable; @@ -170,11 +171,10 @@ private static RexNode rewriteCollection(RexSubQuery e, */ private static RexNode rewriteSome(RexSubQuery e, Set variablesSet, RelBuilder builder, int subQueryIndex) { - // If the sub-query is guaranteed to return 0 row, just return + // If the sub-query is guaranteed empty, just return // FALSE. final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery(); - final Double maxRowCount = mq.getMaxRowCount(e.rel); - if (maxRowCount != null && maxRowCount <= 0D) { + if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) { return builder.getRexBuilder().makeLiteral(Boolean.FALSE, e.getType(), true); } // Most general case, where the left and right keys might have nulls, and @@ -462,15 +462,13 @@ private static RexNode rewriteSome(RexSubQuery e, Set variablesSe */ private static RexNode rewriteExists(RexSubQuery e, Set variablesSet, RelOptUtil.Logic logic, RelBuilder builder) { - // If the sub-query is guaranteed to produce at least one row, just return + // If the sub-query is guaranteed never empty, just return // TRUE. final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery(); - final Double minRowCount = mq.getMinRowCount(e.rel); - if (minRowCount != null && minRowCount > 0D) { + if (RelMdUtil.isRelDefinitelyNotEmpty(mq, e.rel)) { return builder.literal(true); } - final Double maxRowCount = mq.getMaxRowCount(e.rel); - if (maxRowCount != null && maxRowCount <= 0D) { + if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) { return builder.literal(false); } builder.push(e.rel); @@ -562,11 +560,10 @@ private static RexNode rewriteUnique(RexSubQuery e, RelBuilder builder) { */ private static RexNode rewriteIn(RexSubQuery e, Set variablesSet, RelOptUtil.Logic logic, RelBuilder builder, int offset, int subQueryIndex) { - // If the sub-query is guaranteed to return 0 row, just return + // If the sub-query is guaranteed empty, just return // FALSE. final RelMetadataQuery mq = e.rel.getCluster().getMetadataQuery(); - final Double maxRowCount = mq.getMaxRowCount(e.rel); - if (maxRowCount != null && maxRowCount <= 0D) { + if (RelMdUtil.isRelDefinitelyEmpty(mq, e.rel)) { return builder.getRexBuilder().makeLiteral(Boolean.FALSE, e.getType(), true); } // Most general case, where the left and right keys might have nulls, and diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index 89516750f35..6a957efe692 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -68,6 +68,7 @@ import org.apache.calcite.rel.logical.LogicalTableScan; import org.apache.calcite.rel.logical.LogicalValues; import org.apache.calcite.rel.metadata.RelColumnMapping; +import org.apache.calcite.rel.metadata.RelMdUtil; import org.apache.calcite.rel.metadata.RelMetadataQuery; import org.apache.calcite.rel.stream.Delta; import org.apache.calcite.rel.stream.LogicalDelta; @@ -1377,12 +1378,11 @@ private void substituteSubQuery(Blackboard bb, SubQuery subQuery) { final Blackboard seekBb = createBlackboard(seekScope, null, false); final RelNode seekRel = convertQueryOrInList(seekBb, query, null); requireNonNull(seekRel, () -> "seekRel is null for query " + query); - // An EXIST sub-query whose inner child has at least 1 tuple + // An EXIST sub-query whose inner child guaranteed never empty // (e.g. an Aggregate with no grouping columns or non-empty Values // node) should be simplified to a Boolean constant expression. final RelMetadataQuery mq = seekRel.getCluster().getMetadataQuery(); - final Double minRowCount = mq.getMinRowCount(seekRel); - if (minRowCount != null && minRowCount >= 1D) { + if (RelMdUtil.isRelDefinitelyNotEmpty(mq, seekRel)) { subQuery.expr = rexBuilder.makeLiteral(true); return; } diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java index 31c8e27d43f..478dbb41354 100644 --- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java @@ -63,6 +63,7 @@ import org.apache.calcite.rel.logical.LogicalFilter; import org.apache.calcite.rel.logical.LogicalProject; import org.apache.calcite.rel.metadata.RelColumnMapping; +import org.apache.calcite.rel.metadata.RelMdUtil; import org.apache.calcite.rel.metadata.RelMetadataQuery; import org.apache.calcite.rel.rules.AggregateRemoveRule; import org.apache.calcite.rel.type.RelDataType; @@ -2678,10 +2679,7 @@ private boolean alreadyUnique(List aggCallList, List extraNodes) { final RelMetadataQuery mq = peek().getCluster().getMetadataQuery(); if (aggCallList.isEmpty() && groupSet.isEmpty()) { - final Double minRowCount = mq.getMinRowCount(peek()); - if (minRowCount == null || minRowCount < 1d) { - // We can't remove "GROUP BY ()" if there's a chance the rel could be - // empty. + if (RelMdUtil.isRelDefinitelyEmpty(mq, peek())) { return false; } }