You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2022/06/24 06:35:11 UTC

[ignite] branch master updated: IGNITE-16920 SQL Calcite: Use index count scan for COUNT(*) - Fixes #10050.

This is an automated email from the ASF dual-hosted git repository.

alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 486fe7f934a IGNITE-16920 SQL Calcite: Use index count scan for COUNT(*) - Fixes #10050.
486fe7f934a is described below

commit 486fe7f934acfe64cab41759fe32f65747cac0e5
Author: Vladimir Steshin <vl...@gmail.com>
AuthorDate: Fri Jun 24 11:31:53 2022 +0500

    IGNITE-16920 SQL Calcite: Use index count scan for COUNT(*) - Fixes #10050.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../processors/query/calcite/exec/IndexScan.java   |   5 -
 .../query/calcite/exec/LogicalRelImplementor.java  |  22 +++
 .../query/calcite/exec/rel/CollectNode.java        |  62 +++++++-
 .../calcite/metadata/IgniteMdFragmentMapping.java  |   9 ++
 .../processors/query/calcite/prepare/Cloner.java   |   6 +
 .../query/calcite/prepare/IgniteRelShuttle.java    |   6 +
 .../query/calcite/prepare/PlannerPhase.java        |   2 +
 .../query/calcite/rel/IgniteIndexCount.java        | 165 +++++++++++++++++++++
 .../query/calcite/rel/IgniteRelVisitor.java        |   5 +
 .../query/calcite/rel/IgniteTableScan.java         |   2 +-
 .../query/calcite/rule/IndexCountRule.java         | 108 ++++++++++++++
 .../query/calcite/schema/CacheIndexImpl.java       |  31 +++-
 .../query/calcite/schema/IgniteIndex.java          |   9 ++
 .../query/calcite/schema/SystemViewIndexImpl.java  |   5 +
 .../calcite/sql/fun/IgniteStdSqlOperatorTable.java |   1 +
 .../calcite/exec/LogicalRelImplementorTest.java    |  86 +++++++++--
 .../integration/AbstractBasicIntegrationTest.java  |  21 ++-
 .../integration/AggregatesIntegrationTest.java     |  22 ++-
 .../integration/IndexRebuildIntegrationTest.java   |  53 +++++++
 .../planner/AbstractAggregatePlannerTest.java      |  54 +++----
 .../calcite/planner/HashAggregatePlannerTest.java  |  77 ++++++++++
 .../calcite/planner/IndexRebuildPlannerTest.java   |  59 +++++++-
 .../calcite/planner/LimitOffsetPlannerTest.java    |   2 +-
 .../query/calcite/planner/TestTable.java           |   2 +-
 24 files changed, 760 insertions(+), 54 deletions(-)

diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
index 40874bdafd8..805cfa01206 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
@@ -39,7 +39,6 @@ import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKeyFactory;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
-import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTopologyFuture;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtLocalPartition;
@@ -65,9 +64,6 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
     /** */
     private final GridCacheContext<?, ?> cctx;
 
-    /** */
-    private final CacheObjectContext coCtx;
-
     /** */
     private final CacheTableDescriptor desc;
 
@@ -133,7 +129,6 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
         this.idx = idx;
         cctx = desc.cacheContext();
         kctx = cctx.kernalContext();
-        coCtx = cctx.cacheObjectContext();
 
         factory = ectx.rowHandler().factory(ectx.getTypeFactory(), rowType);
         topVer = ectx.topologyVersion();
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index c44e6195eca..20f9def3eb3 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.query.calcite.exec;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -69,6 +70,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
@@ -406,6 +408,26 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
         }
     }
 
+    /** {@inheritDoc} */
+    @Override public Node<Row> visit(IgniteIndexCount rel) {
+        IgniteTable tbl = rel.getTable().unwrap(IgniteTable.class);
+        IgniteIndex idx = tbl.getIndex(rel.indexName());
+
+        if (idx != null && !tbl.isIndexRebuildInProgress()) {
+            return new ScanNode<>(ctx, rel.getRowType(), () -> Collections.singletonList(ctx.rowHandler()
+                .factory(ctx.getTypeFactory(), rel.getRowType())
+                .create(idx.count(ctx, ctx.group(rel.sourceId())))).iterator());
+        }
+        else {
+            CollectNode<Row> replacement = CollectNode.createCountCollector(ctx);
+
+            replacement.register(new ScanNode<>(ctx, rel.getTable().getRowType(), tbl.scan(ctx,
+                ctx.group(rel.sourceId()), null, null, ImmutableBitSet.of(0))));
+
+            return replacement;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override public Node<Row> visit(IgniteTableScan rel) {
         RexNode condition = rel.condition();
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/CollectNode.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/CollectNode.java
index 717a02d58fe..2f63d863776 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/CollectNode.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/CollectNode.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.function.Supplier;
 import com.google.common.collect.Iterables;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
@@ -41,16 +42,49 @@ public class CollectNode<Row> extends AbstractNode<Row> implements SingleNode<Ro
     private int waiting;
 
     /**
+     * Creates Collect node with the collector depending on {@code rowType}.
+     *
      * @param ctx Execution context.
      * @param rowType Output row type.
      */
     public CollectNode(
         ExecutionContext<Row> ctx,
         RelDataType rowType
+    ) {
+        this(ctx, rowType, createCollector(ctx, rowType));
+    }
+
+    /**
+     * Creates Collect node with the collector depending on {@code rowType}.
+     *
+     * @param ctx Execution context.
+     * @param rowType Output row type.
+     * @param collector Collector.
+     */
+    private CollectNode(
+        ExecutionContext<Row> ctx,
+        RelDataType rowType,
+        Collector<Row> collector
     ) {
         super(ctx, rowType);
 
-        collector = createCollector(ctx, rowType);
+        this.collector = collector;
+    }
+
+    /**
+     * Creates row counting Collect node.
+     *
+     * @param ctx Execution context.
+     */
+    public static <Row> CollectNode<Row> createCountCollector(ExecutionContext<Row> ctx) {
+        RelDataType rowType = ctx.getTypeFactory().createSqlType(SqlTypeName.BIGINT);
+
+        Collector<Row> collector = new Counter<>(
+            ctx.rowHandler(),
+            ctx.rowHandler().factory(ctx.getTypeFactory(), rowType),
+            1);
+
+        return new CollectNode<>(ctx, rowType, collector);
     }
 
     /** {@inheritDoc} */
@@ -236,4 +270,30 @@ public class CollectNode<Row> extends AbstractNode<Row> implements SingleNode<Ro
             outBuf = new ArrayList<>(cap);
         }
     }
+
+    /** */
+    private static class Counter<Row> extends Collector<Row> {
+        /** */
+        private long cnt;
+
+        /** */
+        private Counter(RowHandler<Row> hnd, RowHandler.RowFactory<Row> rowFactory, int cap) {
+            super(hnd, rowFactory, cap);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected Object outData() {
+            return cnt;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void push(Row row) {
+            ++cnt;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void clear() {
+            cnt = 0;
+        }
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
index d46cdb6af72..7f38201e501 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
@@ -33,6 +33,7 @@ import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetada
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableFunctionScan;
@@ -190,6 +191,14 @@ public class IgniteMdFragmentMapping implements MetadataHandler<FragmentMappingM
             rel.getTable().unwrap(IgniteTable.class).colocationGroup(ctx));
     }
 
+    /**
+     * See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode, RelMetadataQuery, MappingQueryContext)}
+     */
+    public FragmentMapping fragmentMapping(IgniteIndexCount rel, RelMetadataQuery mq, MappingQueryContext ctx) {
+        return FragmentMapping.create(rel.sourceId(),
+            rel.getTable().unwrap(IgniteTable.class).colocationGroup(ctx));
+    }
+
     /**
      * See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode, RelMetadataQuery, MappingQueryContext)}
      */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
index 9cee53a2ece..606766fa7ee 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
@@ -24,6 +24,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
@@ -153,6 +154,11 @@ public class Cloner implements IgniteRelVisitor<IgniteRel> {
         return rel.clone(cluster, F.asList());
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteIndexCount rel) {
+        return rel.clone(cluster, F.asList());
+    }
+
     /** {@inheritDoc} */
     @Override public IgniteRel visit(IgniteTableScan rel) {
         return rel.clone(cluster, F.asList());
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
index a90de5119b7..65abac5aae7 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
@@ -23,6 +23,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
@@ -147,6 +148,11 @@ public class IgniteRelShuttle implements IgniteRelVisitor<IgniteRel> {
         return processNode(rel);
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteIndexCount rel) {
+        return processNode(rel);
+    }
+
     /** {@inheritDoc} */
     @Override public IgniteRel visit(IgniteTableScan rel) {
         return processNode(rel);
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
index 8a0471677e9..bb4de36b4e5 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
@@ -49,6 +49,7 @@ import org.apache.ignite.internal.processors.query.calcite.rule.FilterConverterR
 import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeToHashIndexSpoolRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeToSortedIndexSpoolRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.HashAggregateConverterRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.IndexCountRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.LogicalScanConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.MergeJoinConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.NestedLoopJoinConverterRule;
@@ -236,6 +237,7 @@ public enum PlannerPhase {
                     ValuesConverterRule.INSTANCE,
                     LogicalScanConverterRule.INDEX_SCAN,
                     LogicalScanConverterRule.TABLE_SCAN,
+                    IndexCountRule.INSTANCE,
                     CollectRule.INSTANCE,
                     HashAggregateConverterRule.COLOCATED,
                     HashAggregateConverterRule.MAP_REDUCE,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexCount.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexCount.java
new file mode 100644
index 00000000000..a9bb03e5527
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexCount.java
@@ -0,0 +1,165 @@
+/*
+ * 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.rel;
+
+import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelInput;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * Returns number of index records.
+ */
+public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgniteRel {
+    /** */
+    private static final double INDEX_TRAVERSE_COST_DIVIDER = 1000;
+
+    /** */
+    private final RelOptTable tbl;
+
+    /** */
+    private final String idxName;
+
+    /** */
+    private final long sourceId;
+
+    /**
+     * Constructor for deserialization.
+     *
+     * @param input Serialized representation.
+     */
+    public IgniteIndexCount(RelInput input) {
+        super(input.getCluster(), input.getTraitSet());
+
+        idxName = input.getString("index");
+        tbl = input.getTable("table");
+
+        Object srcIdObj = input.get("sourceId");
+        if (srcIdObj != null)
+            sourceId = ((Number)srcIdObj).longValue();
+        else
+            sourceId = -1L;
+    }
+
+    /**
+     * Ctor.
+     *
+     * @param cluster Cluster that this relational expression belongs to.
+     * @param traits Traits of this relational expression.
+     * @param tbl Table definition.
+     * @param idxName Index name.
+     */
+    public IgniteIndexCount(
+        RelOptCluster cluster,
+        RelTraitSet traits,
+        RelOptTable tbl,
+        String idxName
+    ) {
+        this(-1, cluster, traits, tbl, idxName);
+    }
+
+    /**
+     * Ctor.
+     *
+     * @param sourceId Source id.
+     * @param cluster Cluster that this relational expression belongs to.
+     * @param traits Traits of this relational expression.
+     * @param tbl Table definition.
+     * @param idxName Index name.
+     */
+    private IgniteIndexCount(
+        long sourceId,
+        RelOptCluster cluster,
+        RelTraitSet traits,
+        RelOptTable tbl,
+        String idxName
+    ) {
+        super(cluster, traits);
+
+        this.idxName = idxName;
+        this.tbl = tbl;
+        this.sourceId = sourceId;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RelDataType deriveRowType() {
+        RelDataTypeFactory tf = getCluster().getTypeFactory();
+
+        return tf.createStructType(singletonList(tf.createSqlType(SqlTypeName.BIGINT)), singletonList("COUNT"));
+    }
+
+    /** */
+    public String indexName() {
+        return idxName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public double estimateRowCount(RelMetadataQuery mq) {
+        // Requesting index count always produces just one record.
+        return 1.0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+        return planner.getCostFactory().makeCost(1.0, tbl.getRowCount() / INDEX_TRAVERSE_COST_DIVIDER, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override public long sourceId() {
+        return sourceId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelOptTable getTable() {
+        return tbl;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelWriter explainTerms(RelWriter pw) {
+        return super.explainTerms(pw)
+            .item("index", idxName)
+            .item("table", tbl.getQualifiedName())
+            .itemIf("sourceId", sourceId, sourceId != -1L);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T accept(IgniteRelVisitor<T> visitor) {
+        return visitor.visit(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
+        return new IgniteIndexCount(sourceId, cluster, traitSet, tbl, idxName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel clone(long srcId) {
+        return new IgniteIndexCount(srcId, getCluster(), traitSet, tbl, idxName);
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
index 56ebc3fea35..fc5084507de 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
@@ -69,6 +69,11 @@ public interface IgniteRelVisitor<T> {
      */
     T visit(IgniteIndexScan rel);
 
+    /**
+     * See {@link IgniteRelVisitor#visit(IgniteRel)}
+     */
+    T visit(IgniteIndexCount rel);
+
     /**
      * See {@link IgniteRelVisitor#visit(IgniteRel)}
      */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
index 0668173e39c..6291a5433b6 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableScan.java
@@ -95,7 +95,7 @@ public class IgniteTableScan extends ProjectableFilterableTableScan implements S
      * @param cond Filters.
      * @param requiredColunms Participating colunms.
      */
-    public IgniteTableScan(
+    private IgniteTableScan(
         long sourceId,
         RelOptCluster cluster,
         RelTraitSet traits,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexCountRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexCountRule.java
new file mode 100644
index 00000000000..475637dfac3
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexCountRule.java
@@ -0,0 +1,108 @@
+/*
+ * 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.rule;
+
+import java.util.Collections;
+import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
+import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalTableScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.immutables.value.Value;
+
+/** Tries to optimize 'COUNT(*)' to use number of index records. */
+@Value.Enclosing
+public class IndexCountRule extends RelRule<IndexCountRule.Config> {
+    /** */
+    public static final IndexCountRule INSTANCE = Config.DEFAULT.toRule();
+
+    /** Ctor. */
+    private IndexCountRule(IndexCountRule.Config cfg) {
+        super(cfg);
+    }
+
+    /** */
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalAggregate aggr = call.rel(0);
+        IgniteLogicalTableScan scan = call.rel(1);
+        IgniteTable table = scan.getTable().unwrap(IgniteTable.class);
+        IgniteIndex idx = table.getIndex(QueryUtils.PRIMARY_KEY_INDEX);
+
+        if (
+            idx == null ||
+                table.isIndexRebuildInProgress() ||
+                scan.condition() != null ||
+                aggr.getGroupCount() > 0 ||
+                aggr.getAggCallList().stream().anyMatch(a -> a.getAggregation().getKind() != SqlKind.COUNT ||
+                    !a.getArgList().isEmpty() || a.hasFilter())
+        )
+            return;
+
+        RelTraitSet idxTraits = aggr.getTraitSet()
+            .replace(IgniteConvention.INSTANCE)
+            .replace(table.distribution().getType() == RelDistribution.Type.HASH_DISTRIBUTED ?
+                IgniteDistributions.random() : table.distribution())
+            .replace(RewindabilityTrait.REWINDABLE);
+
+        IgniteIndexCount idxCnt = new IgniteIndexCount(
+            scan.getCluster(),
+            idxTraits,
+            scan.getTable(),
+            idx.name()
+        );
+
+        RelBuilder b = call.builder();
+
+        // Also cast DECIMAL of SUM0 to BIGINT(Long) of COUNT().
+        call.transformTo(b.push(idxCnt)
+            .aggregate(b.groupKey(), Collections.nCopies(aggr.getAggCallList().size(),
+                b.aggregateCall(SqlStdOperatorTable.SUM0, b.field(0))))
+            .project(Commons.transform(Ord.zip(b.fields()),
+                f -> b.cast(f.e, aggr.getRowType().getFieldList().get(f.i).getType().getSqlTypeName())))
+            .build()
+        );
+    }
+
+    /** The rule config. */
+    @Value.Immutable
+    public interface Config extends RelRule.Config {
+        /** */
+        IndexCountRule.Config DEFAULT = ImmutableIndexCountRule.Config.of()
+            .withDescription("IndexCountRule")
+            .withOperandSupplier(r -> r.operand(LogicalAggregate.class)
+                .oneInput(i -> i.operand(IgniteLogicalTableScan.class).anyInputs()));
+
+        /** {@inheritDoc} */
+        @Override default IndexCountRule toRule() {
+            return new IndexCountRule(this);
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
index 6ebc8e8eb08..59eecbbab34 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
@@ -28,7 +28,10 @@ import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
 import org.apache.ignite.internal.cache.query.index.Index;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
@@ -37,6 +40,8 @@ import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLog
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
+import org.apache.ignite.spi.indexing.IndexingQueryFilter;
+import org.apache.ignite.spi.indexing.IndexingQueryFilterImpl;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -50,13 +55,13 @@ public class CacheIndexImpl implements IgniteIndex {
     private final String idxName;
 
     /** */
-    private final Index idx;
+    private final @Nullable Index idx;
 
     /** */
     private final IgniteCacheTable tbl;
 
     /** */
-    public CacheIndexImpl(RelCollation collation, String name, Index idx, IgniteCacheTable tbl) {
+    public CacheIndexImpl(RelCollation collation, String name, @Nullable Index idx, IgniteCacheTable tbl) {
         this.collation = collation;
         idxName = name;
         this.idx = idx;
@@ -109,6 +114,28 @@ public class CacheIndexImpl implements IgniteIndex {
         return Collections.emptyList();
     }
 
+    /** {@inheritDoc} */
+    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
+        long cnt = 0;
+
+        if (idx != null && grp.nodeIds().contains(ectx.localNodeId())) {
+            IndexingQueryFilter filter = new IndexingQueryFilterImpl(tbl.descriptor().cacheContext().kernalContext(),
+                ectx.topologyVersion(), grp.partitions(ectx.localNodeId()));
+
+            InlineIndex iidx = idx.unwrap(InlineIndex.class);
+
+            try {
+                for (int i = 0; i < iidx.segmentsCount(); ++i)
+                    cnt += iidx.count(i, new IndexQueryContext(filter, null, ectx.mvccSnapshot()));
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException("Unable to count index records.", e);
+            }
+        }
+
+        return cnt;
+    }
+
     /** {@inheritDoc} */
     @Override public IndexConditions toIndexCondition(
         RelOptCluster cluster,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
index 0d29208da17..5753302dbf0 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
@@ -86,4 +86,13 @@ public interface IgniteIndex {
         Function<Row, Row> rowTransformer,
         @Nullable ImmutableBitSet requiredColumns
     );
+
+    /**
+     * Calculates index records number.
+     *
+     * @param ectx Execution context.
+     * @param grp  Colocation group.
+     * @return Index records number for {@code group}.
+     */
+    public long count(ExecutionContext<?> ectx, ColocationGroup grp);
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
index bf83d9ca4f6..050678107cc 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
@@ -97,6 +97,11 @@ public class SystemViewIndexImpl implements IgniteIndex {
         );
     }
 
+    /** {@inheritDoc} */
+    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
+        return tbl.descriptor().systemView().size();
+    }
+
     /** {@inheritDoc} */
     @Override public IndexConditions toIndexCondition(
         RelOptCluster cluster,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteStdSqlOperatorTable.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteStdSqlOperatorTable.java
index 3b19f4665ca..c1e8d900b4a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteStdSqlOperatorTable.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteStdSqlOperatorTable.java
@@ -77,6 +77,7 @@ public class IgniteStdSqlOperatorTable extends ReflectiveSqlOperatorTable {
         // Aggregates.
         register(SqlStdOperatorTable.COUNT);
         register(SqlStdOperatorTable.SUM);
+        register(SqlStdOperatorTable.SUM0);
         register(SqlStdOperatorTable.AVG);
         register(SqlStdOperatorTable.MIN);
         register(SqlStdOperatorTable.MAX);
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
index 4a9e40ce034..ee6f7fd5765 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
@@ -37,6 +37,8 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.mapping.Mappings;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.exec.rel.CollectNode;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.IndexSpoolNode;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.Node;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.ProjectNode;
@@ -45,6 +47,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.rel.SortNode;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.planner.TestTable;
 import org.apache.ignite.internal.processors.query.calcite.prepare.BaseQueryContext;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
 import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
@@ -64,10 +67,28 @@ import static org.apache.ignite.internal.processors.query.calcite.CalciteQueryPr
  */
 public class LogicalRelImplementorTest extends GridCommonAbstractTest {
     /** */
-    @Test
-    public void testIndexScanRewriter() {
-        IgniteTypeFactory tf = Commons.typeFactory();
+    private LogicalRelImplementor<Object[]> relImplementor;
+
+    /** */
+    private RelOptCluster cluster;
+
+    /** */
+    private ScanAwareTable tbl;
+
+    /** */
+    private BaseQueryContext qctx;
+
+    /** */
+    private RexBuilder rexBuilder;
+
+    /** */
+    private IgniteTypeFactory tf;
+
+    /** */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
 
+        tf = Commons.typeFactory();
         RelDataTypeFactory.Builder b = new RelDataTypeFactory.Builder(tf);
 
         RelDataType sqlTypeInt = tf.createSqlType(SqlTypeName.INTEGER);
@@ -80,14 +101,12 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
 
         RelDataType rowType = b.build();
 
-        ScanAwareTable tbl = new ScanAwareTable(rowType);
-
-        tbl.addIndex("IDX", 2);
+        tbl = new ScanAwareTable(rowType);
 
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
         publicSchema.addTable("TBL", tbl);
 
-        BaseQueryContext qctx = BaseQueryContext.builder()
+        qctx = BaseQueryContext.builder()
             .frameworkConfig(
                 newConfigBuilder(FRAMEWORK_CONFIG)
                     .defaultSchema(createRootSchema(false).add(publicSchema.getName(), publicSchema))
@@ -114,7 +133,7 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
             }
         };
 
-        LogicalRelImplementor<Object[]> relImplementor = new LogicalRelImplementor<>(
+        relImplementor = new LogicalRelImplementor<>(
             ectx,
             null,
             null,
@@ -122,9 +141,52 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
             null
         );
 
-        // Construct relational operator corresponding to SQL: "SELECT val, id, id + 1 FROM TBL WHERE id = 1"
-        RelOptCluster cluster = Commons.emptyCluster();
-        RexBuilder rexBuilder = cluster.getRexBuilder();
+        cluster = Commons.emptyCluster();
+
+        rexBuilder = cluster.getRexBuilder();
+    }
+
+    /**
+     * Tests IndexCount execution plan is changed to Collect/Scan when index is unavailable.
+     */
+    @Test
+    public void testIndexCountRewriter() {
+        IgniteIndexCount idxCnt = new IgniteIndexCount(cluster, cluster.traitSet(),
+            qctx.catalogReader().getTable(F.asList("PUBLIC", "TBL")), QueryUtils.PRIMARY_KEY_INDEX);
+
+        checkCollectNode(relImplementor.visit(idxCnt));
+
+        tbl.addIndex(QueryUtils.PRIMARY_KEY_INDEX, 2);
+
+        Node<?> node = relImplementor.visit(idxCnt);
+
+        assertTrue(node instanceof ScanNode);
+        assertNull(node.sources());
+        assertEquals(node.rowType(),
+            tf.createStructType(F.asList(tf.createSqlType(SqlTypeName.BIGINT)), F.asList("COUNT")));
+
+        tbl.markIndexRebuildInProgress(true);
+
+        checkCollectNode(relImplementor.visit(idxCnt));
+    }
+
+    /** */
+    private void checkCollectNode(Node<Object[]> node) {
+        assertTrue(node instanceof CollectNode);
+        assertTrue(node.sources() != null && node.sources().size() == 1);
+        assertTrue(node.sources().get(0) instanceof ScanNode);
+        assertNull(node.sources().get(0).sources());
+        assertEquals(tbl.getRowType(tf), node.sources().get(0).rowType());
+    }
+
+    /** */
+    @Test
+    public void testIndexScanRewriter() {
+        tbl.addIndex(QueryUtils.PRIMARY_KEY_INDEX, 2);
+
+        RelDataType rowType = tbl.getRowType(tf);
+        RelDataType sqlTypeInt = rowType.getFieldList().get(2).getType();
+        RelDataType sqlTypeVarchar = rowType.getFieldList().get(3).getType();
 
         // Projects, filters and required columns.
         List<RexNode> project = F.asList(
@@ -143,7 +205,7 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
         ImmutableBitSet requiredColumns = ImmutableBitSet.of(2, 3);
 
         // Collations.
-        RelCollation idxCollation = tbl.getIndex("IDX").collation();
+        RelCollation idxCollation = tbl.getIndex(QueryUtils.PRIMARY_KEY_INDEX).collation();
 
         RelCollation colCollation = idxCollation.apply(Mappings.target(requiredColumns.asList(),
             tbl.getRowType(tf).getFieldCount()));
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 d55b4633c98..6e8dd456fb4 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
@@ -28,6 +28,7 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.QueryCursor;
@@ -58,6 +59,9 @@ import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
  */
 @WithSystemProperty(key = "calcite.debug", value = "false")
 public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
+    /** */
+    protected static final String TABLE_NAME = "person";
+
     /** */
     protected static IgniteEx client;
 
@@ -142,11 +146,17 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
 
     /** */
     protected IgniteCache<Integer, Employer> createAndPopulateTable() {
+        return createAndPopulateTable(2, CacheMode.PARTITIONED);
+    }
+
+    /** */
+    protected IgniteCache<Integer, Employer> createAndPopulateTable(int backups, CacheMode cacheMode) {
         IgniteCache<Integer, Employer> person = client.getOrCreateCache(new CacheConfiguration<Integer, Employer>()
-            .setName("person")
+            .setName(TABLE_NAME)
             .setSqlSchema("PUBLIC")
-            .setQueryEntities(F.asList(new QueryEntity(Integer.class, Employer.class).setTableName("person")))
-            .setBackups(2)
+            .setQueryEntities(F.asList(new QueryEntity(Integer.class, Employer.class).setTableName(TABLE_NAME)))
+            .setCacheMode(cacheMode)
+            .setBackups(backups)
         );
 
         int idx = 0;
@@ -237,6 +247,11 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
             return delegate.scan(execCtx, grp, filters, lowerIdxConditions, upperIdxConditions, rowTransformer,
                 requiredColumns);
         }
+
+        /** {@inheritDoc} */
+        @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
+            return delegate.count(ectx, grp);
+        }
     }
 
     /** */
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 7532c2fa7e1..105ea61324c 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
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.processors.query.calcite.integration;
 import java.util.List;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
@@ -30,7 +31,23 @@ import org.junit.Test;
 public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
     /** */
     @Test
-    public void countOfNonNumericField() {
+    public void testCountWithBackupsAndCacheModes() {
+        for (int b = 0; b < 2; ++b) {
+            createAndPopulateTable(b, CacheMode.PARTITIONED);
+
+            assertQuery("select count(*) from person").returns(5L).check();
+
+            client.destroyCache(TABLE_NAME);
+        }
+
+        createAndPopulateTable(0, CacheMode.REPLICATED);
+
+        assertQuery("select count(*) from person").returns(5L).check();
+    }
+
+    /** */
+    @Test
+    public void testCountOfNonNumericField() {
         createAndPopulateTable();
 
         assertQuery("select count(name) from person").returns(4L).check();
@@ -38,6 +55,9 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
         assertQuery("select count(1) from person").returns(5L).check();
         assertQuery("select count(null) from person").returns(0L).check();
 
+        assertQuery("select count(DISTINCT name) from person").returns(3L).check();
+        assertQuery("select count(DISTINCT 1) from person").returns(1L).check();
+
         assertQuery("select count(*) from person where salary < 0").returns(0L).check();
         assertQuery("select count(*) from person where salary < 0 and salary > 0").returns(0L).check();
 
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
index 4d6e98604d6..39f32840ec7 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
@@ -19,15 +19,21 @@ package org.apache.ignite.internal.processors.query.calcite.integration;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.cache.query.index.IndexProcessor;
 import org.apache.ignite.internal.managers.indexing.IndexesRebuildTask;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
+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.schema.IgniteCacheTable;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.schema.IndexRebuildCancelToken;
 import org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheVisitorClosure;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
@@ -196,6 +202,53 @@ public class IndexRebuildIntegrationTest extends AbstractBasicIntegrationTest {
         checkRebuildIndexQuery(grid(1), checker, checker);
     }
 
+    /**
+     * Test IndexCount is disabled at index rebuilding.
+     */
+    @Test
+    public void testIndexCountAtUnavailableIndex() throws IgniteCheckedException {
+        int records = 50;
+        int iterations = 500;
+
+        CalciteQueryProcessor srvEngine = Commons.lookupComponent(grid(0).context(), CalciteQueryProcessor.class);
+
+        for (int backups = -1; backups < 3; ++backups) {
+            String ddl = "CREATE TABLE tbl3 (id INT PRIMARY KEY, val VARCHAR, val2 VARCHAR) WITH ";
+
+            ddl += backups < 0 ? "TEMPLATE=REPLICATED" : "TEMPLATE=PARTITIONED,backups=" + backups;
+
+            executeSql(ddl);
+
+            for (int i = 0; i < records; i++)
+                executeSql("INSERT INTO tbl3 VALUES (?, ?, ?)", i, "val" + i, "val" + i);
+
+            IgniteCacheTable tbl3 = (IgniteCacheTable)srvEngine.schemaHolder().schema("PUBLIC").getTable("TBL3");
+
+            AtomicBoolean stop = new AtomicBoolean();
+
+            IgniteInternalFuture<?> fut = GridTestUtils.runAsync(() -> {
+                boolean lever = true;
+
+                while (!stop.get())
+                    tbl3.markIndexRebuildInProgress(lever = !lever);
+            });
+
+            try {
+                for (int i = 0; i < iterations; ++i)
+                    assertQuery("select COUNT(*) from tbl3").returns((long)records).check();
+            }
+            finally {
+                stop.set(true);
+
+                tbl3.markIndexRebuildInProgress(false);
+
+                executeSql("DROP TABLE tbl3");
+            }
+
+            fut.get();
+        }
+    }
+
     /** */
     @Test
     public void testRebuildOnRemoteNodeCorrelated() throws Exception {
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractAggregatePlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractAggregatePlannerTest.java
index dde5390e631..7d87ca5eaa4 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractAggregatePlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractAggregatePlannerTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.Arrays;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
@@ -33,42 +34,28 @@ import org.jetbrains.annotations.NotNull;
 @SuppressWarnings({"TypeMayBeWeakened"})
 public class AbstractAggregatePlannerTest extends AbstractPlannerTest {
     /**
-     * @return REPLICATED test table (ID, VAL0, VAL1, GRP0, GRP1)
+     * @return REPLICATED test table (ID, VAL0, VAL1, GRP0, GRP1) with given defined distribution.
      */
-    @NotNull protected TestTable createBroadcastTable() {
-        IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
-        TestTable tbl = new TestTable(
-            new RelDataTypeFactory.Builder(f)
-                .add("ID", f.createJavaType(Integer.class))
-                .add("VAL0", f.createJavaType(Integer.class))
-                .add("VAL1", f.createJavaType(Integer.class))
-                .add("GRP0", f.createJavaType(Integer.class))
-                .add("GRP1", f.createJavaType(Integer.class))
-                .build()) {
-
+    @NotNull protected TestTable createTable(IgniteDistribution distr) {
+        return new TestTable(createType()) {
             @Override public IgniteDistribution distribution() {
-                return IgniteDistributions.broadcast();
+                return distr;
             }
         };
-        return tbl;
+    }
+
+    /**
+     * @return REPLICATED test table (ID, VAL0, VAL1, GRP0, GRP1)
+     */
+    @NotNull protected TestTable createBroadcastTable() {
+        return createTable(IgniteDistributions.broadcast());
     }
 
     /**
      * @return PARTITIONED test table (ID, VAL0, VAL1, GRP0, GRP1)
      */
     @NotNull protected TestTable createAffinityTable() {
-        IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
-        return new TestTable(
-            new RelDataTypeFactory.Builder(f)
-                .add("ID", f.createJavaType(Integer.class))
-                .add("VAL0", f.createJavaType(Integer.class))
-                .add("VAL1", f.createJavaType(Integer.class))
-                .add("GRP0", f.createJavaType(Integer.class))
-                .add("GRP1", f.createJavaType(Integer.class))
-                .build()) {
-
+        return new TestTable(createType()) {
             @Override public ColocationGroup colocationGroup(MappingQueryContext ctx) {
                 return ColocationGroup.forAssignments(Arrays.asList(
                     select(nodes, 0, 1),
@@ -84,4 +71,19 @@ public class AbstractAggregatePlannerTest extends AbstractPlannerTest {
             }
         };
     }
+
+    /**
+     * @return Test table rel type.
+     */
+    private static RelDataType createType() {
+        IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
+
+        return new RelDataTypeFactory.Builder(f)
+            .add("ID", f.createJavaType(Integer.class))
+            .add("VAL0", f.createJavaType(Integer.class))
+            .add("VAL1", f.createJavaType(Integer.class))
+            .add("GRP0", f.createJavaType(Integer.class))
+            .add("GRP1", f.createJavaType(Integer.class))
+            .build();
+    }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
index 96a83aee98d..403d7465876 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
@@ -18,12 +18,18 @@
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.fun.SqlAvgAggFunction;
 import org.apache.calcite.sql.fun.SqlCountAggFunction;
+import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.agg.IgniteMapHashAggregate;
 import org.apache.ignite.internal.processors.query.calcite.rel.agg.IgniteReduceHashAggregate;
@@ -42,6 +48,77 @@ import org.junit.Test;
  */
 @SuppressWarnings({"TooBroadScope", "FieldCanBeLocal", "TypeMayBeWeakened"})
 public class HashAggregatePlannerTest extends AbstractAggregatePlannerTest {
+    /**
+     * Tests COUNT(...) plan with and without IndexCount optimization.
+     */
+    @Test
+    public void indexCount() throws Exception {
+        IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
+
+        List<IgniteDistribution> lst = ImmutableList.of(
+            IgniteDistributions.single(),
+            IgniteDistributions.random(),
+            IgniteDistributions.broadcast(),
+            IgniteDistributions.hash(ImmutableList.of(0, 1, 2, 3)));
+
+        for (IgniteDistribution distr : lst) {
+            TestTable tbl = createTable(distr);
+            publicSchema.addTable("TEST", tbl);
+
+            assertNoIndexCount("SELECT COUNT(*) FROM TEST", publicSchema);
+
+            tbl.addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0);
+
+            assertIndexCount("SELECT COUNT(*) FROM TEST", publicSchema);
+            assertIndexCount("SELECT COUNT(1) FROM TEST", publicSchema);
+            assertIndexCount("SELECT COUNT(*) FROM (SELECT * FROM TEST)", publicSchema);
+            assertIndexCount("SELECT COUNT(1) FROM (SELECT * FROM TEST)", publicSchema);
+
+            assertIndexCount("SELECT COUNT(*), COUNT(*), COUNT(1) FROM TEST", publicSchema);
+
+            // Count on certain fields can't be optimized. Nulls are count included.
+            assertNoIndexCount("SELECT COUNT(VAL0) FROM TEST", publicSchema);
+            assertNoIndexCount("SELECT COUNT(DISTINCT VAL0) FROM TEST", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(*), COUNT(VAL0) FROM TEST", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(1), COUNT(VAL0) FROM TEST", publicSchema);
+            assertNoIndexCount("SELECT COUNT(DISTINCT 1), COUNT(VAL0) FROM TEST", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(*) FILTER (WHERE VAL0>1) FROM TEST", publicSchema);
+
+            // IndexCount can't be used with a condition, groups, other aggregates or distincts.
+            assertNoIndexCount("SELECT COUNT(*) FROM TEST WHERE VAL0>1", publicSchema);
+            assertNoIndexCount("SELECT COUNT(1) FROM TEST WHERE VAL0>1", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(*), SUM(VAL0) FROM TEST", publicSchema);
+
+            assertNoIndexCount("SELECT VAL0, COUNT(*) FROM TEST GROUP BY VAL0", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(*) FROM TEST GROUP BY VAL0", publicSchema);
+
+            assertNoIndexCount("SELECT COUNT(*) FILTER (WHERE VAL0>1) FROM TEST", publicSchema);
+
+            publicSchema.addTable("TEST2", createBroadcastTable());
+
+            assertNoIndexCount("SELECT COUNT(*) FROM (SELECT T1.VAL0, T2.VAL1 FROM TEST T1, " +
+                "TEST2 T2 WHERE T1.GRP0 = T2.GRP0)", publicSchema);
+
+            publicSchema.removeTable("TEST");
+        }
+    }
+
+    /** */
+    private void assertIndexCount(String sql, IgniteSchema schema) throws Exception {
+        assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+            .and(nodeOrAnyChild(isInstanceOf(IgniteIndexCount.class)))));
+    }
+
+    /** */
+    private void assertNoIndexCount(String sql, IgniteSchema publicSchema) throws Exception {
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteIndexCount.class)).negate());
+    }
+
     /**
      * @throws Exception If failed.
      */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
index da86c456ac6..114322ac88f 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
@@ -19,14 +19,21 @@ package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 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.rel.IgniteTableScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.agg.IgniteColocatedHashAggregate;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
 
+import static org.apache.calcite.sql.SqlKind.COUNT;
+import static org.apache.calcite.sql.SqlKind.SUM0;
+
 /**
  * Planner test for index rebuild.
  */
@@ -42,7 +49,7 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
         super.setup();
 
         tbl = createTable("TBL", 100, IgniteDistributions.single(), "ID", Integer.class, "VAL", String.class)
-            .addIndex("IDX", 0);
+            .addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0);
 
         publicSchema = createSchema(tbl);
     }
@@ -63,6 +70,28 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
         assertPlan(sql, publicSchema, isInstanceOf(IgniteIndexScan.class));
     }
 
+    /** Test IndexCount is disabled when index is unavailable. */
+    @Test
+    public void testIndexCountAtIndexRebuild() throws Exception {
+        String sql = "SELECT COUNT(*) FROM TBL";
+
+        assertPlan(sql, publicSchema, nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == SUM0).count() == 1)
+            .and(nodeOrAnyChild(isInstanceOf(IgniteIndexCount.class)))));
+
+        tbl.markIndexRebuildInProgress(true);
+
+        assertPlan(sql, publicSchema, isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == COUNT).count() == 1)
+            .and(nodeOrAnyChild(isInstanceOf(IgniteTableScan.class))));
+
+        tbl.markIndexRebuildInProgress(false);
+
+        assertPlan(sql, publicSchema, nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == SUM0).count() == 1)
+            .and(nodeOrAnyChild(isInstanceOf(IgniteIndexCount.class)))));
+    }
+
     /** */
     @Test
     public void testConcurrentIndexRebuildStateChange() throws Exception {
@@ -90,4 +119,32 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
 
         fut.get();
     }
+
+    /**
+     * Test IndexCount is disabled when index becomes unavailable.
+     */
+    @Test
+    public void testIndexCountAtConcurrentIndexRebuild() throws Exception {
+        String sql = "SELECT COUNT(*) FROM TBL";
+
+        AtomicBoolean stop = new AtomicBoolean();
+
+        IgniteInternalFuture<?> fut = GridTestUtils.runAsync(() -> {
+            boolean lever = true;
+
+            while (!stop.get())
+                tbl.markIndexRebuildInProgress(lever = !lever);
+        });
+
+        try {
+            for (int i = 0; i < 1000; i++)
+                assertPlan(sql, publicSchema, nodeOrAnyChild(isInstanceOf(IgniteColocatedHashAggregate.class)
+                    .and(input(isInstanceOf(IgniteIndexCount.class)).or(input(isInstanceOf(IgniteTableScan.class))))));
+        }
+        finally {
+            stop.set(true);
+        }
+
+        fut.get();
+    }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
index ae338ce608e..3e0f38aeb30 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
@@ -100,7 +100,7 @@ public class LimitOffsetPlannerTest extends AbstractPlannerTest {
                         .and(s -> doubleFromRex(s.fetch, -1) == 5.0)
                         .and(s -> s.offset == null))))));
 
-        // No special liited sort required if LIMIT is not set.
+        // No special limited sort required if LIMIT is not set.
         assertPlan("SELECT * FROM TEST ORDER BY ID OFFSET 10", publicSchema,
             isInstanceOf(IgniteLimit.class)
                 .and(input(isInstanceOf(IgniteExchange.class)
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TestTable.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TestTable.java
index ccef0d25796..26fd26d26de 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TestTable.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TestTable.java
@@ -229,7 +229,7 @@ public class TestTable implements IgniteCacheTable {
 
     /** {@inheritDoc} */
     @Override public void removeIndex(String idxName) {
-        throw new AssertionError();
+        indexes.remove(idxName);
     }
 
     /** {@inheritDoc} */