You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by tl...@apache.org on 2021/08/31 16:09:02 UTC
[ignite] branch sql-calcite updated: IGNITE-14307 Use statistics in
cost model. (#9276)
This is an automated email from the ASF dual-hosted git repository.
tledkov pushed a commit to branch sql-calcite
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/sql-calcite by this push:
new 1a1aa0c IGNITE-14307 Use statistics in cost model. (#9276)
1a1aa0c is described below
commit 1a1aa0c5b695a86161ebe61a5f382e8716750c5b
Author: Berkof <sa...@mail.ru>
AuthorDate: Tue Aug 31 23:08:44 2021 +0700
IGNITE-14307 Use statistics in cost model. (#9276)
---
.../query/calcite/exec/RuntimeSortedIndex.java | 15 +-
.../calcite/metadata/IgniteMdColumnOrigins.java | 386 +++++++++++++
.../calcite/metadata/IgniteMdSelectivity.java | 637 +++++++++++++++++++--
.../query/calcite/metadata/IgniteMetadata.java | 1 +
.../query/calcite/rel/AbstractIndexScan.java | 10 +-
.../rel/ProjectableFilterableTableScan.java | 14 +
.../query/calcite/schema/IgniteStatisticsImpl.java | 107 ++++
.../query/calcite/schema/IgniteTableImpl.java | 66 +--
.../query/calcite/schema/TableDescriptor.java | 3 +-
.../CalciteBasicSecondaryIndexIntegrationTest.java | 9 +-
.../processors/query/calcite/QueryChecker.java | 41 +-
.../processors/query/calcite/QueryCheckerTest.java | 24 +-
.../integration/AbstractBasicIntegrationTest.java | 10 +-
.../integration/AggregatesIntegrationTest.java | 2 +-
.../CalciteErrorHandlilngIntegrationTest.java | 9 +-
.../ServerStatisticsIntegrationTest.java | 614 ++++++++++++++++++++
.../query/calcite/planner/AbstractPlannerTest.java | 72 +--
.../calcite/planner/StatisticsPlannerTest.java | 460 +++++++++++++++
.../query/calcite/rules/OrToUnionRuleTest.java | 8 +-
.../ignite/testsuites/IntegrationTestSuite.java | 2 +
.../apache/ignite/testsuites/PlannerTestSuite.java | 2 +
.../stat/config/StatisticsObjectConfiguration.java | 9 +
22 files changed, 2331 insertions(+), 170 deletions(-)
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeSortedIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeSortedIndex.java
index 6d9677f..88df20a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeSortedIndex.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeSortedIndex.java
@@ -79,14 +79,13 @@ public class RuntimeSortedIndex<Row> implements RuntimeIndex<Row>, TreeIndex<Row
int firstCol = F.first(collation.getKeys());
- if (ectx.rowHandler().get(firstCol, lower) != null && ectx.rowHandler().get(firstCol, upper) != null)
- return new Cursor(rows, lower, upper);
- else if (ectx.rowHandler().get(firstCol, lower) == null && ectx.rowHandler().get(firstCol, upper) != null)
- return new Cursor(rows, null, upper);
- else if (ectx.rowHandler().get(firstCol, lower) != null && ectx.rowHandler().get(firstCol, upper) == null)
- return new Cursor(rows, lower, null);
- else
- return new Cursor(rows, null, null);
+ Object lowerBound = (lower == null) ? null : ectx.rowHandler().get(firstCol, lower);
+ Object upperBound = (upper == null) ? null : ectx.rowHandler().get(firstCol, upper);
+
+ Row lowerRow = (lowerBound == null) ? null : lower;
+ Row upperRow = (upperBound == null) ? null : upper;
+
+ return new Cursor(rows, lowerRow, upperRow);
}
/**
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdColumnOrigins.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdColumnOrigins.java
new file mode 100644
index 0000000..47315a5
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdColumnOrigins.java
@@ -0,0 +1,386 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.metadata;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Calc;
+import org.apache.calcite.rel.core.Exchange;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.core.TableFunctionScan;
+import org.apache.calcite.rel.core.TableModify;
+import org.apache.calcite.rel.metadata.BuiltInMetadata;
+import org.apache.calcite.rel.metadata.MetadataDef;
+import org.apache.calcite.rel.metadata.MetadataHandler;
+import org.apache.calcite.rel.metadata.ReflectiveRelMetadataProvider;
+import org.apache.calcite.rel.metadata.RelColumnMapping;
+import org.apache.calcite.rel.metadata.RelColumnOrigin;
+import org.apache.calcite.rel.metadata.RelMetadataProvider;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLocalRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexSlot;
+import org.apache.calcite.rex.RexVisitor;
+import org.apache.calcite.rex.RexVisitorImpl;
+import org.apache.calcite.util.BuiltInMethod;
+import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilterableTableScan;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * RelMdColumnOrigins supplies a default implementation of
+ * {@link RelMetadataQuery#getColumnOrigins} for the standard logical algebra.
+ */
+public class IgniteMdColumnOrigins implements MetadataHandler<BuiltInMetadata.ColumnOrigin> {
+ /** */
+ public static final RelMetadataProvider SOURCE = ReflectiveRelMetadataProvider.reflectiveSource(
+ BuiltInMethod.COLUMN_ORIGIN.method, new IgniteMdColumnOrigins());
+
+ /** {@inheritDoc} */
+ @Override public MetadataDef<BuiltInMetadata.ColumnOrigin> getDef() {
+ return BuiltInMetadata.ColumnOrigin.DEF;
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Aggregate rel, RelMetadataQuery mq, int iOutputColumn) {
+ if (iOutputColumn < rel.getGroupCount()) {
+ // get actual index of Group columns.
+ return mq.getColumnOrigins(rel.getInput(), rel.getGroupSet().asList().get(iOutputColumn));
+ }
+
+ // Aggregate columns are derived from input columns
+ AggregateCall call = rel.getAggCallList().get(iOutputColumn - rel.getGroupCount());
+
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ for (Integer iInput : call.getArgList()) {
+ Set<RelColumnOrigin> inputSet = mq.getColumnOrigins(rel.getInput(), iInput);
+ inputSet = createDerivedColumnOrigins(inputSet);
+
+ if (inputSet != null)
+ set.addAll(inputSet);
+
+ }
+ return set;
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Join rel, RelMetadataQuery mq,
+ int iOutputColumn) {
+ int nLeftColumns = rel.getLeft().getRowType().getFieldList().size();
+ Set<RelColumnOrigin> set;
+ boolean derived = false;
+
+ if (iOutputColumn < nLeftColumns) {
+ set = mq.getColumnOrigins(rel.getLeft(), iOutputColumn);
+
+ if (rel.getJoinType().generatesNullsOnLeft())
+ derived = true;
+
+ }
+ else {
+ set = mq.getColumnOrigins(rel.getRight(), iOutputColumn - nLeftColumns);
+
+ if (rel.getJoinType().generatesNullsOnRight())
+ derived = true;
+ }
+
+ if (derived) {
+ // nulls are generated due to outer join; that counts
+ // as derivation
+ set = createDerivedColumnOrigins(set);
+ }
+ return set;
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(SetOp rel, RelMetadataQuery mq, int iOutputColumn) {
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ for (RelNode input : rel.getInputs()) {
+ Set<RelColumnOrigin> inputSet = mq.getColumnOrigins(input, iOutputColumn);
+
+ if (inputSet == null)
+ return null;
+
+ set.addAll(inputSet);
+ }
+
+ return set;
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Project rel,
+ final RelMetadataQuery mq, int iOutputColumn) {
+ final RelNode input = rel.getInput();
+ RexNode rexNode = rel.getProjects().get(iOutputColumn);
+
+ if (rexNode instanceof RexInputRef) {
+ // Direct reference: no derivation added.
+ RexInputRef inputRef = (RexInputRef) rexNode;
+
+ return mq.getColumnOrigins(input, inputRef.getIndex());
+ }
+ // Anything else is a derivation, possibly from multiple columns.
+ final Set<RelColumnOrigin> set = getMultipleColumns(rexNode, input, mq);
+
+ return createDerivedColumnOrigins(set);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Calc rel, final RelMetadataQuery mq, int iOutputColumn) {
+ final RelNode input = rel.getInput();
+ final RexShuttle rexShuttle = new RexShuttle() {
+ @Override public RexNode visitLocalRef(RexLocalRef localRef) {
+ return rel.getProgram().expandLocalRef(localRef);
+ }
+ };
+ final List<RexNode> projects = new ArrayList<>();
+
+ for (RexNode rex: rexShuttle.apply(rel.getProgram().getProjectList()))
+ projects.add(rex);
+
+ final RexNode rexNode = projects.get(iOutputColumn);
+
+ if (rexNode instanceof RexInputRef) {
+ // Direct reference: no derivation added.
+ RexInputRef inputRef = (RexInputRef) rexNode;
+
+ return mq.getColumnOrigins(input, inputRef.getIndex());
+ }
+
+ // Anything else is a derivation, possibly from multiple columns.
+ final Set<RelColumnOrigin> set = getMultipleColumns(rexNode, input, mq);
+
+ return createDerivedColumnOrigins(set);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Filter rel, RelMetadataQuery mq, int iOutputColumn) {
+ return mq.getColumnOrigins(rel.getInput(), iOutputColumn);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Sort rel, RelMetadataQuery mq, int iOutputColumn) {
+ return mq.getColumnOrigins(rel.getInput(), iOutputColumn);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(TableModify rel, RelMetadataQuery mq, int iOutputColumn) {
+ return mq.getColumnOrigins(rel.getInput(), iOutputColumn);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(Exchange rel, RelMetadataQuery mq, int iOutputColumn) {
+ return mq.getColumnOrigins(rel.getInput(), iOutputColumn);
+ }
+
+ /** */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(
+ TableFunctionScan rel,
+ RelMetadataQuery mq,
+ int iOutputColumn
+ ) {
+ Set<RelColumnMapping> mappings = rel.getColumnMappings();
+
+ if (mappings == null) {
+ if (!rel.getInputs().isEmpty()) {
+ // This is a non-leaf transformation: say we don't
+ // know about origins, because there are probably
+ // columns below.
+ return null;
+ }
+ else {
+ // This is a leaf transformation: say there are for sure no
+ // column origins.
+ return Collections.emptySet();
+ }
+ }
+
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ for (RelColumnMapping mapping : mappings) {
+ if (mapping.iOutputColumn != iOutputColumn)
+ continue;
+
+ final RelNode input = rel.getInputs().get(mapping.iInputRel);
+ final int column = mapping.iInputColumn;
+ Set<RelColumnOrigin> origins = mq.getColumnOrigins(input, column);
+
+ if (origins == null)
+ return null;
+
+ if (mapping.derived)
+ origins = createDerivedColumnOrigins(origins);
+
+ set.addAll(origins);
+ }
+
+ return set;
+ }
+
+ /**
+ * Get column origins.
+ *
+ * @param rel Rel to get origins from.
+ * @param mq Rel metadata query.
+ * @param iOutputColumn Column idx.
+ * @return Set of column origins.
+ */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(
+ ProjectableFilterableTableScan rel,
+ RelMetadataQuery mq,
+ int iOutputColumn
+ ) {
+ if (rel.projects() != null) {
+ RexNode proj = rel.projects().get(iOutputColumn);
+ Set<RexSlot> sources = new HashSet<>();
+
+ getOperands(proj, RexSlot.class, sources);
+
+ boolean derived = sources.size() > 1;
+ Set<RelColumnOrigin> res = new HashSet<>();
+
+ for (RexSlot slot : sources) {
+ if (slot instanceof RexLocalRef) {
+ RelColumnOrigin slotOrigin = rel.columnOriginsByRelLocalRef(slot.getIndex());
+
+ res.add(new RelColumnOrigin(slotOrigin.getOriginTable(), slotOrigin.getOriginColumnOrdinal(),
+ derived));
+ }
+ }
+
+ return res;
+ }
+
+ return Collections.singleton(rel.columnOriginsByRelLocalRef(iOutputColumn));
+ }
+
+ /**
+ * Get operands of specified type from RexCall nodes.
+ *
+ * @param rn RexNode to get operands
+ * @param cls Target class.
+ * @param res Set to store results into.
+ */
+ private <T> void getOperands(RexNode rn, Class<T> cls, Set<T> res) {
+ if (cls.isAssignableFrom(rn.getClass()))
+ res.add((T)rn);
+
+ if (rn instanceof RexCall) {
+ List<RexNode> operands = ((RexCall)rn).getOperands();
+
+ for (RexNode op : operands)
+ getOperands(op, cls, res);
+ }
+ }
+
+ /**
+ * Catch-all rule when none of the others apply.
+ *
+ * @param rel RelNode.
+ * @param mq RelMetadataQuery.
+ * @param iOutputColumn output column idx.
+ * @return Set of column origins.
+ */
+ public @Nullable Set<RelColumnOrigin> getColumnOrigins(RelNode rel, RelMetadataQuery mq, int iOutputColumn) {
+ // NOTE jvs 28-Mar-2006: We may get this wrong for a physical table
+ // expression which supports projections. In that case,
+ // it's up to the plugin writer to override with the
+ // correct information.
+
+ if (!rel.getInputs().isEmpty()) {
+ // No generic logic available for non-leaf rels.
+ return null;
+ }
+
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ RelOptTable table = rel.getTable();
+ if (table == null) {
+ // Somebody is making column values up out of thin air, like a
+ // VALUES clause, so we return an empty set.
+ return set;
+ }
+
+ // Detect the case where a physical table expression is performing
+ // projection, and say we don't know instead of making any assumptions.
+ // (Theoretically we could try to map the projection using column
+ // names.) This detection assumes the table expression doesn't handle
+ // rename as well.
+ if (table.getRowType() != rel.getRowType())
+ return null;
+
+ set.add(new RelColumnOrigin(table, iOutputColumn, false));
+
+ return set;
+ }
+
+ /**
+ * Create derived set of column origins from specified.
+ *
+ * @param inputSet RelColumnOrigin set to derive from.
+ * @return derived RelColumnOrigin or {@code null}.
+ */
+ private static Set<RelColumnOrigin> createDerivedColumnOrigins(Set<RelColumnOrigin> inputSet) {
+ if (inputSet == null)
+ return null;
+
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ for (RelColumnOrigin rco : inputSet) {
+ RelColumnOrigin derived = new RelColumnOrigin(rco.getOriginTable(), rco.getOriginColumnOrdinal(), true);
+ set.add(derived);
+ }
+
+ return set;
+ }
+
+ /** */
+ private static Set<RelColumnOrigin> getMultipleColumns(RexNode rexNode, RelNode input, final RelMetadataQuery mq) {
+ final Set<RelColumnOrigin> set = new HashSet<>();
+
+ final RexVisitor<Void> visitor = new RexVisitorImpl<Void>(true) {
+ @Override public Void visitInputRef(RexInputRef inputRef) {
+ Set<RelColumnOrigin> inputSet = mq.getColumnOrigins(input, inputRef.getIndex());
+
+ if (inputSet != null)
+ set.addAll(inputSet);
+
+ return null;
+ }
+ };
+
+ rexNode.accept(visitor);
+
+ return set;
+ }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
index 83bfc8f..07ac446 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
@@ -17,83 +17,91 @@
package org.apache.ignite.internal.processors.query.calcite.metadata;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.metadata.ReflectiveRelMetadataProvider;
+import org.apache.calcite.rel.metadata.RelColumnOrigin;
import org.apache.calcite.rel.metadata.RelMdSelectivity;
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;
import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexLocalRef;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.rex.RexSlot;
+import org.apache.calcite.schema.Statistic;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.BasicSqlType;
+import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.util.BuiltInMethod;
-import org.apache.ignite.internal.processors.query.calcite.rel.AbstractIndexScan;
+import org.apache.calcite.util.DateString;
+import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimestampString;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilterableTableScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteStatisticsImpl;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
-import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.processors.query.stat.ColumnStatistics;
+import org.h2.value.Value;
+import org.jetbrains.annotations.Nullable;
/** */
public class IgniteMdSelectivity extends RelMdSelectivity {
- /** */
- public static final RelMetadataProvider SOURCE =
- ReflectiveRelMetadataProvider.reflectiveSource(
- BuiltInMethod.SELECTIVITY.method, new IgniteMdSelectivity());
-
- /** */
- public Double getSelectivity(AbstractIndexScan rel, RelMetadataQuery mq, RexNode predicate) {
- if (predicate != null)
- return getSelectivity((ProjectableFilterableTableScan)rel, mq, predicate);
+ /** Default selectivity for IS NULL conditions. */
+ private static final double IS_NULL_SELECTIVITY = 0.1;
- List<RexNode> lowerCond = rel.lowerCondition();
- List<RexNode> upperCond = rel.upperCondition();
+ /** Default selectivity for IS NOT NULL conditions. */
+ private static final double IS_NOT_NULL_SELECTIVITY = 1 - IS_NULL_SELECTIVITY;
- if (F.isEmpty(lowerCond) && F.isEmpty(upperCond))
- return RelMdUtil.guessSelectivity(rel.condition());
+ /** Default selectivity for equals conditions. */
+ private static final double EQUALS_SELECTIVITY = 0.15;
- double idxSelectivity = 1.0;
- int len = F.isEmpty(lowerCond) ? upperCond.size() : F.isEmpty(upperCond) ? lowerCond.size() :
- Math.max(lowerCond.size(), upperCond.size());
-
- for (int i = 0; i < len; i++) {
- RexCall lower = F.isEmpty(lowerCond) || lowerCond.size() <= i ? null : (RexCall)lowerCond.get(i);
- RexCall upper = F.isEmpty(upperCond) || upperCond.size() <= i ? null : (RexCall)upperCond.get(i);
-
- assert lower != null || upper != null;
-
- if (lower != null && upper != null)
- idxSelectivity *= lower.op.kind == SqlKind.EQUALS ? .1 : .2;
- else
- idxSelectivity *= .35;
- }
+ /** Default selectivity for comparison conitions. */
+ private static final double COMPARISON_SELECTIVITY = 0.5;
- List<RexNode> conjunctions = RelOptUtil.conjunctions(rel.condition());
+ /** Default selectivity for other conditions. */
+ private static final double OTHER_SELECTIVITY = 0.25;
- if (!F.isEmpty(lowerCond))
- conjunctions.removeAll(lowerCond);
- if (!F.isEmpty(upperCond))
- conjunctions.removeAll(upperCond);
+ /**
+ * Math context to use in estimations calculations.
+ */
+ private final MathContext MATH_CONTEXT = MathContext.DECIMAL64;
- RexNode remaining = RexUtil.composeConjunction(RexUtils.builder(rel), conjunctions, true);
-
- return idxSelectivity * RelMdUtil.guessSelectivity(remaining);
- }
+ /** */
+ public static final RelMetadataProvider SOURCE =
+ ReflectiveRelMetadataProvider.reflectiveSource(
+ BuiltInMethod.SELECTIVITY.method, new IgniteMdSelectivity());
/** */
public Double getSelectivity(ProjectableFilterableTableScan rel, RelMetadataQuery mq, RexNode predicate) {
if (predicate == null)
- return RelMdUtil.guessSelectivity(rel.condition());
+ return getTablePredicateBasedSelectivity(rel, mq, rel.condition());
RexNode condition = rel.pushUpPredicate();
+
if (condition == null)
- return RelMdUtil.guessSelectivity(predicate);
+ return getTablePredicateBasedSelectivity(rel, mq, predicate);
RexNode diff = RelMdUtil.minusPreds(RexUtils.builder(rel), predicate, condition);
- return RelMdUtil.guessSelectivity(diff);
+
+ return getTablePredicateBasedSelectivity(rel, mq, diff);
}
/** */
@@ -110,6 +118,549 @@ public class IgniteMdSelectivity extends RelMdSelectivity {
}
/** */
+ public Double getSelectivity(RelSubset rel, RelMetadataQuery mq, RexNode predicate) {
+ RelNode best = rel.getBest();
+
+ if (best == null)
+ return super.getSelectivity(rel, mq, predicate);
+
+ return getSelectivity(best, mq, predicate);
+ }
+
+ /**
+ * Convert specified value into comparable type: BigDecimal,
+ *
+ * @param val Value to convert to comparable form.
+ * @return Comparable form of value.
+ */
+ private BigDecimal toComparableValue(RexLiteral val) {
+ RelDataType type = val.getType();
+
+ if (type instanceof BasicSqlType) {
+ BasicSqlType bType = (BasicSqlType)type;
+
+ switch ((SqlTypeFamily)bType.getFamily()) {
+ case NULL:
+ return null;
+
+ case NUMERIC:
+ return val.getValueAs(BigDecimal.class);
+
+ case DATE:
+ return new BigDecimal(val.getValueAs(DateString.class).getMillisSinceEpoch());
+
+ case TIME:
+ return new BigDecimal(val.getValueAs(TimeString.class).getMillisOfDay());
+
+ case TIMESTAMP:
+ return new BigDecimal(val.getValueAs(TimestampString.class).getMillisSinceEpoch());
+
+ case BOOLEAN:
+ return (val.getValueAs(Boolean.class)) ? BigDecimal.ONE : BigDecimal.ZERO;
+
+ default:
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert specified value into comparable type: BigDecimal,
+ *
+ * @param val Value to convert to comparable form.
+ * @return Comparable form of value.
+ */
+ private BigDecimal toComparableValue(Value val) {
+ if (val == null)
+ return null;
+
+ switch (val.getType()) {
+ case Value.NULL:
+ throw new IllegalArgumentException("Can't compare null values");
+
+ case Value.BOOLEAN:
+ return (val.getBoolean()) ? BigDecimal.ONE : BigDecimal.ZERO;
+
+ case Value.BYTE:
+ return new BigDecimal(val.getByte());
+
+ case Value.SHORT:
+ return new BigDecimal(val.getShort());
+
+ case Value.INT:
+ return new BigDecimal(val.getInt());
+
+ case Value.LONG:
+ return new BigDecimal(val.getLong());
+
+ case Value.DECIMAL:
+ return val.getBigDecimal();
+
+ case Value.DOUBLE:
+ return BigDecimal.valueOf(val.getDouble());
+
+ case Value.FLOAT:
+ return BigDecimal.valueOf(val.getFloat());
+
+ case Value.DATE:
+ return BigDecimal.valueOf(val.getDate().getTime());
+
+ case Value.TIME:
+ return BigDecimal.valueOf(val.getTime().getTime());
+
+ case Value.TIMESTAMP:
+ return BigDecimal.valueOf(val.getTimestamp().getTime());
+
+ case Value.BYTES:
+ BigInteger bigInteger = new BigInteger(1, val.getBytes());
+ return new BigDecimal(bigInteger);
+
+ case Value.STRING:
+ case Value.STRING_FIXED:
+ case Value.STRING_IGNORECASE:
+ case Value.ARRAY:
+ case Value.JAVA_OBJECT:
+ case Value.GEOMETRY:
+ return null;
+
+ case Value.UUID:
+ BigInteger bigInt = new BigInteger(1, val.getBytes());
+ return new BigDecimal(bigInt);
+
+ default:
+ throw new IllegalStateException("Unsupported H2 type: " + val.getType());
+ }
+ }
+
+ /**
+ * Predicate based selectivity for table. Estimate condition on each column taking in comparison it's statistics.
+ *
+ * @param rel Original rel node to fallback calculation by.
+ * @param mq RelMetadataQuery.
+ * @param predicate Predicate to estimate selectivity by.
+ * @return Selectivity.
+ */
+ private double getTablePredicateBasedSelectivity(
+ ProjectableFilterableTableScan rel,
+ RelMetadataQuery mq,
+ RexNode predicate
+ ) {
+ if (predicate == null || predicate.isAlwaysTrue())
+ return 1.0;
+
+ if (predicate.isAlwaysFalse())
+ return 0.0;
+
+ double sel = 1.0;
+
+ Map<RexSlot, Boolean> addNotNull = new HashMap<>();
+
+ for (RexNode pred : RelOptUtil.conjunctions(predicate)) {
+ SqlKind predKind = pred.getKind();
+
+ if (predKind == SqlKind.OR) {
+ double orSelTotal = 1;
+
+ for (RexNode orPred : RelOptUtil.disjunctions(pred))
+ orSelTotal *= 1 - getTablePredicateBasedSelectivity(rel, mq, orPred);
+
+ sel *= 1 - orSelTotal;
+
+ continue;
+ }
+ else if (predKind == SqlKind.NOT) {
+ assert pred instanceof RexCall;
+
+ sel *= 1 - getTablePredicateBasedSelectivity(rel, mq, ((RexCall)pred).getOperands().get(0));
+
+ continue;
+ }
+
+ RexSlot op = null;
+
+ if (pred instanceof RexCall)
+ op = getOperand((RexCall)pred);
+ else if (pred instanceof RexSlot)
+ op = (RexSlot)pred;
+
+ ColumnStatistics colStat = getColumnStatistics(mq, rel, op);
+
+ if (colStat == null)
+ sel *= guessSelectivity(pred);
+ else if (predKind == SqlKind.LOCAL_REF) {
+ if (op != null)
+ addNotNull.put(op, Boolean.TRUE);
+
+ sel *= estimateRefSelectivity(rel, mq, (RexLocalRef)pred);
+ }
+ else if (predKind == SqlKind.IS_NULL) {
+ if (op != null)
+ addNotNull.put(op, Boolean.FALSE);
+
+ sel *= estimateIsNullSelectivity(colStat);
+ }
+ else if (predKind == SqlKind.IS_NOT_NULL) {
+ if (op != null)
+ addNotNull.put(op, Boolean.FALSE);
+
+ sel *= estimateIsNotNullSelectivity(colStat);
+ }
+ else if (predKind == SqlKind.EQUALS) {
+ if (op != null)
+ addNotNull.put(op, Boolean.TRUE);
+
+ assert pred instanceof RexCall;
+
+ sel *= estimateEqualsSelectivity(colStat, (RexCall)pred);
+ }
+ else if (predKind.belongsTo(SqlKind.COMPARISON)) {
+ if (op != null)
+ addNotNull.put(op, Boolean.TRUE);
+
+ assert pred instanceof RexCall;
+
+ sel *= estimateRangeSelectivity(colStat, (RexCall)pred);
+ }
+ else
+ sel *= .25;
+ }
+
+ // Estimate not null selectivity in addition to comparison.
+ for (Map.Entry<RexSlot, Boolean> colAddNotNull : addNotNull.entrySet()) {
+ if (colAddNotNull.getValue()) {
+ ColumnStatistics colStat = getColumnStatistics(mq, rel, colAddNotNull.getKey());
+
+ sel *= (colStat == null) ? IS_NOT_NULL_SELECTIVITY : estimateIsNotNullSelectivity(colStat);
+ }
+ }
+
+ return sel;
+ }
+
+ /**
+ * Finds a column statistics by a given operand within table scan.
+ *
+ * @param mq Metadata query which used to find column origins in case
+ * the operand is input reference.
+ * @param rel Table scan the operand related to.
+ * @param op Operand to search statistics for.
+ * @return Column statistcs or {@code null} if it's not possoble to determine
+ * the origins of the given operand or the is no statistics gathered
+ * for given column.
+ */
+ private @Nullable ColumnStatistics getColumnStatistics(RelMetadataQuery mq, ProjectableFilterableTableScan rel, RexSlot op) {
+ RelColumnOrigin origin;
+
+ if (op instanceof RexLocalRef)
+ origin = rel.columnOriginsByRelLocalRef(op.getIndex());
+ else if (op instanceof RexInputRef)
+ origin = mq.getColumnOrigin(rel, op.getIndex());
+ else
+ return null;
+
+ String colName = extactFieldName(origin);
+
+ IgniteTable tbl = rel.getTable().unwrap(IgniteTable.class);
+
+ assert tbl != null;
+
+ if (QueryUtils.KEY_FIELD_NAME.equals(colName))
+ colName = tbl.descriptor().typeDescription().keyFieldName();
+
+ Statistic stat = tbl.getStatistic();
+
+ if (!(stat instanceof IgniteStatisticsImpl))
+ return null;
+
+ return ((IgniteStatisticsImpl)stat).getColumnStatistics(colName);
+ }
+
+ /** Returns field name for provided {@link RelColumnOrigin}. */
+ private static String extactFieldName(RelColumnOrigin origin) {
+ return origin.getOriginTable().getRowType().getFieldNames().get(origin.getOriginColumnOrdinal());
+ }
+
+ /**
+ * Estimate local ref selectivity (means is true confition).
+ *
+ * @param rel RelNode.
+ * @param mq RelMetadataQuery.
+ * @param ref RexLocalRef.
+ * @return Selectivity estimation.
+ */
+ private double estimateRefSelectivity(ProjectableFilterableTableScan rel, RelMetadataQuery mq, RexLocalRef ref) {
+ ColumnStatistics colStat = getColumnStatistics(mq, rel, ref);
+ double res = 0.33;
+
+ if (colStat == null) {
+ // true, false and null with equivalent probability
+ return res;
+ }
+
+ if (colStat.max() == null || colStat.max().getType() != Value.BOOLEAN)
+ return res;
+
+ Boolean min = colStat.min().getBoolean();
+ Boolean max = colStat.max().getBoolean();
+
+ if (!max)
+ return 0;
+
+ double notNullSel = estimateIsNotNullSelectivity(colStat);
+
+ return (max && min) ? notNullSel : notNullSel / 2;
+ }
+
+ /**
+ * Estimate range selectivity based on predicate.
+ *
+ * @param colStat Column statistics to use.
+ * @param pred Condition.
+ * @return Selectivity.
+ */
+ private double estimateRangeSelectivity(ColumnStatistics colStat, RexCall pred) {
+ RexLiteral literal = null;
+
+ if (pred.getOperands().get(1) instanceof RexLiteral)
+ literal = (RexLiteral)pred.getOperands().get(1);
+
+ if (literal == null)
+ return guessSelectivity(pred);
+
+ BigDecimal val = toComparableValue(literal);
+
+ return estimateSelectivity(colStat, val, pred);
+ }
+
+ /**
+ * Estimate range selectivity based on predicate, condition and column statistics.
+ *
+ * @param colStat Column statistics to use.
+ * @param val Condition value.
+ * @param pred Condition.
+ * @return Selectivity.
+ */
+ private double estimateSelectivity(ColumnStatistics colStat, BigDecimal val, RexNode pred) {
+ // Without value or statistics we can only guess.
+ if (val == null)
+ return guessSelectivity(pred);
+
+ SqlOperator op = ((RexCall)pred).op;
+
+ BigDecimal min = toComparableValue(colStat.min());
+ BigDecimal max = toComparableValue(colStat.max());
+ BigDecimal total = (min == null || max == null) ? null : max.subtract(min).abs();
+
+ if (total == null)
+ // No min/max mean that all values are null for column.
+ return guessSelectivity(pred);
+
+ // All values the same so check condition and return all or nothing selectivity.
+ if (total.signum() == 0) {
+ BigDecimal diff = val.subtract(min);
+ int diffSign = diff.signum();
+
+ switch (op.getKind()) {
+ case GREATER_THAN:
+ return (diffSign < 0) ? 1. : 0.;
+
+ case LESS_THAN:
+ return (diffSign > 0) ? 1. : 0.;
+
+ case GREATER_THAN_OR_EQUAL:
+ return (diffSign <= 0) ? 1. : 0.;
+
+ case LESS_THAN_OR_EQUAL:
+ return (diffSign >= 0) ? 1. : 0.;
+
+ default:
+ return guessSelectivity(pred);
+ }
+ }
+
+ // Estimate percent of selectivity by ranges.
+ BigDecimal actual = BigDecimal.ZERO;
+
+ switch (op.getKind()) {
+ case GREATER_THAN:
+ case GREATER_THAN_OR_EQUAL:
+ actual = max.subtract(val);
+
+ if (actual.signum() < 0)
+ return 0.;
+
+ break;
+
+ case LESS_THAN:
+ case LESS_THAN_OR_EQUAL:
+ actual = val.subtract(min);
+
+ if (actual.signum() < 0)
+ return 0.;
+
+ break;
+
+ default:
+ return guessSelectivity(pred);
+ }
+
+ return (actual.compareTo(total) > 0) ? 1 : actual.divide(total, MATH_CONTEXT).doubleValue();
+ }
+
+ /**
+ * Estimate "=" selectivity by column statistics.
+ *
+ * @param colStat Column statistics.
+ * @param pred Comparable value to compare with.
+ * @return Selectivity.
+ */
+ private double estimateEqualsSelectivity(ColumnStatistics colStat, RexCall pred) {
+ if (colStat.total() == 0)
+ return 1.;
+
+ if (colStat.total() - colStat.nulls() == 0)
+ return 0.;
+
+ RexLiteral literal = null;
+ if (pred.getOperands().get(1) instanceof RexLiteral)
+ literal = (RexLiteral)pred.getOperands().get(1);
+
+ if (literal == null)
+ return guessSelectivity(pred);
+
+ BigDecimal comparableVal = toComparableValue(literal);
+
+ if (comparableVal == null)
+ return guessSelectivity(pred);
+
+ if (colStat.min() != null) {
+ BigDecimal minComparable = toComparableValue(colStat.min());
+ if (minComparable != null && minComparable.compareTo(comparableVal) > 0)
+ return 0.;
+ }
+
+ if (colStat.max() != null) {
+ BigDecimal maxComparable = toComparableValue(colStat.max());
+ if (maxComparable != null && maxComparable.compareTo(comparableVal) < 0)
+ return 0.;
+ }
+
+ double expectedRows = ((double)(colStat.total() - colStat.nulls())) / (colStat.distinct());
+
+ return expectedRows / colStat.total();
+ }
+
+ /**
+ * Estimate "is not null" selectivity by column statistics.
+ *
+ * @param colStat Column statistics.
+ * @return Selectivity.
+ */
+ private double estimateIsNotNullSelectivity(ColumnStatistics colStat) {
+ if (colStat.total() == 0)
+ return IS_NOT_NULL_SELECTIVITY;
+
+ return (double)(colStat.total() - colStat.nulls()) / colStat.total();
+ }
+
+ /**
+ * Estimate "is null" selectivity by column statistics.
+ *
+ * @param colStat Column statistics.
+ * @return Selectivity.
+ */
+ private double estimateIsNullSelectivity(ColumnStatistics colStat) {
+ if (colStat.total() == 0)
+ return IS_NULL_SELECTIVITY;
+
+ return (double)colStat.nulls() / colStat.total();
+ }
+
+ /**
+ * Get operand from given predicate.
+ *
+ * Assumes that predicat is normalized, thus operand should be only on the left side.
+ *
+ * @param pred RexNode to get operand by.
+ * @return Operand or {@code null} if it's not possible to find operand with specified type.
+ */
+ private RexSlot getOperand(RexCall pred) {
+ List<RexNode> operands = pred.getOperands();
+
+ if (operands.isEmpty() || operands.size() > 2)
+ return null;
+
+ RexNode op = operands.get(0);
+
+ if (op instanceof RexCall && op.isA(SqlKind.CAST))
+ op = ((RexCall)op).operands.get(0);
+
+ return op instanceof RexSlot ? (RexSlot)op : null;
+ }
+
+ /**
+ * Guess selectivity by predicate type only.
+ *
+ * @param pred Predicate to guess selectivity by.
+ * @return Selectivity.
+ */
+ private double guessSelectivity(RexNode pred) {
+ if (pred.getKind() == SqlKind.IS_NULL)
+ return IS_NULL_SELECTIVITY;
+ else if (pred.getKind() == SqlKind.IS_NOT_NULL)
+ return IS_NOT_NULL_SELECTIVITY;
+ else if (pred.isA(SqlKind.EQUALS))
+ return EQUALS_SELECTIVITY;
+ else if (pred.isA(SqlKind.COMPARISON))
+ return COMPARISON_SELECTIVITY;
+ else
+ return OTHER_SELECTIVITY;
+ }
+
+ /**
+ * Get selectivity of exchange by it's input selectivity.
+ *
+ * @param exch IgniteExchange.
+ * @param mq RelMetadataQuery.
+ * @param predicate Predicate.
+ * @return Selectivity or {@code null} if it can't be estimated.
+ */
+ public Double getSelectivity(IgniteExchange exch, RelMetadataQuery mq, RexNode predicate) {
+ RelNode input = exch.getInput();
+
+ if (input == null)
+ return null;
+
+ return getSelectivity(input, mq, predicate);
+ }
+
+ /**
+ * Get selectivity of table spool by it's input selectivity.
+ *
+ * @param tspool IgniteTableSpool.
+ * @param mq RelMetadataQuery.
+ * @param predicate Predicate.
+ * @return Selectivity or {@code null} if it can't be estimated.
+ */
+ public Double getSelectivity(IgniteTableSpool tspool, RelMetadataQuery mq, RexNode predicate) {
+ RelNode input = tspool.getInput();
+
+ if (input == null)
+ return null;
+
+ return getSelectivity(input, mq, predicate);
+ }
+
+ /**
+ * Get selectivity of hash index spool by it's input selectivity.
+ *
+ * @param rel IgniteHashIndexSpool.
+ * @param mq RelMetadataQuery.
+ * @param predicate Predicate.
+ * @return Selectivity or {@code null} if it can't be estimated.
+ */
public Double getSelectivity(IgniteHashIndexSpool rel, RelMetadataQuery mq, RexNode predicate) {
if (predicate != null) {
return mq.getSelectivity(rel.getInput(),
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMetadata.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMetadata.java
index b279c05..eff7bb7 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMetadata.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMetadata.java
@@ -47,6 +47,7 @@ public class IgniteMetadata {
IgniteMdPredicates.SOURCE,
IgniteMdCollation.SOURCE,
IgniteMdSelectivity.SOURCE,
+ IgniteMdColumnOrigins.SOURCE,
IgniteMdDistinctRowCount.SOURCE,
// Basic providers
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
index f061e05..41916dc 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
@@ -122,9 +122,11 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
@Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
double rows = table.getRowCount();
- double cost = rows * IgniteCost.ROW_PASS_THROUGH_COST;
+ double cost;
- if (condition != null) {
+ if (condition == null)
+ cost = rows * IgniteCost.ROW_PASS_THROUGH_COST;
+ else {
RexBuilder builder = getCluster().getRexBuilder();
double selectivity = 1;
@@ -136,10 +138,10 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
selectivity -= 1 - selectivity0;
- cost += Math.log(rows);
+ cost += Math.log(rows) * IgniteCost.ROW_COMPARISON_COST;
}
- if (upperCondition() != null && lowerCondition() != null && !lowerCondition().equals(upperCondition())) {
+ if (upperCondition() != null && (lowerCondition() == null || !lowerCondition().equals(upperCondition()))) {
double selectivity0 = mq.getSelectivity(this, RexUtil.composeConjunction(builder, upperCondition()));
selectivity -= 1 - selectivity0;
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
index b9b568d..0967394 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/ProjectableFilterableTableScan.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.processors.query.calcite.rel;
import java.util.ArrayList;
import java.util.List;
+
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptPlanner;
@@ -30,6 +31,7 @@ import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.metadata.RelColumnOrigin;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexInputRef;
@@ -177,4 +179,16 @@ public abstract class ProjectableFilterableTableScan extends TableScan {
return RexUtil.composeConjunction(builder(getCluster()), conjunctions, true);
}
+
+ /**
+ * Get column origin by local ref idx (required column or base tables column idx).
+ *
+ * @param colIdx Column idx.
+ * @return Set of column origins for the given idx or {@code null} if unable to found it.
+ */
+ public RelColumnOrigin columnOriginsByRelLocalRef(int colIdx) {
+ int originColIdx = (requiredColumns() == null) ? colIdx : requiredColumns().toArray()[colIdx];
+
+ return new RelColumnOrigin(getTable(), originColIdx, false);
+ }
}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteStatisticsImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteStatisticsImpl.java
new file mode 100644
index 0000000..0547e57
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteStatisticsImpl.java
@@ -0,0 +1,107 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.schema;
+
+import java.util.List;
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelReferentialConstraint;
+import org.apache.calcite.schema.Statistic;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.internal.processors.query.stat.ColumnStatistics;
+import org.apache.ignite.internal.processors.query.stat.ObjectStatisticsImpl;
+
+/** Calcite statistics wrapper. */
+public class IgniteStatisticsImpl implements Statistic {
+ /** Internal statistics implementation. */
+ private final ObjectStatisticsImpl statistics;
+
+ /** Grid table. */
+ private final GridH2Table tbl;
+
+ /**
+ * Constructor.
+ *
+ * @param statistics Internal object statistics.
+ */
+ public IgniteStatisticsImpl(ObjectStatisticsImpl statistics) {
+ this.statistics = statistics;
+ tbl = null;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param tbl Base grid table.
+ */
+ public IgniteStatisticsImpl(GridH2Table tbl) {
+ statistics = null;
+ this.tbl = tbl;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Double getRowCount() {
+ long rows;
+
+ if (statistics != null)
+ rows = statistics.rowCount();
+ else if (tbl != null)
+ rows = tbl.getRowCountApproximationNoCheck();
+ else
+ rows = 1000;
+
+ return (double)rows;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isKey(ImmutableBitSet cols) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<ImmutableBitSet> getKeys() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<RelReferentialConstraint> getReferentialConstraints() {
+ return ImmutableList.of();
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<RelCollation> getCollations() {
+ return ImmutableList.of();
+ }
+
+ /** {@inheritDoc} */
+ @Override public IgniteDistribution getDistribution() {
+ return null;
+ }
+
+ /**
+ * Get column statistics.
+ *
+ * @param colName Column name.
+ * @return Column statistics or {@code null} if there are no statistics for specified column.
+ */
+ public ColumnStatistics getColumnStatistics(String colName) {
+ return (statistics == null) ? null : statistics.columnStatistics(colName);
+ }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteTableImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteTableImpl.java
index 729d2ce..4343553 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteTableImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteTableImpl.java
@@ -24,12 +24,8 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
-
-import com.google.common.collect.ImmutableList;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptTable;
-import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.RelReferentialConstraint;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rex.RexNode;
@@ -48,6 +44,8 @@ import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribut
import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.internal.processors.query.stat.ObjectStatisticsImpl;
+import org.apache.ignite.internal.processors.query.stat.StatisticsKey;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;
@@ -59,16 +57,13 @@ public class IgniteTableImpl extends AbstractTable implements IgniteTable {
private final TableDescriptor desc;
/** */
- private final Statistic statistic;
-
- /** */
private final GridKernalContext ctx;
/** */
- private volatile GridH2Table tbl;
+ private final Map<String, IgniteIndex> indexes = new ConcurrentHashMap<>();
/** */
- private final Map<String, IgniteIndex> indexes = new ConcurrentHashMap<>();
+ private volatile GridH2Table tbl;
/**
* @param ctx Kernal context.
@@ -77,7 +72,6 @@ public class IgniteTableImpl extends AbstractTable implements IgniteTable {
public IgniteTableImpl(GridKernalContext ctx, TableDescriptor desc) {
this.ctx = ctx;
this.desc = desc;
- statistic = new StatisticsImpl();
}
/** {@inheritDoc} */
@@ -87,16 +81,21 @@ public class IgniteTableImpl extends AbstractTable implements IgniteTable {
/** {@inheritDoc} */
@Override public Statistic getStatistic() {
- if (tbl == null) {
- IgniteH2Indexing idx = (IgniteH2Indexing)ctx.query().getIndexing();
+ IgniteH2Indexing idx = (IgniteH2Indexing)ctx.query().getIndexing();
- final String tblName = desc.typeDescription().tableName();
- final String schemaName = desc.typeDescription().schemaName();
+ final String tblName = desc.typeDescription().tableName();
+ final String schemaName = desc.typeDescription().schemaName();
+ ObjectStatisticsImpl statistics = (ObjectStatisticsImpl)idx.statsManager().getLocalStatistics(
+ new StatisticsKey(schemaName, tblName));
+
+ if (statistics != null)
+ return new IgniteStatisticsImpl(statistics);
+
+ if (tbl == null)
tbl = idx.schemaManager().dataTable(schemaName, tblName);
- }
- return statistic;
+ return new IgniteStatisticsImpl(tbl);
}
/** {@inheritDoc} */
@@ -193,39 +192,4 @@ public class IgniteTableImpl extends AbstractTable implements IgniteTable {
}
}
}
-
- /** */
- private class StatisticsImpl implements Statistic {
- /** {@inheritDoc} */
- @Override public Double getRowCount() {
- long rows = tbl.getRowCountApproximationNoCheck();
-
- return (double)rows;
- }
-
- /** {@inheritDoc} */
- @Override public boolean isKey(ImmutableBitSet cols) {
- return false; // TODO
- }
-
- /** {@inheritDoc} */
- @Override public List<ImmutableBitSet> getKeys() {
- return null; // TODO
- }
-
- /** {@inheritDoc} */
- @Override public List<RelReferentialConstraint> getReferentialConstraints() {
- return ImmutableList.of();
- }
-
- /** {@inheritDoc} */
- @Override public List<RelCollation> getCollations() {
- return ImmutableList.of(); // The method isn't used
- }
-
- /** {@inheritDoc} */
- @Override public IgniteDistribution getDistribution() {
- return distribution();
- }
- }
}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptor.java
index abc7dcf..74c9e51 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptor.java
@@ -146,7 +146,8 @@ public interface TableDescriptor extends RelProtoDataType, InitializerExpression
/**
* Returns column descriptor for given field name.
*
- * @return Column descriptor
+ * @param fieldName Field name.
+ * @return Column descriptor.
*/
ColumnDescriptor columnDescriptor(String fieldName);
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteBasicSecondaryIndexIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteBasicSecondaryIndexIntegrationTest.java
index ae86dff..87cf90f 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteBasicSecondaryIndexIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteBasicSecondaryIndexIntegrationTest.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.processors.query.calcite;
import java.util.LinkedHashMap;
+
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.cache.CacheMode;
@@ -46,7 +47,6 @@ import static org.apache.ignite.internal.processors.query.calcite.QueryChecker.c
import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.AFFINITY_KEY_IDX_NAME;
import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.PK_IDX_NAME;
import static org.apache.ignite.internal.processors.query.h2.opt.GridH2Table.generateProxyIdxName;
-import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.not;
/**
@@ -774,12 +774,7 @@ public class CalciteBasicSecondaryIndexIntegrationTest extends GridCommonAbstrac
@Test
public void testOrCondition1() {
assertQuery("SELECT * FROM Developer WHERE name='Mozart' OR age=55")
- .matches(containsUnion(true))
- .matches(anyOf(
- containsIndexScan("PUBLIC", "DEVELOPER", NAME_CITY_IDX),
- containsIndexScan("PUBLIC", "DEVELOPER", NAME_DEPID_CITY_IDX))
- )
- .matches(containsAnyScan("PUBLIC", "DEVELOPER"))
+ .matches(containsTableScan("PUBLIC", "DEVELOPER"))
.returns(1, "Mozart", 3, "Vienna", 33)
.returns(3, "Bach", 1, "Leipzig", 55)
.check();
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
index 66977b6..42732f6 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
@@ -40,6 +40,7 @@ import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.testframework.GridTestUtils;
import org.hamcrest.CoreMatchers;
+import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.core.SubstringMatcher;
@@ -90,6 +91,44 @@ public abstract class QueryChecker {
}
/**
+ * Ignite result row count matсher.
+ *
+ * @param rowCount Expected result row count.
+ * @return Mather.
+ */
+ public static Matcher<String> containsResultRowCount(double rowCount) {
+ String rowCountStr = String.format(".*rowcount = %s,.*", rowCount);
+
+ return new RegexpMather(rowCountStr);
+ }
+
+ /**
+ * Regexp string matсher.
+ */
+ private static class RegexpMather extends CustomTypeSafeMatcher<String> {
+ /** Compilled pathern. */
+ private Pattern pattern;
+
+ /**
+ * Constructor.
+ *
+ * @param regexp Regexp to search.
+ */
+ public RegexpMather(String regexp) {
+ super(regexp);
+
+ pattern = Pattern.compile(regexp, Pattern.DOTALL);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected boolean matchesSafely(String item) {
+ java.util.regex.Matcher matcher = pattern.matcher(item);
+
+ return matcher.matches();
+ }
+ }
+
+ /**
* Ignite table|index scan with projects unmatcher.
*
* @param schema Schema name.
@@ -315,7 +354,7 @@ public abstract class QueryChecker {
if (!F.isEmpty(planMatchers)) {
for (Matcher<String> matcher : planMatchers)
- assertThat("Invalid plan:\n" + actualPlan, actualPlan, matcher);
+ assertThat("Invalid plan:\n" + actualPlan + "\n for query: " + qry, actualPlan, matcher);
}
if (exactPlan != null)
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryCheckerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryCheckerTest.java
index 045c235..d125e3d 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryCheckerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryCheckerTest.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.processors.query.calcite;
import org.hamcrest.Matcher;
import org.junit.Test;
+import static org.apache.ignite.internal.processors.query.calcite.QueryChecker.containsResultRowCount;
import static org.apache.ignite.internal.processors.query.calcite.QueryChecker.matchesOnce;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -29,7 +30,7 @@ public class QueryCheckerTest {
/** */
@Test
public void testMatchesOnce() {
- String plan = "PLAN=IgniteExchange(distribution=[single])\n " +
+ String planMatchesOnce = "PLAN=IgniteExchange(distribution=[single])\n " +
"IgniteProject(NAME=[$2])\n " +
"IgniteTableScan(table=[[PUBLIC, DEVELOPER]], projects=[[$t0]], requiredColunms=[{2}])\n " +
"IgniteTableScan(table=[[PUBLIC, DEVELOPER]], projects=[[$t1]], requiredColunms=[{2, 3}])";
@@ -37,7 +38,24 @@ public class QueryCheckerTest {
Matcher<String> matcherTbl = matchesOnce("IgniteTableScan");
Matcher<String> matcherPrj = matchesOnce("IgniteProject");
- assertFalse(matcherTbl.matches(plan));
- assertTrue(matcherPrj.matches(plan));
+ assertFalse(matcherTbl.matches(planMatchesOnce));
+ assertTrue(matcherPrj.matches(planMatchesOnce));
+ }
+
+ /**
+ * Check that result row query matcher match result rows and doesn't match scan row count.
+ */
+ @Test
+ public void testContainsResultRow() {
+ String plan = " IgniteMapHashAggregate(group=[{}], COUNT(NAME)=[COUNT($0)]): rowcount = 1.0, " +
+ "cumulative cost = IgniteCost [rowCount=2000.0, cpu=2000.0, memory=5.0, io=0.0, network=0.0], id = 43\n" +
+ " IgniteTableScan(table=[[PUBLIC, PERSON]], requiredColumns=[{2}]): rowcount = 1000.0, " +
+ "cumulative cost = IgniteCost [rowCount=1000.0, cpu=1000.0, memory=0.0, io=0.0, network=0.0], id = 34";
+
+ Matcher<String> containsScan = containsResultRowCount(2000);
+ Matcher<String> containsResult = containsResultRowCount(1);
+
+ assertFalse(containsScan.matches(plan));
+ assertTrue(containsResult.matches(plan));
}
}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
index 8004e0d..950b74f 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
@@ -40,12 +40,9 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
/** */
protected static IgniteEx client;
- /** */
- protected static final int GRID_CNT = 3;
-
/** {@inheritDoc} */
@Override protected void beforeTestsStarted() throws Exception {
- startGrids(GRID_CNT);
+ startGrids(nodeCount());
client = startClientGrid("client");
}
@@ -61,6 +58,11 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
}
/** */
+ protected int nodeCount() {
+ return 3;
+ }
+
+ /** */
protected void cleanQueryPlanCache() {
for (Ignite ign : G.allGrids()) {
CalciteQueryProcessor qryProc = (CalciteQueryProcessor)Commons.lookupComponent(
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
index 83e50c2..f4bd515 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
@@ -128,7 +128,7 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
person.clear();
- for (int gridIdx = 0; gridIdx < GRID_CNT; gridIdx++)
+ for (int gridIdx = 0; gridIdx < nodeCount(); gridIdx++)
person.put(primaryKey(grid(gridIdx).cache(cacheName)), new Employer(gridIdx == 0 ? "Emp" : null, 0.0d));
GridTestUtils.assertThrowsWithCause(() -> assertQuery("SELECT (SELECT name FROM person)").check(),
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteErrorHandlilngIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteErrorHandlilngIntegrationTest.java
index f769ca6..8b65fc7 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteErrorHandlilngIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteErrorHandlilngIntegrationTest.java
@@ -22,6 +22,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
+
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.cache.query.SqlFieldsQuery;
@@ -201,18 +202,14 @@ public class CalciteErrorHandlilngIntegrationTest extends GridCommonAbstractTest
sql(client, "create table test (id integer primary key, val varchar)");
sql(client, "create index test_id_idx on test (id)");
- sql(client, "insert into test values (0, 'val_0');");
- sql(client, "insert into test values (1, 'val_0');");
- sql(client, "insert into test values (2, 'val_0');");
- sql(client, "insert into test values (3, 'val_0');");
awaitPartitionMapExchange(true, true, null);
shouldThrow.set(true);
List<String> sqls = F.asList(
- "select id from test where id > -10",
- "select max(id) from test where id > -10"
+ "select /*+ DISABLE_RULE('LogicalTableScanConverterRule') */ id from test where id > -10",
+ "select /*+ DISABLE_RULE('LogicalTableScanConverterRule') */ max(id) from test where id > -10"
);
for (String sql : sqls) {
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java
new file mode 100644
index 0000000..a7d2db4
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java
@@ -0,0 +1,614 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.integration;
+
+import java.math.BigInteger;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.QueryEngine;
+import org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.internal.processors.query.stat.IgniteStatisticsManager;
+import org.apache.ignite.internal.processors.query.stat.StatisticsKey;
+import org.apache.ignite.internal.processors.query.stat.StatisticsTarget;
+import org.apache.ignite.internal.processors.query.stat.config.StatisticsObjectConfiguration;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.junit.Test;
+
+/**
+ * Tests for server side statistics usage.
+ */
+public class ServerStatisticsIntegrationTest extends AbstractBasicIntegrationTest {
+ /** Server instance. */
+ private IgniteEx srv;
+
+ /** All types table row count. */
+ private static final int ROW_COUNT = 100;
+
+ /** All types table nullable fields. */
+ private static final String[] NULLABLE_FIELDS = {
+ "string_field",
+ "boolean_obj_field",
+ "short_obj_field",
+ "integer_field",
+ "long_obj_field",
+ "float_obj_field",
+ "double_obj_field",
+ };
+
+ /** All types table non nullable fields. */
+ private static final String[] NON_NULLABLE_FIELDS = {
+ "short_field",
+ "int_field",
+ "long_field",
+ "float_field",
+ "double_field"
+ };
+
+ /** All types table numeric fields. */
+ private static final String[] NUMERIC_FIELDS = {
+ "short_obj_field",
+ "integer_field",
+ "long_obj_field",
+ "float_obj_field",
+ "double_obj_field",
+ "short_field",
+ "int_field",
+ "long_field",
+ "float_field",
+ "double_field"
+ };
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ createAndPopulateAllTypesTable(0, ROW_COUNT);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected int nodeCount() {
+ return 1;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() {
+ cleanQueryPlanCache();
+ }
+
+ /**
+ * Run select and check that result rows take statisitcs in account:
+ * 1) without statistics - by row count and heuristic;
+ * 2) with statistics - by statistics;
+ * 3) after deleting statistics - by row count and heuristics again.
+ */
+ @Test
+ public void testQueryCostWithStatistics() throws IgniteCheckedException {
+ String sql = "select name from person where salary is not null";
+ createAndPopulateTable();
+ StatisticsKey key = new StatisticsKey("PUBLIC", "PERSON");
+ srv = ignite(0);
+
+ assertQuerySrv(sql).matches(QueryChecker.containsResultRowCount(4.5)).check();
+
+ clearQryCache(srv);
+
+ collectStatistics(srv, key);
+
+ assertQuerySrv(sql).matches(QueryChecker.containsResultRowCount(5)).check();
+
+ statMgr(srv).dropStatistics(new StatisticsTarget(key));
+ clearQryCache(srv);
+
+ assertQuerySrv(sql).matches(QueryChecker.containsResultRowCount(4.5)).check();
+ }
+
+ /**
+ * Check is null/is not null conditions for nullable and non nullable fields.
+ */
+ @Test
+ public void testNullConditions() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ String sql = "select * from all_types ";
+
+ for (String nullableField : NULLABLE_FIELDS) {
+ assertQuerySrv(sql + "where " + nullableField + " is null")
+ .matches(QueryChecker.containsResultRowCount(25.)).check();
+
+ assertQuerySrv(sql + "where " + nullableField + " is not null")
+ .matches(QueryChecker.containsResultRowCount(75.)).check();
+ }
+
+ for (String nonNullableField : NON_NULLABLE_FIELDS) {
+ assertQuerySrv(sql + "where " + nonNullableField + " is null")
+ .matches(QueryChecker.containsResultRowCount(1.)).check();
+
+ assertQuerySrv(sql + "where " + nonNullableField + " is not null")
+ .matches(QueryChecker.containsResultRowCount(ROW_COUNT)).check();
+ }
+ }
+
+ /**
+ * Test multiple condition for the same query.
+ *
+ * @throws IgniteCheckedException In case of errors.
+ */
+ @Test
+ public void testMultipleConditionQuery() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+
+ for (String numericField : NUMERIC_FIELDS) {
+ double allRowCnt = (nonNullableFields.contains(numericField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ String fieldSql = String.format("select * from all_types where %s > -100 and %s > 0", numericField,
+ numericField);
+
+ assertQuerySrv(fieldSql).matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+
+ fieldSql = String.format("select * from all_types where %s < 1000 and %s < 101", numericField,
+ numericField);
+
+ assertQuerySrv(fieldSql).matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+
+ fieldSql = String.format("select * from all_types where %s > -100 and %s < 1000", numericField,
+ numericField);
+
+ assertQuerySrv(fieldSql).matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+ }
+ }
+
+ /**
+ * Check range condition with not null conditions.
+ *
+ * @throws IgniteCheckedException In case of error.
+ */
+ @Test
+ public void testNonNullMultipleConditionQuery() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+
+ // time
+ String timeSql = "select * from all_types where time_field is not null";
+
+ assertQuerySrv(timeSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ timeSql += " and time_field > '00:00:00'";
+
+ assertQuerySrv(timeSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ // date
+ String dateSql = "select * from all_types where date_field is not null";
+
+ assertQuerySrv(dateSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ dateSql += " and date_field > '1000-01-01'";
+
+ assertQuerySrv(dateSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ // timestamp
+ String timestampSql = "select * from all_types where timestamp_field is not null ";
+
+ assertQuerySrv(timestampSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ timestampSql += " and timestamp_field > '1000-01-10 11:59:59'";
+
+ assertQuerySrv(timestampSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ // numeric fields
+ for (String numericField : NUMERIC_FIELDS) {
+ double allRowCnt = (nonNullableFields.contains(numericField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ String fieldSql = String.format("select * from all_types where %s is not null", numericField);
+
+ assertQuerySrv(fieldSql).matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+
+ fieldSql = String.format("select * from all_types where %s is not null and %s > 0", numericField,
+ numericField);
+
+ assertQuerySrv(fieldSql).matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+ }
+ }
+
+ /**
+ * Check condition with projections:
+ *
+ * 1) Condition on the one of fields in select list.
+ * 2) Confition on the field not from select list.
+ *
+ * @throws IgniteCheckedException In case of errors.
+ */
+ @Test
+ public void testProjections() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ String sql = "select %s, %s from all_types where %s < " + ROW_COUNT;
+
+ String sql2 = "select %s from all_types where %s >= " + (-ROW_COUNT);
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+
+ for (int firstFieldIdx = 0; firstFieldIdx < NUMERIC_FIELDS.length - 1; firstFieldIdx++) {
+ String firstField = NUMERIC_FIELDS[firstFieldIdx];
+ double firstAllRowCnt = (nonNullableFields.contains(firstField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ for (int secFieldIdx = firstFieldIdx + 1; secFieldIdx < NUMERIC_FIELDS.length; secFieldIdx++) {
+ String secField = NUMERIC_FIELDS[secFieldIdx];
+
+ double secAllRowCnt = (nonNullableFields.contains(secField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ String qry = String.format(sql, secField, firstField, secField);
+
+ assertQuerySrv(qry).matches(QueryChecker.containsResultRowCount(secAllRowCnt)).check();
+
+ qry = String.format(sql, firstField, secField, firstField);
+
+ assertQuerySrv(qry).matches(QueryChecker.containsResultRowCount(firstAllRowCnt)).check();
+
+ qry = String.format(sql2, firstField, secField);
+
+ assertQuerySrv(qry).matches(QueryChecker.containsResultRowCount(secAllRowCnt)).check();
+
+ qry = String.format(sql2, secField, firstField);
+
+ assertQuerySrv(qry).matches(QueryChecker.containsResultRowCount(firstAllRowCnt)).check();
+ }
+ }
+ }
+
+ /**
+ * Test not null counting with two range conjuncted condition on one and two columns.
+ *
+ * @throws IgniteCheckedException In case of errors.
+ */
+ @Test
+ public void testNotNullCountingSelectivity() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+
+ for (String numericField : NUMERIC_FIELDS) {
+ double allRowCnt = (nonNullableFields.contains(numericField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ assertQuerySrv(String.format("select * from all_types where " +
+ "%s > %d and %s < %d", numericField, -1, numericField, 101))
+ .matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+
+ assertQuerySrv(String.format("select /*+ DISABLE_RULE('LogicalOrToUnionRule') */ * from all_types where " +
+ "(%s > %d and %s < %d) or " +
+ "(int_field > -1 and int_field < 101)",
+ numericField, -1, numericField, 101))
+ .matches(QueryChecker.containsResultRowCount(ROW_COUNT)).check();
+ }
+ }
+
+ /**
+ * Test disjunctions selectivity for each column:
+ * 1) with select all conditions
+ * 2) with select none conditions
+ * 3) with is null or select all conditions
+ * 4) with is null or select none conditions
+ *
+ * @throws IgniteCheckedException In case of errors.
+ */
+ @Test
+ public void testDisjunctionSelectivity() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+
+ for (String numericField : NUMERIC_FIELDS) {
+ double allRowIsNullCnt = (nonNullableFields.contains(numericField)) ? (double) ROW_COUNT : 0.8125 * ROW_COUNT;
+ double allRowRangeCnt = (nonNullableFields.contains(numericField)) ? (double) ROW_COUNT : 0.75 * ROW_COUNT;
+
+ assertQuerySrv(String.format("select * from all_types where " +
+ "%s > %d or %s < %d", numericField, -1, numericField, 101))
+ .matches(QueryChecker.containsResultRowCount(allRowRangeCnt)).check();
+
+ assertQuerySrv(String.format("select * from all_types where " +
+ "%s > %d or %s < %d", numericField, 101, numericField, -1))
+ .matches(QueryChecker.containsResultRowCount(1.)).check();
+
+ assertQuerySrv(String.format("select * from all_types where " +
+ "%s > %d or %s is null", numericField, -1, numericField))
+ .matches(QueryChecker.containsResultRowCount(allRowIsNullCnt)).check();
+
+ assertQuerySrv(String.format("select * from all_types where " +
+ "%s > %d or %s is null", numericField, 101, numericField))
+ .matches(QueryChecker.containsResultRowCount(
+ nonNullableFields.contains(numericField) ? 1. : (double)ROW_COUNT * 0.25)).check();
+ }
+ }
+
+ /**
+ * Check randge with min/max borders.
+ */
+ @Test
+ public void testBorders() throws IgniteCheckedException {
+ StatisticsKey key = new StatisticsKey("PUBLIC", "ALL_TYPES");
+ srv = ignite(0);
+
+ collectStatistics(srv, key);
+
+ // time
+ String timeSql = "select * from all_types where time_field > '00:00:00'";
+
+ assertQuerySrv(timeSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ // date
+ String dateSql = "select * from all_types where date_field > '1000-01-10'";
+
+ assertQuerySrv(dateSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ // timestamp
+ String timestampSql = "select * from all_types where timestamp_field > '1000-01-10 11:59:59'";
+
+ assertQuerySrv(timestampSql).matches(QueryChecker.containsResultRowCount(ROW_COUNT * 0.75)).check();
+
+ String sql = "select * from all_types ";
+
+ Set<String> nonNullableFields = new HashSet<>(Arrays.asList(NON_NULLABLE_FIELDS));
+ for (String numericField : NUMERIC_FIELDS) {
+ double allRowCnt = (nonNullableFields.contains(numericField)) ? (double)ROW_COUNT : 0.75 * ROW_COUNT;
+
+ String fieldSql = sql + "where " + numericField;
+
+ assertQuerySrv(fieldSql + " < -1").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " < 0").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " <= 0").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " >= 0").matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+ assertQuerySrv(fieldSql + " > 0").matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+
+ assertQuerySrv(fieldSql + " > 101").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " > 100").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " >= 100").matches(QueryChecker.containsResultRowCount(1.)).check();
+ assertQuerySrv(fieldSql + " <= 100").matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+ assertQuerySrv(fieldSql + " < 100").matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
+ }
+ }
+
+ /**
+ * Clear query cache in specified node.
+ *
+ * @param ign Ignite node to clear calcite query cache on.
+ */
+ protected void clearQryCache(IgniteEx ign) {
+ CalciteQueryProcessor qryProc = (CalciteQueryProcessor)Commons.lookupComponent(
+ (ign).context(), QueryEngine.class);
+
+ qryProc.queryPlanCache().clear();
+ }
+
+ /**
+ * Collect statistics by speicifed key on specified node.
+ *
+ * @param ign Node to collect statistics on.
+ * @param key Statistics key to collect statistics by.
+ * @throws IgniteCheckedException In case of errors.
+ */
+ protected void collectStatistics(IgniteEx ign, StatisticsKey key) throws IgniteCheckedException {
+ IgniteStatisticsManager statMgr = statMgr(ign);
+
+ statMgr.collectStatistics(new StatisticsObjectConfiguration(key));
+
+ assertTrue(GridTestUtils.waitForCondition(() -> statMgr.getLocalStatistics(key) != null, 1000));
+ }
+
+ /**
+ * Get statistics manager.
+ *
+ * @param ign Node to get statistics manager from.
+ * @return IgniteStatisticsManager.
+ */
+ protected IgniteStatisticsManager statMgr(IgniteEx ign) {
+ IgniteH2Indexing indexing = (IgniteH2Indexing)ign.context().query().getIndexing();
+
+ return indexing.statsManager();
+ }
+
+ /** */
+ protected QueryChecker assertQuerySrv(String qry) {
+ return new QueryChecker(qry) {
+ @Override protected QueryEngine getEngine() {
+ return Commons.lookupComponent(srv.context(), QueryEngine.class);
+ }
+ };
+ }
+
+ /**
+ * Create (if not exists) and populate cache with all types.
+ *
+ * @param start First key idx.
+ * @param count Rows count.
+ * @return Populated cache.
+ */
+ protected IgniteCache<Integer, AllTypes> createAndPopulateAllTypesTable(int start, int count) {
+ IgniteCache<Integer, AllTypes> all_types = grid(0).getOrCreateCache(new CacheConfiguration<Integer, AllTypes>()
+ .setName("all_types")
+ .setSqlSchema("PUBLIC")
+ .setQueryEntities(F.asList(new QueryEntity(Integer.class, AllTypes.class).setTableName("all_types")))
+ .setBackups(2)
+ );
+
+ for (int i = start; i < start + count; i++) {
+ boolean null_values = (i & 3) == 1;
+
+ all_types.put(i, new AllTypes(i, null_values));
+ }
+
+ return all_types;
+ }
+
+ /**
+ * Test class with fields of all types.
+ */
+ public static class AllTypes {
+ /** */
+ @QuerySqlField(name = "string_field")
+ public String stringField;
+
+ /** */
+ @QuerySqlField(name = "byte_arr_field")
+ public byte[] byteArrField;
+
+ /** */
+ @QuerySqlField(name = "boolean_field")
+ public boolean booleanField;
+
+ /** */
+ @QuerySqlField(name = "boolean_obj_field")
+ public Boolean booleanObjField;
+
+ /** */
+ @QuerySqlField(name = "short_field")
+ public short shortField;
+
+ /** */
+ @QuerySqlField(name = "short_obj_field")
+ public Short shortObjField;
+
+ /** */
+ @QuerySqlField(name = "int_field")
+ public int intField;
+
+ /** */
+ @QuerySqlField(name = "integer_field")
+ public Integer integerField;
+
+ /** */
+ @QuerySqlField(name = "long_field")
+ public long longField;
+
+ /** */
+ @QuerySqlField(name = "long_obj_field")
+ public Long longObjField;
+
+ /** */
+ @QuerySqlField(name = "float_field")
+ public float floatField;
+
+ /** */
+ @QuerySqlField(name = "float_obj_field")
+ public Float floatObjField;
+
+ /** */
+ @QuerySqlField(name = "double_field")
+ public double doubleField;
+
+ /** */
+ @QuerySqlField(name = "double_obj_field")
+ public Double doubleObjField;
+
+ /** */
+ @QuerySqlField(name = "date_field")
+ public Date dateField;
+
+ /** */
+ @QuerySqlField(name = "time_field")
+ public Time timeField;
+
+ /** */
+ @QuerySqlField(name = "timestamp_field")
+ public Timestamp timestampField;
+
+ /**
+ * Constructor.
+ *
+ * @param i idx to generate all fields values by.
+ * @param null_val Should object fields be equal to {@code null}.
+ */
+ public AllTypes(int i, boolean null_val) {
+ stringField = (null_val) ? null : "string_field_value" + i;
+ byteArrField = (null_val) ? null : BigInteger.valueOf(i).toByteArray();
+ booleanField = (i & 1) == 0;
+ booleanObjField = (null_val) ? null : (i & 1) == 0;
+ shortField = (short)i;
+ shortObjField = (null_val) ? null : shortField;
+ intField = i;
+ integerField = (null_val) ? null : i;
+ longField = i;
+ longObjField = (null_val) ? null : longField;
+ floatField = i;
+ floatObjField = (null_val) ? null : floatField;
+ doubleField = i;
+ doubleObjField = (null_val) ? null : doubleField;
+ dateField = (null_val) ? null : Date.valueOf(String.format("%04d-04-09", 1000 + i));
+ timeField = (null_val) ? null : new Time(i * 1000);
+ timestampField = (null_val) ? null : Timestamp.valueOf(String.format("%04d-04-09 12:00:00", 1000 + i));
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "AllTypes{" +
+ "stringField='" + stringField + '\'' +
+ ", byteArrField=" + byteArrField +
+ ", booleanField=" + booleanField +
+ ", boolean_obj_field=" + booleanObjField +
+ ", short_field=" + shortField +
+ ", short_obj_field=" + shortObjField +
+ ", int_field=" + intField +
+ ", Integer_field=" + integerField +
+ ", long_field=" + longField +
+ ", long_obj_field=" + longObjField +
+ ", float_field=" + floatField +
+ ", float_obj_field=" + floatObjField +
+ ", double_field=" + doubleField +
+ ", double_obj_field=" + doubleObjField +
+ ", date_field=" + dateField +
+ ", time_field=" + timeField +
+ ", timestamp_field=" + timestampField +
+ '}';
+ }
+ }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
index 16abb35..a584d3a 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
@@ -38,9 +38,7 @@ import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.AbstractRelNode;
import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.RelDistribution;
import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.RelReferentialConstraint;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.RelVisitor;
import org.apache.calcite.rel.core.TableModify;
@@ -91,11 +89,13 @@ import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLog
import org.apache.ignite.internal.processors.query.calcite.schema.ColumnDescriptor;
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteStatisticsImpl;
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
import org.apache.ignite.internal.processors.query.calcite.schema.TableDescriptor;
import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
+import org.apache.ignite.internal.processors.query.stat.ObjectStatisticsImpl;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.lang.IgniteBiTuple;
import org.apache.ignite.plugin.extensions.communication.Message;
@@ -610,7 +610,7 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
}
/** */
- abstract static class TestTable implements IgniteTable {
+ protected static class TestTable implements IgniteTable {
/** */
private final String name;
@@ -621,7 +621,10 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
private final Map<String, IgniteIndex> indexes = new HashMap<>();
/** */
- private final double rowCnt;
+ private IgniteDistribution distribution;
+
+ /** */
+ private IgniteStatisticsImpl statistics;
/** */
private final TableDescriptor desc;
@@ -639,12 +642,36 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
/** */
TestTable(String name, RelDataType type, double rowCnt) {
protoType = RelDataTypeImpl.proto(type);
- this.rowCnt = rowCnt;
+ statistics = new IgniteStatisticsImpl(new ObjectStatisticsImpl((long)rowCnt, Collections.emptyMap()));
this.name = name;
desc = new TestTableDescriptor(this::distribution, type);
}
+ /**
+ * Set table distribution.
+ *
+ * @param distribution Table distribution to set.
+ * @return TestTable for chaining.
+ */
+ public TestTable setDistribution(IgniteDistribution distribution) {
+ this.distribution = distribution;
+
+ return this;
+ }
+
+ /**
+ * Set table statistics;
+ *
+ * @param statistics Statistics to set.
+ * @return TestTable for chaining.
+ */
+ public TestTable setStatistics(IgniteStatisticsImpl statistics) {
+ this.statistics = statistics;
+
+ return this;
+ }
+
/** {@inheritDoc} */
@Override public IgniteLogicalTableScan toRel(
RelOptCluster cluster,
@@ -684,37 +711,7 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
/** {@inheritDoc} */
@Override public Statistic getStatistic() {
- return new Statistic() {
- /** {@inheritDoc */
- @Override public Double getRowCount() {
- return rowCnt;
- }
-
- /** {@inheritDoc */
- @Override public boolean isKey(ImmutableBitSet cols) {
- return false;
- }
-
- /** {@inheritDoc */
- @Override public List<ImmutableBitSet> getKeys() {
- throw new AssertionError();
- }
-
- /** {@inheritDoc */
- @Override public List<RelReferentialConstraint> getReferentialConstraints() {
- throw new AssertionError();
- }
-
- /** {@inheritDoc */
- @Override public List<RelCollation> getCollations() {
- return Collections.emptyList();
- }
-
- /** {@inheritDoc */
- @Override public RelDistribution getDistribution() {
- throw new AssertionError();
- }
- };
+ return statistics;
}
/** {@inheritDoc} */
@@ -754,6 +751,9 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
/** {@inheritDoc} */
@Override public IgniteDistribution distribution() {
+ if (distribution != null)
+ return distribution;
+
throw new AssertionError();
}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/StatisticsPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/StatisticsPlannerTest.java
new file mode 100644
index 0000000..8fe5df0
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/StatisticsPlannerTest.java
@@ -0,0 +1,460 @@
+/*
+ * 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.ignite.internal.processors.query.calcite.planner;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.util.ImmutableIntList;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteStatisticsImpl;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
+import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
+import org.apache.ignite.internal.processors.query.stat.ColumnStatistics;
+import org.apache.ignite.internal.processors.query.stat.ObjectStatisticsImpl;
+import org.h2.value.ValueBoolean;
+import org.h2.value.ValueByte;
+import org.h2.value.ValueDate;
+import org.h2.value.ValueDouble;
+import org.h2.value.ValueFloat;
+import org.h2.value.ValueInt;
+import org.h2.value.ValueLong;
+import org.h2.value.ValueShort;
+import org.h2.value.ValueString;
+import org.h2.value.ValueTime;
+import org.h2.value.ValueTimestamp;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Statistic related simple tests.
+ */
+public class StatisticsPlannerTest extends AbstractPlannerTest {
+ /** */
+ private IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
+
+ /** */
+ private static final Date MIN_DATE = Date.valueOf("1980-04-09");
+
+ /** */
+ private static final Date MAX_DATE = Date.valueOf("2020-04-09");
+
+ /** */
+ private static final Time MIN_TIME = Time.valueOf("09:00:00");
+
+ /** */
+ private static final Time MAX_TIME = Time.valueOf("21:59:59");
+
+ /** */
+ private static final Timestamp MIN_TIMESTAMP = Timestamp.valueOf("1980-04-09 09:00:00");
+
+ /** */
+ private static final Timestamp MAX_TIMESTAMP = Timestamp.valueOf("2020-04-09 21:59:59");
+
+ /** */
+ private RelDataType tbl1rt;
+
+ private Set<String> tbl1NumericFields = new HashSet<>();
+
+ /** Base table with all types. */
+ private TestTable tbl1;
+
+ /** Equal to tbl1 with some complex indexes. */
+ private TestTable tbl4;
+
+ /** */
+ private IgniteSchema publicSchema;
+
+ /** */
+ private IgniteStatisticsImpl tbl1stat;
+
+ /** {@inheritDoc} */
+ @Before
+ @Override public void setup() {
+ super.setup();
+
+ int t1rc = 1000;
+
+ tbl1NumericFields.addAll(Arrays.asList("T1C1INT", "T1C3DBL", "T1C4BYTE", "T1C7SHORT", "T1C8LONG", "T1C9FLOAT"));
+
+ tbl1rt = new RelDataTypeFactory.Builder(f)
+ .add("T1C1INT", f.createJavaType(Integer.class))
+ .add("T1C2STR", f.createJavaType(String.class))
+ .add("T1C3DBL", f.createJavaType(Double.class))
+ .add("T1C4BYTE", f.createJavaType(Byte.class))
+ .add("T1C5BOOLEAN", f.createJavaType(Boolean.class))
+ .add("T1C6CHARACTER", f.createJavaType(Character.class))
+ .add("T1C7SHORT", f.createJavaType(Short.class))
+ .add("T1C8LONG", f.createJavaType(Long.class))
+ .add("T1C9FLOAT", f.createJavaType(Float.class))
+ .add("T1C10DATE", f.createJavaType(Date.class))
+ .add("T1C11TIME", f.createJavaType(Time.class))
+ .add("T1C12TIMESTAMP", f.createJavaType(Timestamp.class))
+ .build();
+
+ tbl1 = new TestTable(tbl1rt)
+ .setDistribution(IgniteDistributions.affinity(0, "TBL1", "hash"));
+
+ tbl1.addIndex(new IgniteIndex(RelCollations.of(0), "PK", null, tbl1));
+
+ for (RelDataTypeField field : tbl1rt.getFieldList()) {
+ if (field.getIndex() == 0)
+ continue;
+
+ int idx = field.getIndex();
+ String name = getIdxName(1, field.getName().toUpperCase());
+
+ tbl1.addIndex(new IgniteIndex(RelCollations.of(idx), name, null, tbl1));
+ }
+
+ tbl4 = new TestTable(tbl1rt)
+ .setDistribution(IgniteDistributions.affinity(0, "TBL4", "hash"));
+ tbl4.addIndex(new IgniteIndex(RelCollations.of(0), "PK", null, tbl1));
+
+ for (RelDataTypeField field : tbl1rt.getFieldList()) {
+ if (field.getIndex() == 0)
+ continue;
+
+ int idx = field.getIndex();
+ String name = getIdxName(4, field.getName().toUpperCase());
+
+ tbl4.addIndex(new IgniteIndex(RelCollations.of(idx), name, null, tbl4));
+ }
+
+ tbl4.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(6, 7)), "TBL4_SHORT_LONG", null, tbl4));
+
+ HashMap<String, ColumnStatistics> colStat1 = new HashMap<>();
+ colStat1.put("T1C1INT", new ColumnStatistics(ValueInt.get(1), ValueInt.get(1000),
+ 0, 1000, t1rc, 4, null, 1, 0));
+
+ colStat1.put("T1C2STR", new ColumnStatistics(ValueString.get("A1"), ValueString.get("Z9"),
+ 100, 20, t1rc, 2, null, 1, 0));
+
+ colStat1.put("T1C3DBL", new ColumnStatistics(ValueDouble.get(0.01), ValueDouble.get(0.99),
+ 10, 1000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C4BYTE", new ColumnStatistics(ValueByte.get((byte)0), ValueByte.get((byte)255),
+ 10, 1000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C5BOOLEAN", new ColumnStatistics(ValueBoolean.get(false), ValueBoolean.get(true),
+ 0, 2, t1rc, 1, null, 1, 0));
+
+ colStat1.put("T1C6CHARACTER", new ColumnStatistics(ValueString.get("A"), ValueString.get("Z"),
+ 10, 10, t1rc, 1, null, 1, 0));
+
+ colStat1.put("T1C7SHORT", new ColumnStatistics(ValueShort.get((short)1), ValueShort.get((short)5000),
+ 110, 500, t1rc, 2, null, 1, 0));
+
+ colStat1.put("T1C8LONG", new ColumnStatistics(ValueLong.get(1L), ValueLong.get(100000L),
+ 10, 100000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C9FLOAT", new ColumnStatistics(ValueFloat.get((float)0.1), ValueFloat.get((float)0.9),
+ 10, 1000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C10DATE", new ColumnStatistics(ValueDate.get(MIN_DATE), ValueDate.get(MAX_DATE),
+ 20, 1000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C11TIME", new ColumnStatistics(ValueTime.get(MIN_TIME), ValueTime.get(MAX_TIME),
+ 10, 1000, t1rc, 8, null, 1, 0));
+
+ colStat1.put("T1C12TIMESTAMP", new ColumnStatistics(ValueTimestamp.get(MIN_TIMESTAMP), ValueTimestamp.get(MAX_TIMESTAMP),
+ 20, 1000, t1rc, 8, null, 1, 0));
+
+ tbl1stat = new IgniteStatisticsImpl(new ObjectStatisticsImpl(1000, colStat1));
+
+ publicSchema = new IgniteSchema("PUBLIC");
+
+ publicSchema.addTable("TBL1", tbl1);
+ publicSchema.addTable("TBL4", tbl4);
+ }
+
+ /**
+ * Check index usage with and without statistics:
+ *
+ * 1) With statistics planner choose second one with better selectivity.
+ * 2) Without statistics planner choose first one.
+ *
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testIndexChoosing() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+
+ String sql = "select * from TBL1 where t1c7short > 5 and t1c8long > 55555";
+
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertNotNull(idxScan);
+ assertEquals("TBL1_T1C8LONG", idxScan.indexName());
+
+ tbl1.setStatistics(new IgniteStatisticsImpl((GridH2Table)null));
+
+ IgniteRel phys2 = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan2 = findFirstNode(phys2, byClass(IgniteIndexScan.class));
+
+ assertNotNull(idxScan2);
+ assertEquals("TBL1_T1C7SHORT", idxScan2.indexName());
+
+ tbl1.setStatistics(tbl1stat);
+ }
+
+ /**
+ * Check index choosing with is null condition. Due to AbstractIndexScan logic - no index should be choosen.
+ */
+ @Test
+ public void testIsNull() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+
+ String isNullTemplate = "select * from TBL1 where %s is null";
+ String isNullPKTemplate = "select * from TBL1 where %s is null and T1C1INT is null";
+
+ for (RelDataTypeField field : tbl1rt.getFieldList()) {
+ if (field.getIndex() == 0)
+ continue;
+
+ if (tbl1NumericFields.contains(field.getName())) {
+ String isNullSql = String.format(isNullTemplate, field.getName());
+ String isNullPKSql = String.format(isNullPKTemplate, field.getName());
+
+ String idxName = getIdxName(1, field.getName().toUpperCase());
+
+ checkIdxNotUsed(isNullSql, idxName);
+ checkIdxNotUsed(isNullPKSql, idxName);
+ }
+ }
+ }
+
+ /**
+ * Check index choosing with not null condition. Due to AbstractIndexScan logic - no index should be choosen.
+ */
+ @Test
+ public void testNotNull() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+
+ String isNullTemplate = "select * from TBL1 where %s is not null";
+ String isNullPKTemplate = "select * from TBL1 where %s is not null and T1C1INT is null";
+
+ for (RelDataTypeField field : tbl1rt.getFieldList()) {
+ if (field.getIndex() == 0)
+ continue;
+
+ if (tbl1NumericFields.contains(field.getName())) {
+ String isNullSql = String.format(isNullTemplate, field.getName());
+ String isNullPKSql = String.format(isNullPKTemplate, field.getName());
+
+ String idxName = getIdxName(1, field.getName().toUpperCase());
+
+ checkIdxNotUsed(isNullSql, idxName);
+ checkIdxNotUsed(isNullPKSql, idxName);
+ }
+ }
+ }
+
+ /**
+ * Test borders with statistics and check that correct index used.
+ * @throws Exception In case of errors.
+ */
+ @Test
+ public void testBorders() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+ String templateFieldIdxLower = "select * from TBL1 where %s > 1000 and T1C1INT > 0";
+ String templateFieldIdxLowerOrEq = "select * from TBL1 where %s >= 1000 and T1C1INT > 0";
+ String templateFieldIdxUpper = "select * from TBL1 where %s < 1 and T1C1INT > 10";
+ String templateFieldIdxUpperOrEq = "select * from TBL1 where %s < 1 and T1C1INT > 10";
+
+ for (RelDataTypeField field : tbl1rt.getFieldList()) {
+ if (field.getIndex() == 0)
+ continue;
+
+ if (tbl1NumericFields.contains(field.getName())) {
+ String sqlLower = String.format(templateFieldIdxLower, field.getName());
+ String sqlLowerOrEq = String.format(templateFieldIdxLowerOrEq, field.getName());
+ String sqlUpper = String.format(templateFieldIdxUpper, field.getName());
+ String sqlUpperOrEq = String.format(templateFieldIdxUpperOrEq, field.getName());
+
+ String idxName = getIdxName(1, field.getName().toUpperCase());
+
+ checkIdxUsed(sqlLower, idxName);
+ checkIdxUsed(sqlLowerOrEq, idxName);
+ checkIdxUsed(sqlUpper, idxName);
+ checkIdxUsed(sqlUpperOrEq, idxName);
+ }
+ }
+ // time
+ checkIdxUsed("select * from TBL1 where T1C11TIME < '" + MIN_TIME + "'", "TBL1_T1C11TIME");
+ checkIdxUsed("select * from TBL1 where T1C11TIME <= '" + MIN_TIME + "'", "TBL1_T1C11TIME");
+ checkIdxUsed("select * from TBL1 where T1C11TIME > '" + MAX_TIME + "'", "TBL1_T1C11TIME");
+ checkIdxUsed("select * from TBL1 where T1C11TIME >= '" + MAX_TIME + "'", "TBL1_T1C11TIME");
+
+ // date
+ checkIdxUsed("select * from TBL1 where T1C10DATE < '" + MIN_DATE + "'", "TBL1_T1C10DATE");
+ checkIdxUsed("select * from TBL1 where T1C10DATE <= '" + MIN_DATE + "'", "TBL1_T1C10DATE");
+ checkIdxUsed("select * from TBL1 where T1C10DATE > '" + MAX_DATE + "'", "TBL1_T1C10DATE");
+ checkIdxUsed("select * from TBL1 where T1C10DATE >= '" + MAX_DATE + "'", "TBL1_T1C10DATE");
+
+ // timestamp
+ checkIdxUsed("select * from TBL1 where T1C12TIMESTAMP < '" + MIN_TIMESTAMP + "'", "TBL1_T1C12TIMESTAMP");
+ checkIdxUsed("select * from TBL1 where T1C12TIMESTAMP <= '" + MIN_TIMESTAMP + "'", "TBL1_T1C12TIMESTAMP");
+ checkIdxUsed("select * from TBL1 where T1C12TIMESTAMP > '" + MAX_TIMESTAMP + "'", "TBL1_T1C12TIMESTAMP");
+ checkIdxUsed("select * from TBL1 where T1C12TIMESTAMP >= '" + MAX_TIMESTAMP + "'", "TBL1_T1C12TIMESTAMP");
+ }
+
+ /**
+ * Check index usage.
+ *
+ * @param sql Query.
+ * @param idxName Expected index name.
+ * @throws Exception In case of errors.
+ */
+ private void checkIdxUsed(String sql, String idxName) throws Exception {
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertNotNull(idxScan);
+ assertEquals(idxName, idxScan.indexName());
+ }
+
+ /**
+ * Check index is not used.
+ *
+ * @param sql Query.
+ * @param idxName Not expected index name.
+ * @throws Exception In case of errors.
+ */
+ private void checkIdxNotUsed(String sql, String idxName) throws Exception {
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertTrue(idxScan == null || !idxName.equals(idxScan.indexName()));
+ }
+
+ /**
+ * Get index name by column.
+ *
+ * @param tblIdx Table index.
+ * @param fieldName Column name.
+ * @return Index name.
+ */
+ private String getIdxName(int tblIdx, String fieldName) {
+ return "TBL" + tblIdx + "_" + fieldName;
+ }
+
+ /**
+ * Run query with expression and check index wouldn't be choosen for the sum of two columns.
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testIndexChoosingFromExpression() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+ // 1) for sum of two columns
+ String sql = "select * from TBL1 where t1c7short + t1c8long > 55555";
+
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertNull(idxScan);
+ }
+
+ /**
+ * Run query with expression and check index wouldn't be choosen for the sum of column with constant.
+ *
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testIndexChoosingFromSumConst() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+ String sql = "select * from TBL1 where t1c7short + 1 > 55555";
+
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertNull(idxScan);
+ }
+
+ /**
+ * Run query with expression and check index wouldn't be choosen for the function of column value.
+ *
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testIndexChoosingFromUnifunction() throws Exception {
+ tbl1.setStatistics(tbl1stat);
+ String sql = "select * from TBL1 where abs(t1c7short) > 55555";
+
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertNull(idxScan);
+ }
+
+ /**
+ * Check composite index wouldn't choosen.
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testCompositeIndexAvoid() throws Exception {
+ tbl4.setStatistics(tbl1stat);
+ checkIdxUsed("select * from TBL4 where t1c7short > 1 and t1c8long > 80000", "TBL4_T1C8LONG");
+ }
+
+ /**
+ * Check that index over column of type SHORT will be chosen because
+ * it has better selectivity: need to scan only last 500 elements
+ * whereas index over column of type STRING has default range selectivity
+ * equals to 0.5.
+ *
+ * @throws Exception In case of error.
+ */
+ @Test
+ public void testIndexWithBetterSelectivityPreferred() throws Exception {
+ int rowCnt = 10_000;
+
+ HashMap<String, ColumnStatistics> colStat1 = new HashMap<>();
+ colStat1.put("T1C2STR", new ColumnStatistics(ValueString.get("A1"), ValueString.get("Z9"),
+ 0, 1, rowCnt, 2, null, 1, 0));
+
+ colStat1.put("T1C7SHORT", new ColumnStatistics(ValueShort.get((short)1), ValueShort.get((short)5000),
+ 0, rowCnt, rowCnt, 2, null, 1, 0));
+
+ IgniteStatisticsImpl stat = new IgniteStatisticsImpl(new ObjectStatisticsImpl(1000, colStat1));
+
+ tbl1.setStatistics(stat);
+
+ String sql = "select * from TBL1 where t1c7short > 4500 and T1C2STR > 'asd'";
+
+ IgniteRel phys = physicalPlan(sql, publicSchema);
+ IgniteIndexScan idxScan = findFirstNode(phys, byClass(IgniteIndexScan.class));
+
+ assertEquals(getIdxName(1, "T1C7SHORT"), idxScan.indexName());
+ }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/OrToUnionRuleTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/OrToUnionRuleTest.java
index c97a1cc..1b3eb83 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/OrToUnionRuleTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/OrToUnionRuleTest.java
@@ -195,15 +195,13 @@ public class OrToUnionRuleTest extends GridCommonAbstractTest {
/**
* Check 'OR -> UNION' rule is not applied for range conditions on indexed columns.
- *
- * @throws Exception If failed.
*/
@Test
public void testRangeOrToUnionAllRewrite() {
checkQuery("SELECT * " +
"FROM products " +
"WHERE cat_id > 1 " +
- "OR subcat_id < 10")
+ "OR subcat_id < 10 ")
.matches(not(containsUnion(true)))
.matches(containsTableScan("PUBLIC", "PRODUCTS"))
.returns(5, "Video", 2, "Camera Media", 21, "Media 3")
@@ -222,8 +220,8 @@ public class OrToUnionRuleTest extends GridCommonAbstractTest {
"FROM products " +
"WHERE name = 'Canon' " +
"OR category = 'Video'")
- .matches(containsUnion(true))
- .matches(containsIndexScan("PUBLIC", "PRODUCTS", "IDX_CATEGORY"))
+ .matches(not(containsUnion(true)))
+ .matches(containsTableScan("PUBLIC", "PRODUCTS"))
.returns(5, "Video", 2, "Camera Media", 21, "Media 3")
.returns(6, "Video", 2, "Camera Lens", 22, "Lens 3")
.returns(7, "Video", 1, null, 0, "Canon")
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index a09c8d6..39a546a 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -32,6 +32,7 @@ import org.apache.ignite.internal.processors.query.calcite.integration.IndexDdlI
import org.apache.ignite.internal.processors.query.calcite.integration.IndexSpoolIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.JoinIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.MetadataIntegrationTest;
+import org.apache.ignite.internal.processors.query.calcite.integration.ServerStatisticsIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.SetOpIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.SortAggregateIntegrationTest;
import org.apache.ignite.internal.processors.query.calcite.integration.TableDdlIntegrationTest;
@@ -70,6 +71,7 @@ import org.junit.runners.Suite;
SetOpIntegrationTest.class,
UnstableTopologyTest.class,
JoinCommuteRulesTest.class,
+ ServerStatisticsIntegrationTest.class,
JoinIntegrationTest.class,
})
public class IntegrationTestSuite {
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
index 87ef0cd..c9cc87d 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
@@ -30,6 +30,7 @@ import org.apache.ignite.internal.processors.query.calcite.planner.PlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.SetOpPlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.SortAggregatePlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.SortedIndexSpoolPlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.StatisticsPlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.TableDmlPlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.TableFunctionPlannerTest;
import org.apache.ignite.internal.processors.query.calcite.planner.TableSpoolPlannerTest;
@@ -59,6 +60,7 @@ import org.junit.runners.Suite;
JoinCommutePlannerTest.class,
LimitOffsetPlannerTest.class,
MergeJoinPlannerTest.class,
+ StatisticsPlannerTest.class,
})
public class PlannerTestSuite {
}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/stat/config/StatisticsObjectConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/stat/config/StatisticsObjectConfiguration.java
index dcd6281..31d1219 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/stat/config/StatisticsObjectConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/stat/config/StatisticsObjectConfiguration.java
@@ -75,6 +75,15 @@ public class StatisticsObjectConfiguration implements Serializable {
}
/**
+ * Constructor.
+ *
+ * @param key Statistics key.
+ */
+ public StatisticsObjectConfiguration(StatisticsKey key) {
+ this(key, null, DEFAULT_OBSOLESCENCE_MAX_PERCENT);
+ }
+
+ /**
* Merge configuration changes with existing configuration.
*
* @param oldCfg Previous configuration.