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/12/12 14:57:12 UTC

[ignite] branch master updated: IGNITE-13919 SQL Calcite: Optimization for COUNT(column) if column is indexed - Fixes #10390.

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 b303f3641dc IGNITE-13919 SQL Calcite: Optimization for COUNT(column) if column is indexed - Fixes #10390.
b303f3641dc is described below

commit b303f3641dc43be3aef837aab5765c29ebb23714
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Mon Dec 12 17:54:22 2022 +0300

    IGNITE-13919 SQL Calcite: Optimization for COUNT(column) if column is indexed - Fixes #10390.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../query/calcite/exec/IndexFirstLastScan.java     | 37 +--------------
 .../processors/query/calcite/exec/IndexScan.java   | 29 ++++++++++++
 .../query/calcite/exec/LogicalRelImplementor.java  |  2 +-
 .../query/calcite/rel/IgniteIndexCount.java        | 28 ++++++++---
 .../query/calcite/rule/IndexCountRule.java         | 54 ++++++++++++++++++----
 .../query/calcite/schema/CacheIndexImpl.java       | 43 ++++++++++++++++-
 .../query/calcite/schema/IgniteIndex.java          |  3 +-
 .../query/calcite/schema/SystemViewIndexImpl.java  |  4 +-
 .../calcite/exec/LogicalRelImplementorTest.java    |  2 +-
 .../integration/AbstractBasicIntegrationTest.java  |  4 +-
 .../integration/AggregatesIntegrationTest.java     | 36 ++++++++++++++-
 .../calcite/planner/HashAggregatePlannerTest.java  |  7 ++-
 .../index/sorted/inline/InlineIndexKeyType.java    | 19 ++++++--
 .../inline/types/NullableInlineIndexKeyType.java   | 21 ++++++---
 .../inline/types/StringInlineIndexKeyType.java     |  3 +-
 .../types/StringNoCompareInlineIndexKeyType.java   |  3 +-
 16 files changed, 218 insertions(+), 77 deletions(-)

diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java
index aacada64133..0034c39f46a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java
@@ -16,24 +16,15 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.exec;
 
-import java.util.List;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexImpl;
-import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
-import org.apache.ignite.internal.cache.query.index.sorted.inline.io.InlineIO;
-import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
-import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
-import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
-import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
 import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor;
 import org.apache.ignite.internal.util.lang.GridCursor;
-import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -66,35 +57,9 @@ public class IndexFirstLastScan<Row> extends IndexScan<Row> {
     @Override protected IndexQueryContext indexQueryContext() {
         IndexQueryContext res = super.indexQueryContext();
 
-        BPlusTree.TreeRowClosure<IndexRow, IndexRow> f = res.rowFilter();
-
-        List<InlineIndexKeyType> inlineKeyTypes = idx.segment(0).rowHandler().inlineIndexKeyTypes();
-
-        InlineIndexKeyType keyType = F.isEmpty(inlineKeyTypes) ? null : inlineKeyTypes.get(0);
-
         return new IndexQueryContext(
             res.cacheFilter(),
-            new BPlusTree.TreeRowClosure<IndexRow, IndexRow>() {
-                /** {@inheritDoc} */
-                @Override public boolean apply(
-                    BPlusTree<IndexRow, IndexRow> tree,
-                    BPlusIO<IndexRow> io,
-                    long pageAddr,
-                    int idx
-                ) throws IgniteCheckedException {
-                    if (f != null && !f.apply(tree, io, pageAddr, idx))
-                        return false;
-
-                    if (keyType != null && io instanceof InlineIO) {
-                        IndexKey key = keyType.get(pageAddr, io.offset(idx), ((InlineIO)io).inlineSize());
-
-                        if (key != null)
-                            return key != NullIndexKey.INSTANCE;
-                    }
-
-                    return io.getLookupRow(tree, pageAddr, idx).key(0).type() != IndexKeyType.NULL;
-                }
-            },
+            createNotNullRowFilter(idx),
             res.mvccSnapshot()
         );
     }
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 49a54d10199..1ad6a91cdcb 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
@@ -55,6 +55,7 @@ import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDesc
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
 import org.apache.ignite.internal.util.lang.GridCursor;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.spi.indexing.IndexingQueryFilter;
 import org.apache.ignite.spi.indexing.IndexingQueryFilterImpl;
 import org.jetbrains.annotations.Nullable;
@@ -440,6 +441,34 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
         }
     }
 
+    /**
+     * Creates row filter to skip null values in the first index column.
+     */
+    public static BPlusTree.TreeRowClosure<IndexRow, IndexRow> createNotNullRowFilter(InlineIndex idx) {
+        List<InlineIndexKeyType> inlineKeyTypes = idx.segment(0).rowHandler().inlineIndexKeyTypes();
+
+        InlineIndexKeyType keyType = F.isEmpty(inlineKeyTypes) ? null : inlineKeyTypes.get(0);
+
+        return new BPlusTree.TreeRowClosure<IndexRow, IndexRow>() {
+            /** {@inheritDoc} */
+            @Override public boolean apply(
+                BPlusTree<IndexRow, IndexRow> tree,
+                BPlusIO<IndexRow> io,
+                long pageAddr,
+                int idx
+            ) throws IgniteCheckedException {
+                if (keyType != null && io instanceof InlineIO) {
+                    Boolean keyIsNull = keyType.isNull(pageAddr, io.offset(idx), ((InlineIO)io).inlineSize());
+
+                    if (keyIsNull != null)
+                        return !keyIsNull;
+                }
+
+                return io.getLookupRow(tree, pageAddr, idx).key(0).type() != IndexKeyType.NULL;
+            }
+        };
+    }
+
     /** */
     protected static class TreeIndexWrapper implements TreeIndex<IndexRow> {
         /** Underlying index. */
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 98bc94a8285..07dc8c645f1 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
@@ -408,7 +408,7 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
         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());
+                .create(idx.count(ctx, ctx.group(rel.sourceId()), rel.notNull()))).iterator());
         }
         else {
             CollectNode<Row> replacement = CollectNode.createCountCollector(ctx);
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
index a9bb03e5527..932768df877 100644
--- 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
@@ -49,6 +49,9 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
     /** */
     private final long sourceId;
 
+    /** */
+    private final boolean notNull;
+
     /**
      * Constructor for deserialization.
      *
@@ -65,6 +68,8 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
             sourceId = ((Number)srcIdObj).longValue();
         else
             sourceId = -1L;
+
+        notNull = input.getBoolean("notNull", false);
     }
 
     /**
@@ -74,14 +79,16 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
      * @param traits Traits of this relational expression.
      * @param tbl Table definition.
      * @param idxName Index name.
+     * @param notNull Count only not-null values.
      */
     public IgniteIndexCount(
         RelOptCluster cluster,
         RelTraitSet traits,
         RelOptTable tbl,
-        String idxName
+        String idxName,
+        boolean notNull
     ) {
-        this(-1, cluster, traits, tbl, idxName);
+        this(-1, cluster, traits, tbl, idxName, notNull);
     }
 
     /**
@@ -92,19 +99,22 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
      * @param traits Traits of this relational expression.
      * @param tbl Table definition.
      * @param idxName Index name.
+     * @param notNull Count only not-null values.
      */
     private IgniteIndexCount(
         long sourceId,
         RelOptCluster cluster,
         RelTraitSet traits,
         RelOptTable tbl,
-        String idxName
+        String idxName,
+        boolean notNull
     ) {
         super(cluster, traits);
 
         this.idxName = idxName;
         this.tbl = tbl;
         this.sourceId = sourceId;
+        this.notNull = notNull;
     }
 
     /** {@inheritDoc} */
@@ -145,7 +155,8 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
         return super.explainTerms(pw)
             .item("index", idxName)
             .item("table", tbl.getQualifiedName())
-            .itemIf("sourceId", sourceId, sourceId != -1L);
+            .itemIf("sourceId", sourceId, sourceId != -1L)
+            .item("notNull", notNull);
     }
 
     /** {@inheritDoc} */
@@ -155,11 +166,16 @@ public class IgniteIndexCount extends AbstractRelNode implements SourceAwareIgni
 
     /** {@inheritDoc} */
     @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
-        return new IgniteIndexCount(sourceId, cluster, traitSet, tbl, idxName);
+        return new IgniteIndexCount(sourceId, cluster, traitSet, tbl, idxName, notNull);
     }
 
     /** {@inheritDoc} */
     @Override public IgniteRel clone(long srcId) {
-        return new IgniteIndexCount(srcId, getCluster(), traitSet, tbl, idxName);
+        return new IgniteIndexCount(srcId, getCluster(), traitSet, tbl, idxName, notNull);
+    }
+
+    /** */
+    public boolean notNull() {
+        return notNull;
     }
 }
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
index 475637dfac3..28449d38213 100644
--- 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
@@ -18,11 +18,14 @@
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
 import java.util.Collections;
+import java.util.List;
 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.RelFieldCollation;
+import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
@@ -38,7 +41,7 @@ import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTr
 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. */
+/** Tries to optimize 'COUNT()' to use number of index records. */
 @Value.Enclosing
 public class IndexCountRule extends RelRule<IndexCountRule.Config> {
     /** */
@@ -54,18 +57,50 @@ public class IndexCountRule extends RelRule<IndexCountRule.Config> {
         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())
+            table.isIndexRebuildInProgress() ||
+            scan.condition() != null ||
+            aggr.getGroupCount() > 0 ||
+            aggr.getAggCallList().size() != 1
         )
             return;
 
+        AggregateCall agg = aggr.getAggCallList().get(0);
+
+        if (agg.getAggregation().getKind() != SqlKind.COUNT || agg.hasFilter() || agg.isDistinct())
+            return;
+
+        List<Integer> argList = agg.getArgList();
+
+        IgniteIndex idx = null;
+        boolean notNull = false;
+
+        if (argList.isEmpty())
+            idx = table.getIndex(QueryUtils.PRIMARY_KEY_INDEX);
+        else {
+            if (scan.projects() != null || argList.size() > 1)
+                return;
+
+            notNull = true;
+            int fieldIdx = argList.get(0);
+
+            if (!scan.requiredColumns().isEmpty())
+                fieldIdx = scan.requiredColumns().nth(fieldIdx);
+
+            for (IgniteIndex idx0 : table.indexes().values()) {
+                List<RelFieldCollation> fieldCollations = idx0.collation().getFieldCollations();
+
+                if (!fieldCollations.isEmpty() && fieldCollations.get(0).getFieldIndex() == fieldIdx) {
+                    idx = idx0;
+                    break;
+                }
+            }
+        }
+
+        if (idx == null)
+            return;
+
         RelTraitSet idxTraits = aggr.getTraitSet()
             .replace(IgniteConvention.INSTANCE)
             .replace(table.distribution().getType() == RelDistribution.Type.HASH_DISTRIBUTED ?
@@ -76,7 +111,8 @@ public class IndexCountRule extends RelRule<IndexCountRule.Config> {
             scan.getCluster(),
             idxTraits,
             scan.getTable(),
-            idx.name()
+            idx.name(),
+            notNull
         );
 
         RelBuilder b = call.builder();
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 d096f2ae89b..68c8ea896a0 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
@@ -20,11 +20,13 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
@@ -35,11 +37,14 @@ import org.apache.ignite.internal.cache.query.index.Index;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 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.cache.query.index.sorted.inline.InlineIndexImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyTypeRegistry;
+import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexFirstLastScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
@@ -151,7 +156,7 @@ public class CacheIndexImpl implements IgniteIndex {
     }
 
     /** {@inheritDoc} */
-    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
+    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp, boolean notNull) {
         long cnt = 0;
 
         if (idx != null && grp.nodeIds().contains(ectx.localNodeId())) {
@@ -160,9 +165,43 @@ public class CacheIndexImpl implements IgniteIndex {
 
             InlineIndex iidx = idx.unwrap(InlineIndex.class);
 
+            BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter = null;
+
+            if (notNull) {
+                boolean nullsFirst = collation.getFieldCollations().get(0).nullDirection ==
+                    RelFieldCollation.NullDirection.FIRST;
+
+                BPlusTree.TreeRowClosure<IndexRow, IndexRow> notNullRowFilter = IndexScan.createNotNullRowFilter(iidx);
+
+                AtomicBoolean skipCheck = new AtomicBoolean();
+
+                rowFilter = new BPlusTree.TreeRowClosure<IndexRow, IndexRow>() {
+                    @Override public boolean apply(
+                        BPlusTree<IndexRow, IndexRow> tree,
+                        BPlusIO<IndexRow> io,
+                        long pageAddr,
+                        int idx
+                    ) throws IgniteCheckedException {
+                        // If we have NULLS-FIRST collation, all values after first not-null value will be not-null,
+                        // don't need to check it with notNullRowFilter.
+                        // In case of NULL-LAST collation, all values after first null value will be null,
+                        // don't need to check it too.
+                        if (skipCheck.get())
+                            return nullsFirst;
+
+                        boolean res = notNullRowFilter.apply(tree, io, pageAddr, idx);
+
+                        if (res == nullsFirst)
+                            skipCheck.set(true);
+
+                        return res;
+                    }
+                };
+            }
+
             try {
                 for (int i = 0; i < iidx.segmentsCount(); ++i)
-                    cnt += iidx.count(i, new IndexQueryContext(filter, null, ectx.mvccSnapshot()));
+                    cnt += iidx.count(i, new IndexQueryContext(filter, rowFilter, ectx.mvccSnapshot()));
             }
             catch (IgniteCheckedException e) {
                 throw new IgniteException("Unable to count index records.", e);
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 3f691945936..81ded7289bc 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
@@ -91,9 +91,10 @@ public interface IgniteIndex {
      *
      * @param ectx Execution context.
      * @param grp  Colocation group.
+     * @param notNull Exclude null values.
      * @return Index records number for {@code group}.
      */
-    public long count(ExecutionContext<?> ectx, ColocationGroup grp);
+    public long count(ExecutionContext<?> ectx, ColocationGroup grp, boolean notNull);
 
     /**
      * Takes only first or last not-null index value.
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 ccf6674516d..85717a5e9ad 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
@@ -98,7 +98,9 @@ public class SystemViewIndexImpl implements IgniteIndex {
     }
 
     /** {@inheritDoc} */
-    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
+    @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp, boolean notNull) {
+        assert !notNull; // Collation is empty, cannot come here with "notNull" flag.
+
         return tbl.descriptor().systemView().size();
     }
 
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 78baf7f068d..03ea89b5299 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
@@ -204,7 +204,7 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
     @Test
     public void testIndexCountRewriter() {
         IgniteIndexCount idxCnt = new IgniteIndexCount(cluster, cluster.traitSet(),
-            qctx.catalogReader().getTable(F.asList("PUBLIC", "TBL")), QueryUtils.PRIMARY_KEY_INDEX);
+            qctx.catalogReader().getTable(F.asList("PUBLIC", "TBL")), QueryUtils.PRIMARY_KEY_INDEX, false);
 
         checkCollectNode(relImplementor.visit(idxCnt));
 
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 3cdc981943e..8e3a38476b7 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
@@ -254,8 +254,8 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
         }
 
         /** {@inheritDoc} */
-        @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
-            return delegate.count(ectx, grp);
+        @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp, boolean notNull) {
+            return delegate.count(ectx, grp, notNull);
         }
 
         /** {@inheritDoc} */
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 b2383932656..4b910029cae 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
@@ -108,6 +108,38 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
         assertQuery("select count(*) from person").returns(7L).check();
     }
 
+    /** */
+    @Test
+    public void testCountIndexedField() {
+        createAndPopulateIndexedTable(1, CacheMode.PARTITIONED);
+
+        assertQuery("select count(salary) from person").returns(4L).check();
+        assertQuery("select count(descVal) from person").returns(4L).check();
+        assertQuery("select count(salary + 1) from person").returns(4L).check();
+        assertQuery("select count(distinct descVal) from person").returns(3L).check();
+        assertQuery("select count(salary) from person where salary >= 5").returns(2L).check();
+        assertQuery("select count(salary) filter (where salary >= 5) from person").returns(2L).check();
+        assertQuery("select count(salary), descVal from person group by descVal")
+            .returns(1L, 1d)
+            .returns(1L, 9d)
+            .returns(1L, 15d)
+            .returns(1L, null)
+            .check();
+
+        // Check count with two columns index.
+        sql("CREATE TABLE tbl (a INT, b INT, c INT)");
+        sql("CREATE INDEX idx_a ON tbl(a, c)");
+        sql("CREATE INDEX idx_b ON tbl(b DESC, c)");
+
+        for (int i = 0; i < 100; i++) {
+            sql("INSERT INTO tbl VALUES (null, null, ?)", i % 2 == 0 ? i : null);
+            sql("INSERT INTO tbl VALUES (?, ?, ?)", i, i, i % 2 == 0 ? null : i);
+        }
+
+        assertQuery("SELECT COUNT(a) FROM tbl").returns(100L).check();
+        assertQuery("SELECT COUNT(b) FROM tbl").returns(100L).check();
+    }
+
     /** */
     @Test
     public void testCountOfNonNumericField() {
@@ -308,7 +340,7 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
 
         int idx = 0;
 
-        person.put(idx++, new IndexedEmployer("Igor", 5d, 7d));
+        person.put(idx++, new IndexedEmployer("Igor", 5d, 9d));
         person.put(idx++, new IndexedEmployer(null, 3d, null));
         person.put(idx++, new IndexedEmployer("Ilya", 1d, 1d));
         person.put(idx++, new IndexedEmployer("Roma", null, 9d));
@@ -335,7 +367,7 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
         public IndexedEmployer(String name, Double salary, Double descVal) {
             this.name = name;
             this.salary = salary;
-            this.descVal = salary;
+            this.descVal = descVal;
         }
     }
 }
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 01122091fe0..41a0fe09f16 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
@@ -140,8 +140,13 @@ public class HashAggregatePlannerTest extends AbstractAggregatePlannerTest {
 
             assertIndexCount("SELECT COUNT(*), COUNT(*), COUNT(1) FROM TEST", publicSchema);
 
-            // Count on certain fields can't be optimized. Nulls are count included.
+            assertIndexCount("SELECT COUNT(ID) FROM TEST", publicSchema);
+            assertNoIndexCount("SELECT COUNT(ID + 1) FROM TEST", publicSchema);
             assertNoIndexCount("SELECT COUNT(VAL0) FROM TEST", publicSchema);
+
+            tbl.addIndex("TEST_IDX", 1, 2);
+
+            assertIndexCount("SELECT COUNT(VAL0) FROM TEST", publicSchema);
             assertNoIndexCount("SELECT COUNT(DISTINCT VAL0) FROM TEST", publicSchema);
 
             assertNoIndexCount("SELECT COUNT(*), COUNT(VAL0) FROM TEST", publicSchema);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
index 18f174138ef..fe0fa262ebb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexKeyType.java
@@ -64,7 +64,7 @@ public interface InlineIndexKeyType {
      * @param pageAddr Page address.
      * @param off Offset.
      * @param key Index key.
-     * @param maxSize Max size.
+     * @param maxSize Remaining inlined buffer size (max available bytes to write for the current row).
      * @return Amount of bytes actually stored.
      */
     public int put(long pageAddr, int off, IndexKey key, int maxSize);
@@ -74,17 +74,28 @@ public interface InlineIndexKeyType {
      *
      * @param pageAddr Page address.
      * @param off Offset.
-     * @param maxSize Max size.
+     * @param maxSize Remaining inlined buffer size (max available bytes to read for the current row).
      * @return Index key extracted from index tree.
      */
     @Nullable public IndexKey get(long pageAddr, int off, int maxSize);
 
+    /**
+     * Checks if inlined index key is null.
+     *
+     * @param pageAddr Page address.
+     * @param off Offset.
+     * @param maxSize Remaining inlined buffer size (max available bytes to read for the current row).
+     * @return {@code Boolean.TRUE} if index key is null, {@code Boolean.FALSE} if index key is not null,
+     *      {@code null} if can't say for sure.
+     */
+    @Nullable public Boolean isNull(long pageAddr, int off, int maxSize);
+
     /**
      * Compares inlined and given value.
      *
      * @param pageAddr Page address.
      * @param off Offset.
-     * @param maxSize Max size.
+     * @param maxSize Remaining inlined buffer size (max available bytes to read for the current row).
      * @param v Value that should be compare.
      * @return -1, 0 or 1 if inlined value less, equal or greater
      * than given respectively, or -2 if inlined part is not enough to compare.
@@ -108,7 +119,7 @@ public interface InlineIndexKeyType {
      *
      * @param pageAddr Page address.
      * @param off Offset.
-     * @param maxSize Max size.
+     * @param maxSize Remaining inlined buffer size (max available bytes to read for the current row).
      * @return {@code true} if inline contains full index key. Can be {@code false} for truncated variable length types.
      */
     public boolean inlinedFullValue(long pageAddr, int off, int maxSize);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/NullableInlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/NullableInlineIndexKeyType.java
index f3a86e0f604..6b947620933 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/NullableInlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/NullableInlineIndexKeyType.java
@@ -22,7 +22,6 @@ import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKey
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
 import org.apache.ignite.internal.pagemem.PageUtils;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Abstract inline key. Store base logic for work with inlined keys. Handle NULL values.
@@ -117,12 +116,20 @@ public abstract class NullableInlineIndexKeyType<T extends IndexKey> implements
 
         ensureKeyType(typeCode);
 
-        IndexKey o = get0(pageAddr, off);
+        return get0(pageAddr, off);
+    }
 
-        if (o == null)
-            return NullIndexKey.INSTANCE;
+    /** {@inheritDoc} */
+    @Override public Boolean isNull(long pageAddr, int off, int maxSize) {
+        if (maxSize < 1)
+            return null;
+
+        int typeCode = PageUtils.getByte(pageAddr, off);
+
+        if (typeCode == IndexKeyType.UNKNOWN.code())
+            return null;
 
-        return o;
+        return typeCode == IndexKeyType.NULL.code();
     }
 
     /** {@inheritDoc} */
@@ -170,9 +177,9 @@ public abstract class NullableInlineIndexKeyType<T extends IndexKey> implements
      * @param pageAddr Page address.
      * @param off Offset.
      *
-     * @return Inline value or {@code null} if value can't be restored.
+     * @return Inline value.
      */
-    protected abstract @Nullable T get0(long pageAddr, int off);
+    protected abstract T get0(long pageAddr, int off);
 
     /** Read variable length bytearray */
     public static byte[] readBytes(long pageAddr, int off) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringInlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringInlineIndexKeyType.java
index 593e743d428..83265c4b620 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringInlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringInlineIndexKeyType.java
@@ -24,7 +24,6 @@ import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.StringIndexKey;
 import org.apache.ignite.internal.pagemem.PageUtils;
 import org.apache.ignite.internal.util.GridUnsafe;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Inline index key implementation for inlining {@link String} values.
@@ -67,7 +66,7 @@ public class StringInlineIndexKeyType extends NullableInlineIndexKeyType<StringI
     }
 
     /** {@inheritDoc} */
-    @Override protected @Nullable StringIndexKey get0(long pageAddr, int off) {
+    @Override protected StringIndexKey get0(long pageAddr, int off) {
         String s = new String(readBytes(pageAddr, off), CHARSET);
 
         return new StringIndexKey(s);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringNoCompareInlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringNoCompareInlineIndexKeyType.java
index 37584b2ea34..99ecc21eb73 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringNoCompareInlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/StringNoCompareInlineIndexKeyType.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.cache.query.index.sorted.inline.types;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.StringIndexKey;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Skip optimized String comparison implemented in {@link StringInlineIndexKeyType}.
@@ -40,7 +39,7 @@ public class StringNoCompareInlineIndexKeyType extends NullableInlineIndexKeyTyp
     }
 
     /** {@inheritDoc} */
-    @Override protected @Nullable StringIndexKey get0(long pageAddr, int off) {
+    @Override protected StringIndexKey get0(long pageAddr, int off) {
         return delegate.get0(pageAddr, off);
     }