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.