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/09 06:35:16 UTC

[ignite] branch master updated: IGNITE-13030 SQL Calcite: Inline index scans - Fixes #10397.

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 029754992c5 IGNITE-13030 SQL Calcite: Inline index scans - Fixes #10397.
029754992c5 is described below

commit 029754992c55cd1f25436f21ecf870e18004349a
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Fri Dec 9 09:27:19 2022 +0300

    IGNITE-13030 SQL Calcite: Inline index scans - Fixes #10397.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../benchmarks/jmh/sql/JmhSqlBenchmark.java        |  43 ++++--
 .../benchmarks/jmh/tree/IndexFindBenchmark.java    |   4 +-
 .../processors/query/calcite/exec/IndexScan.java   | 143 ++++++++++++++++++-
 .../query/calcite/rel/AbstractIndexScan.java       |  27 +++-
 .../query/calcite/schema/CacheIndexImpl.java       |  33 +++++
 .../query/calcite/schema/IgniteIndex.java          |   5 +
 .../query/calcite/schema/SystemViewIndexImpl.java  |   5 +
 .../calcite/exec/LogicalRelImplementorTest.java    |   2 +-
 .../integration/AbstractBasicIntegrationTest.java  |   5 +
 .../integration/IndexScanlIntegrationTest.java     | 152 +++++++++++++++++++--
 .../query/calcite/planner/AbstractPlannerTest.java |   3 +-
 .../planner/InlineIndexScanPlannerTest.java        |  76 +++++++++++
 .../calcite/planner/MergeJoinPlannerTest.java      |   3 +-
 .../query/calcite/planner/TestTable.java           |  44 +++++-
 .../apache/ignite/testsuites/PlannerTestSuite.java |   2 +
 .../query/index/IndexQueryCriteriaClosure.java     |   3 +-
 .../cache/query/index/IndexSingleRangeQuery.java   |  14 +-
 ...exSearchRowImpl.java => IndexPlainRowImpl.java} |  18 +--
 .../cache/query/index/sorted/IndexRow.java         |   6 +-
 .../cache/query/index/sorted/IndexRowImpl.java     |  13 +-
 .../index/sorted/client/ClientIndexFactory.java    |   2 +-
 .../index/sorted/client/ClientInlineIndex.java     |  18 ++-
 .../index/sorted/inline/IndexQueryContext.java     |  21 +++
 .../query/index/sorted/inline/InlineIndex.java     |   6 +
 .../query/index/sorted/inline/InlineIndexImpl.java |  12 +-
 .../index/sorted/inline/InlineIndexKeyType.java    |   7 +-
 .../query/index/sorted/inline/InlineIndexTree.java |   4 +-
 .../index/sorted/inline/InlineRecommender.java     |   2 +-
 .../inline/types/BytesInlineIndexKeyType.java      |   7 +-
 .../inline/types/NullableInlineIndexKeyType.java   |  19 +++
 .../types/ObjectByteArrayInlineIndexKeyType.java   |   4 +-
 .../inline/types/StringInlineIndexKeyType.java     |   7 +-
 .../types/StringNoCompareInlineIndexKeyType.java   |   4 +-
 .../cache/persistence/tree/BPlusTree.java          |  63 +++++++--
 .../processors/query/h2/database/H2TreeIndex.java  |   4 +-
 35 files changed, 667 insertions(+), 114 deletions(-)

diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlBenchmark.java
index a4d231e97de..78c0589f1d2 100644
--- a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlBenchmark.java
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/sql/JmhSqlBenchmark.java
@@ -149,7 +149,8 @@ public class JmhSqlBenchmark {
 
         List<?> res = executeSql("SELECT name FROM Item WHERE fld=?", key);
 
-        assert res.size() == 1;
+        if (res.size() != 1)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -161,7 +162,8 @@ public class JmhSqlBenchmark {
 
         List<?> res = executeSql("SELECT name FROM Item WHERE fldIdx=?", key);
 
-        assert res.size() == 1;
+        if (res.size() != 1)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -173,7 +175,8 @@ public class JmhSqlBenchmark {
 
         List<?> res = executeSql("SELECT name FROM Item WHERE fldBatch=?", key / BATCH_SIZE);
 
-        assert res.size() == BATCH_SIZE;
+        if (res.size() != BATCH_SIZE)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -185,7 +188,8 @@ public class JmhSqlBenchmark {
 
         List<?> res = executeSql("SELECT name FROM Item WHERE fldIdxBatch=?", key / BATCH_SIZE);
 
-        assert res.size() == BATCH_SIZE;
+        if (res.size() != BATCH_SIZE)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -195,7 +199,8 @@ public class JmhSqlBenchmark {
     public void queryGroupBy() {
         List<?> res = executeSql("SELECT fldBatch, AVG(fld) FROM Item GROUP BY fldBatch");
 
-        assert res.size() == KEYS_CNT / BATCH_SIZE;
+        if (res.size() != KEYS_CNT / BATCH_SIZE)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -205,7 +210,8 @@ public class JmhSqlBenchmark {
     public void queryGroupByIndexed() {
         List<?> res = executeSql("SELECT fldIdxBatch, AVG(fld) FROM Item GROUP BY fldIdxBatch");
 
-        assert res.size() == KEYS_CNT / BATCH_SIZE;
+        if (res.size() != KEYS_CNT / BATCH_SIZE)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -215,7 +221,8 @@ public class JmhSqlBenchmark {
     public void queryOrderByFull() {
         List<?> res = executeSql("SELECT name, fld FROM Item ORDER BY fld DESC");
 
-        assert res.size() == KEYS_CNT;
+        if (res.size() != KEYS_CNT)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
     /**
@@ -227,14 +234,26 @@ public class JmhSqlBenchmark {
 
         List<?> res = executeSql("SELECT name, fld FROM Item WHERE fldIdxBatch=? ORDER BY fld DESC", key / BATCH_SIZE);
 
-        assert res.size() == BATCH_SIZE;
+        if (res.size() != BATCH_SIZE)
+            throw new AssertionError("Unexpected result size: " + res.size());
     }
 
-    /** */
-    private List<?> executeSql(String sql, Object... args) {
-        List<List<?>> res = cache.query(new SqlFieldsQuery(sql).setArgs(args)).getAll();
+    /**
+     * Query sum of indexed field.
+     */
+    @Benchmark
+    public void querySumIndexed() {
+        List<List<?>> res = executeSql("SELECT sum(fldIdx) FROM Item");
 
-        return res.get(0);
+        Long expRes = ((long)KEYS_CNT) * (KEYS_CNT - 1) / 2;
+
+        if (!expRes.equals(res.get(0).get(0)))
+            throw new AssertionError("Unexpected result: " + res.get(0));
+    }
+
+    /** */
+    private List<List<?>> executeSql(String sql, Object... args) {
+        return cache.query(new SqlFieldsQuery(sql).setArgs(args)).getAll();
     }
 
     /**
diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/tree/IndexFindBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/tree/IndexFindBenchmark.java
index 0b215f945a0..b63525fbe4c 100644
--- a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/tree/IndexFindBenchmark.java
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/tree/IndexFindBenchmark.java
@@ -27,8 +27,8 @@ import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexPlainRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexSearchRowImpl;
 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;
@@ -118,7 +118,7 @@ public class IndexFindBenchmark {
     /** */
     private static IndexRow searchRow(Object key, IndexKeyType type) {
         IndexKey[] keys = new IndexKey[] { IndexKeyFactory.wrap(key, type, null, null), null };
-        return new IndexSearchRowImpl(keys, null);
+        return new IndexPlainRowImpl(keys, null);
     }
 
     /** */
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 7a6ca9ea761..49a54d10199 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
@@ -30,11 +30,14 @@ import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.cluster.ClusterTopologyException;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexPlainRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexSearchRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandler;
 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.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.IndexKeyFactory;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
@@ -44,6 +47,8 @@ import org.apache.ignite.internal.processors.cache.distributed.dht.topology.Grid
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtPartitionState;
 import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtPartitionTopology;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
+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.RowHandler.RowFactory;
 import org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
 import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor;
@@ -91,6 +96,9 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
     /** Mapping from index keys to row fields. */
     private final ImmutableIntList idxFieldMapping;
 
+    /** Mapping from row fields to index keys. */
+    private final int[] fieldIdxMapping;
+
     /** Types of key fields stored in index. */
     private final Type[] fieldsStoreTypes;
 
@@ -163,6 +171,50 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
 
         for (int i = 0; i < srcRowType.getFieldCount(); i++)
             fieldsStoreTypes[i] = typeFactory.getResultClass(srcRowType.getFieldList().get(i).getType());
+
+        fieldIdxMapping = fieldToInlinedKeysMapping(srcRowType.getFieldCount());
+    }
+
+    /**
+     * Checks if we can use inlined index keys instead of cache row iteration and returns fields to keys mapping.
+     *
+     * @return Mapping from target row fields to inlined index keys, or {@code null} if inlined index keys
+     * should not be used.
+     */
+    private int[] fieldToInlinedKeysMapping(int srcFieldsCnt) {
+        List<InlineIndexKeyType> inlinedKeys = idx.segment(0).rowHandler().inlineIndexKeyTypes();
+
+        // Even if we need some subset of inlined keys we are required to the read full inlined row, since this row
+        // is also participated in comparison with other rows when cursor processing the next index page.
+        if (inlinedKeys.size() < idx.segment(0).rowHandler().indexKeyDefinitions().size() ||
+            inlinedKeys.size() < (requiredColumns == null ? srcFieldsCnt : requiredColumns.cardinality()))
+            return null;
+
+        for (InlineIndexKeyType keyType : inlinedKeys) {
+            // Variable length types can be not fully inlined, so it's probably better to directly read full cache row
+            // instead of trying to read inlined value and than falllback to cache row reading.
+            // Inlined JAVA_OBJECT can't be compared with fill cache row in case of hash collision, this can lead to
+            // issues when processing the next index page in cursor if current page was concurrently splitted.
+            if (keyType.keySize() < 0 || keyType.type() == IndexKeyType.JAVA_OBJECT)
+                return null;
+        }
+
+        ImmutableBitSet reqCols = requiredColumns == null ? ImmutableBitSet.range(0, srcFieldsCnt) :
+            requiredColumns;
+
+        int[] fieldIdxMapping = new int[rowType.getFieldCount()];
+
+        for (int i = 0, j = reqCols.nextSetBit(0); j != -1; j = reqCols.nextSetBit(j + 1), i++) {
+            // j = source field index, i = target field index.
+            int keyIdx = idxFieldMapping.indexOf(j);
+
+            if (keyIdx >= 0 && keyIdx < inlinedKeys.size())
+                fieldIdxMapping[i] = keyIdx;
+            else
+                return null;
+        }
+
+        return fieldIdxMapping;
     }
 
     /** {@inheritDoc} */
@@ -208,12 +260,27 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
             }
         }
 
-        return nullSearchRow ? null : new IndexSearchRowImpl(keys, idxRowHnd);
+        return nullSearchRow ? null : new IndexPlainRowImpl(keys, idxRowHnd);
     }
 
     /** {@inheritDoc} */
     @Override protected Row indexRow2Row(IndexRow row) throws IgniteCheckedException {
-        return desc.toRow(ectx, row.cacheDataRow(), factory, requiredColumns);
+        if (row.indexPlainRow())
+            return inlineIndexRow2Row(row);
+        else
+            return desc.toRow(ectx, row.cacheDataRow(), factory, requiredColumns);
+    }
+
+    /** */
+    private Row inlineIndexRow2Row(IndexRow row) {
+        RowHandler<Row> hnd = ectx.rowHandler();
+
+        Row res = factory.create();
+
+        for (int i = 0; i < fieldIdxMapping.length; i++)
+            hnd.set(i, res, TypeUtils.toInternal(ectx, row.key(fieldIdxMapping[i]).key()));
+
+        return res;
     }
 
     /** */
@@ -302,7 +369,75 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
     /** {@inheritDoc} */
     @Override protected IndexQueryContext indexQueryContext() {
         IndexingQueryFilter filter = new IndexingQueryFilterImpl(kctx, topVer, parts);
-        return new IndexQueryContext(filter, null, mvccSnapshot);
+
+        InlineIndexRowHandler rowHnd = idx.segment(0).rowHandler();
+
+        InlineIndexRowFactory rowFactory = isInlineScan() ?
+            new InlineIndexRowFactory(rowHnd.inlineIndexKeyTypes().toArray(new InlineIndexKeyType[0]), rowHnd) : null;
+
+        return new IndexQueryContext(filter, null, rowFactory, mvccSnapshot);
+    }
+
+    /** */
+    public boolean isInlineScan() {
+        return fieldIdxMapping != null;
+    }
+
+    /** */
+    private static class InlineIndexRowFactory implements BPlusTree.TreeRowFactory<IndexRow, IndexRow> {
+        /** Inline key types. */
+        private final InlineIndexKeyType[] keyTypes;
+
+        /** */
+        private final InlineIndexRowHandler idxRowHnd;
+
+        /** Read full cache index row instead of inlined values. */
+        private boolean useCacheRow;
+
+        /** */
+        private InlineIndexRowFactory(
+            InlineIndexKeyType[] keyTypes,
+            InlineIndexRowHandler idxRowHnd
+        ) {
+            this.keyTypes = keyTypes;
+            this.idxRowHnd = idxRowHnd;
+        }
+
+        /** {@inheritDoc} */
+        @Override public IndexRow create(
+            BPlusTree<IndexRow, IndexRow> tree,
+            BPlusIO<IndexRow> io,
+            long pageAddr,
+            int idx
+        ) throws IgniteCheckedException {
+            if (useCacheRow)
+                return io.getLookupRow(tree, pageAddr, idx);
+
+            int inlineSize = ((InlineIO)io).inlineSize();
+            int rowOffset = io.offset(idx);
+            int keyOffset = 0;
+
+            IndexKey[] keys = new IndexKey[keyTypes.length];
+
+            // Check if all required keys is inlined before creating index row.
+            for (int keyIdx = 0; keyIdx < keyTypes.length; keyIdx++) {
+                InlineIndexKeyType keyType = keyTypes[keyIdx];
+
+                if (!keyType.inlinedFullValue(pageAddr, rowOffset + keyOffset, inlineSize - keyOffset)) {
+                    // Since we are checking only fixed-length keys, this condition means that for all rows current
+                    // key type is not fully inlined, so fallback to cache index row.
+                    useCacheRow = true;
+
+                    return io.getLookupRow(tree, pageAddr, idx);
+                }
+
+                keys[keyIdx] = keyType.get(pageAddr, rowOffset + keyOffset, inlineSize - keyOffset);
+
+                keyOffset += keyType.inlineSize(pageAddr, rowOffset + keyOffset);
+            }
+
+            return new IndexPlainRowImpl(keys, idxRowHnd);
+        }
     }
 
     /** */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
index c63a59c28f7..b5a5a7e695d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java
@@ -30,10 +30,13 @@ import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.processors.query.calcite.externalize.RelInputEx;
 import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
 import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds;
+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.util.Commons;
 import org.jetbrains.annotations.Nullable;
 
@@ -82,6 +85,9 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
         pw = super.explainTerms0(pw);
         pw = pw.itemIf("searchBounds", searchBounds, searchBounds != null);
 
+        if (pw.getDetailLevel() == SqlExplainLevel.ALL_ATTRIBUTES)
+            pw = pw.item("inlineScan", isInlineScan());
+
         return pw;
     }
 
@@ -92,14 +98,31 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
         return idxName;
     }
 
+    /** */
+    public boolean isInlineScan() {
+        IgniteTable tbl = table.unwrap(IgniteTable.class);
+        IgniteIndex idx = tbl.getIndex(idxName);
+
+        if (idx != null)
+            return idx.isInlineScanPossible(requiredColumns);
+
+        return false;
+    }
+
     /** {@inheritDoc} */
     @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
         double rows = table.getRowCount();
 
         double cost;
 
+        IgniteTable tbl = table.unwrap(IgniteTable.class);
+        IgniteIndex idx = tbl.getIndex(idxName);
+
+        double inlineReward = (idx != null && isInlineScan()) ?
+            (0.5d + 0.5d * idx.collation().getFieldCollations().size() / table.getRowType().getFieldCount()) : 1d;
+
         if (condition == null)
-            cost = rows * IgniteCost.ROW_PASS_THROUGH_COST;
+            cost = rows * IgniteCost.ROW_PASS_THROUGH_COST * inlineReward;
         else {
             RexBuilder builder = getCluster().getRexBuilder();
 
@@ -119,7 +142,7 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
             if (rows <= 0)
                 rows = 1;
 
-            cost += rows * (IgniteCost.ROW_COMPARISON_COST + IgniteCost.ROW_PASS_THROUGH_COST);
+            cost += rows * (IgniteCost.ROW_COMPARISON_COST + IgniteCost.ROW_PASS_THROUGH_COST * inlineReward);
         }
 
         // additional tiny cost for preventing equality with table scan.
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 368e4cdfd6b..d096f2ae89b 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
@@ -16,6 +16,7 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.schema;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
@@ -27,12 +28,18 @@ 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.calcite.util.ImmutableIntList;
 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.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.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.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexFirstLastScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
@@ -190,4 +197,30 @@ public class CacheIndexImpl implements IgniteIndex {
         // Empty index find predicate.
         return null;
     }
+
+    /** {@inheritDoc} */
+    @Override public boolean isInlineScanPossible(@Nullable ImmutableBitSet requiredColumns) {
+        if (idx == null)
+            return false;
+
+        if (requiredColumns == null)
+            requiredColumns = ImmutableBitSet.range(tbl.descriptor().columnDescriptors().size());
+
+        ImmutableIntList idxKeys = collation.getKeys();
+
+        // All indexed keys should be inlined, all required colummns should be inlined.
+        if (idxKeys.size() < requiredColumns.cardinality() || !ImmutableBitSet.of(idxKeys).contains(requiredColumns))
+            return false;
+
+        List<IndexKeyDefinition> keyDefs = new ArrayList<>(idx.unwrap(InlineIndex.class).indexDefinition()
+            .indexKeyDefinitions().values());
+
+        for (InlineIndexKeyType keyType : InlineIndexKeyTypeRegistry.types(keyDefs, new IndexKeyTypeSettings())) {
+            // Skip variable length keys and java objects (see comments about these limitations in IndexScan class).
+            if (keyType.keySize() < 0 || keyType.type() == IndexKeyType.JAVA_OBJECT)
+                return false;
+        }
+
+        return true;
+    }
 }
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 83e76f2d346..3f691945936 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
@@ -110,4 +110,9 @@ public interface IgniteIndex {
         ColocationGroup grp,
         @Nullable ImmutableBitSet requiredColumns
     );
+
+    /**
+     * If its possible to scan requred columns using inlined index keys.
+     */
+    public boolean isInlineScanPossible(@Nullable ImmutableBitSet requiredColumns);
 }
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 d511aa47e16..ccf6674516d 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
@@ -125,4 +125,9 @@ public class SystemViewIndexImpl implements IgniteIndex {
 
         return RexUtils.buildHashSearchBounds(cluster, cond, rowType, requiredColumns, true);
     }
+
+    /** {@inheritDoc} */
+    @Override public boolean isInlineScanPossible(@Nullable ImmutableBitSet requiredColumns) {
+        return false;
+    }
 }
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 59b16875e4c..78baf7f068d 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
@@ -133,7 +133,7 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
             null
         ) {
             @Override public ColocationGroup group(long srcId) {
-                return ColocationGroup.forNodes(Collections.singletonList(nodeId));
+                return ColocationGroup.forNodes(Collections.emptyList());
             }
         };
 
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 18d390108eb..3cdc981943e 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
@@ -267,6 +267,11 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
         ) {
             return delegate.firstOrLast(first, ectx, grp, requiredColumns);
         }
+
+        /** {@inheritDoc} */
+        @Override public boolean isInlineScanPossible(@Nullable ImmutableBitSet requiredColumns) {
+            return delegate.isInlineScanPossible(requiredColumns);
+        }
     }
 
     /** */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java
index e4c03e9f422..ddd2e320b47 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java
@@ -17,20 +17,33 @@
 
 package org.apache.ignite.internal.processors.query.calcite.integration;
 
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
+import java.util.function.IntFunction;
 import java.util.function.Predicate;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 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.schema.management.SchemaManager;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.G;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 
@@ -38,6 +51,9 @@ import org.junit.Test;
  * Index scan test.
  */
 public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
+    /** */
+    private static final int ROWS_CNT = 100;
+
     /** {@inheritDoc} */
     @Override protected int nodeCount() {
         return 1;
@@ -50,11 +66,7 @@ public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
         executeSql("INSERT INTO t VALUES (0, null), (1, null), (2, 2), (3, null), (4, null), (null, 5)");
         executeSql("CREATE INDEX t_idx ON t(i1)");
 
-        IgniteTable tbl = (IgniteTable)queryProcessor(grid(0)).schemaHolder().schema("PUBLIC").getTable("T");
-
-        RowCountingIndex idx = new RowCountingIndex(tbl.getIndex("T_IDX"));
-
-        tbl.addIndex(idx);
+        RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX");
 
         String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t1.i1, t2.i1 " +
             "FROM t t1 " +
@@ -83,11 +95,7 @@ public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
         executeSql("INSERT INTO t VALUES (null, 0), (1, null), (2, 2), (3, null)");
         executeSql("CREATE INDEX t_idx ON t(i1, i2)");
 
-        IgniteTable tbl = (IgniteTable)queryProcessor(grid(0)).schemaHolder().schema("PUBLIC").getTable("T");
-
-        RowCountingIndex idx = new RowCountingIndex(tbl.getIndex("T_IDX"));
-
-        tbl.addIndex(idx);
+        RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX");
 
         assertQuery("SELECT * FROM t WHERE i1 = ?")
             .withParams(new Object[] { null })
@@ -240,11 +248,121 @@ public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
             .check();
     }
 
+    /** */
+    @Test
+    public void testInlineScan() {
+        // Single column scans.
+        checkSingleColumnInlineScan(true, "INTEGER", i -> i);
+        checkSingleColumnInlineScan(true, "DOUBLE", i -> (double)i);
+        checkSingleColumnInlineScan(true, "UUID", i -> new UUID(0, i));
+        checkSingleColumnInlineScan(true, "TIMESTAMP",
+            i -> new Timestamp(Timestamp.valueOf("2022-01-01 00:00:00").getTime() + TimeUnit.SECONDS.toMillis(i)));
+        checkSingleColumnInlineScan(true, "DATE",
+            i -> new Date(Date.valueOf("2022-01-01").getTime() + TimeUnit.DAYS.toMillis(i)));
+        checkSingleColumnInlineScan(true, "TIME",
+            i -> new Time(Time.valueOf("00:00:00").getTime() + TimeUnit.SECONDS.toMillis(i)));
+        checkSingleColumnInlineScan(false, "VARCHAR", i -> "str" + i);
+        checkSingleColumnInlineScan(false, "DECIMAL", BigDecimal::valueOf);
+
+        // Multi columns scans.
+        executeSql("CREATE TABLE t(id INTEGER PRIMARY KEY, i1 INTEGER, i2 INTEGER, i3 INTEGER)");
+        executeSql("CREATE INDEX t_idx ON t(i1, i3)");
+        RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX");
+
+        for (int i = 0; i < ROWS_CNT; i++)
+            executeSql("INSERT INTO t VALUES (?, ?, ?, ?)", i, i * 2, i * 3, i * 4);
+
+        checkMultiColumnsInlineScan(true, "SELECT i1, i3 FROM t", idx, i -> new Object[] {i * 2, i * 4});
+        checkMultiColumnsInlineScan(false, "SELECT i1, i2 FROM t", idx, i -> new Object[] {i * 2, i * 3});
+        checkMultiColumnsInlineScan(true, "SELECT sum(i1), i3 FROM t GROUP BY i3", idx,
+            i -> new Object[] {(long)i * 2, i * 4});
+    }
+
+    /** */
+    public void checkSingleColumnInlineScan(boolean expInline, String dataType, IntFunction<Object> valFactory) {
+        executeSql("CREATE TABLE t(id INTEGER PRIMARY KEY, val " + dataType + ')');
+
+        try {
+            executeSql("CREATE INDEX t_idx ON t(val)");
+            RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX");
+
+            for (int i = 0; i < ROWS_CNT; i++)
+                executeSql("INSERT INTO t VALUES (?, ?)", i, valFactory.apply(i));
+
+            QueryChecker checker = assertQuery("SELECT val FROM t");
+
+            for (int i = 0; i < ROWS_CNT; i++)
+                checker.returns(valFactory.apply(i));
+
+            if (expInline) {
+                checker.matches(QueryChecker.containsIndexScan("PUBLIC", "T", "T_IDX")).check();
+
+                assertEquals(ROWS_CNT, idx.rowsProcessed());
+                assertTrue(idx.isInlineScan());
+            }
+            else {
+                checker.check();
+
+                assertFalse(idx.isInlineScan());
+            }
+        }
+        finally {
+            executeSql("DROP TABLE t");
+        }
+    }
+
+    /** */
+    public void checkMultiColumnsInlineScan(
+        boolean expInline,
+        String sql,
+        RowCountingIndex idx,
+        IntFunction<Object[]> rowFactory
+    ) {
+        QueryChecker checker = assertQuery(sql);
+
+        for (int i = 0; i < ROWS_CNT; i++)
+            checker.returns(rowFactory.apply(i));
+
+        if (expInline) {
+            checker.matches(QueryChecker.containsIndexScan("PUBLIC", "T", "T_IDX")).check();
+
+            assertEquals(ROWS_CNT, idx.rowsProcessed());
+            assertTrue(idx.isInlineScan());
+        }
+        else {
+            checker.check();
+
+            assertFalse(idx.isInlineScan());
+        }
+    }
+
+    /** */
+    private RowCountingIndex injectRowCountingIndex(IgniteEx node, String tableName, String idxName) {
+        RowCountingIndex idx = null;
+
+        for (Ignite ignite : G.allGrids()) {
+            IgniteTable tbl = (IgniteTable)queryProcessor((IgniteEx)ignite).schemaHolder().schema("PUBLIC").getTable(tableName);
+
+            if (ignite == node) {
+                idx = new RowCountingIndex(tbl.getIndex(idxName));
+
+                tbl.addIndex(idx);
+            }
+
+            tbl.removeIndex(SchemaManager.generateProxyIdxName(idxName));
+        }
+
+        return idx;
+    }
+
     /** */
     private static class RowCountingIndex extends DelegatingIgniteIndex {
         /** */
         private final AtomicInteger filteredRows = new AtomicInteger();
 
+        /** */
+        private final AtomicBoolean isInlineScan = new AtomicBoolean();
+
         /** */
         public RowCountingIndex(IgniteIndex delegate) {
             super(delegate);
@@ -265,14 +383,24 @@ public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
                 return true;
             };
 
-            filters = filter.and(filters);
+            filters = filters == null ? filter : filter.and(filters);
 
-            return delegate.scan(execCtx, grp, filters, ranges, rowTransformer, requiredColumns);
+            IndexScan<Row> scan = (IndexScan<Row>)delegate.scan(execCtx, grp, filters, ranges, rowTransformer,
+                requiredColumns);
+
+            isInlineScan.set(scan.isInlineScan());
+
+            return scan;
         }
 
         /** */
         public int rowsProcessed() {
             return filteredRows.getAndSet(0);
         }
+
+        /** */
+        public boolean isInlineScan() {
+            return isInlineScan.getAndSet(false);
+        }
     }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
index a1ecdecb5c5..64c5858e435 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java
@@ -85,6 +85,7 @@ import org.apache.ignite.internal.processors.query.calcite.schema.ModifyTuple;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.plugin.extensions.communication.Message;
 import org.apache.ignite.testframework.GridTestUtils;
@@ -753,7 +754,7 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
 
         /** {@inheritDoc} */
         @Override public Collection<ColumnDescriptor> columnDescriptors() {
-            throw new AssertionError();
+            return Commons.transform(rowType.getFieldList(), f -> new TestColumnDescriptor(f.getIndex(), f.getName()));
         }
 
         /** {@inheritDoc} */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/InlineIndexScanPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/InlineIndexScanPlannerTest.java
new file mode 100644
index 00000000000..a434da809e4
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/InlineIndexScanPlannerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.planner;
+
+import java.sql.Timestamp;
+import java.util.UUID;
+import org.apache.ignite.internal.processors.query.calcite.rel.AbstractIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.junit.Test;
+
+/**
+ * Planner test for index inline scan.
+ */
+public class InlineIndexScanPlannerTest extends AbstractPlannerTest {
+    /** */
+    @Test
+    public void testInlinScan() throws Exception {
+        TestTable tbl = createTable("TBL", 100, IgniteDistributions.single(),
+            "I0", UUID.class,
+            "I1", Integer.class,
+            "I2", Long.class,
+            "I3", String.class,
+            "I4", Timestamp.class,
+            "I5", Byte.class,
+            "I6", Object.class
+        );
+
+        tbl.addIndex("IDX1", 0, 2, 4);
+        tbl.addIndex("IDX2", 2, 0);
+        tbl.addIndex("IDX3", 5, 3);
+        tbl.addIndex("IDX4", 5, 6);
+
+        IgniteSchema publicSchema = createSchema(tbl);
+
+        // Index IDX1 the only possible index with inlined I4 key.
+        assertPlan("SELECT I4 FROM TBL", publicSchema, isIndexScan("TBL", "IDX1")
+            .and(AbstractIndexScan::isInlineScan));
+
+        // Index IDX2 has less keys count than IDX1.
+        assertPlan("SELECT I2 FROM TBL", publicSchema, isIndexScan("TBL", "IDX2")
+            .and(AbstractIndexScan::isInlineScan));
+
+        // But if we have filter on the first key than IDX1 is prefered.
+        assertPlan("SELECT I2 FROM TBL WHERE I0 = ?", publicSchema, isIndexScan("TBL", "IDX1")
+            .and(AbstractIndexScan::isInlineScan));
+
+        // Index IDX1 is prefered, but inline scan can't be used, since I1 is not inlined.
+        assertPlan("SELECT I2 FROM TBL WHERE I0 = ? AND I1 = ?", publicSchema, isIndexScan("TBL", "IDX1")
+            .and(i -> !i.isInlineScan()));
+
+        // Don't use variable length types for inline scans.
+        assertPlan("SELECT I3 FROM TBL", publicSchema, isTableScan("TBL"));
+
+        // Don't use objects for inline scans.
+        assertPlan("SELECT I6 FROM TBL", publicSchema, isTableScan("TBL"));
+
+        // Don't use any indexes that contain variable length types or objects for inline scans.
+        assertPlan("SELECT I5 FROM TBL", publicSchema, isTableScan("TBL"));
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/MergeJoinPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/MergeJoinPlannerTest.java
index 1bc501afcc7..5cc79b4ef05 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/MergeJoinPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/MergeJoinPlannerTest.java
@@ -26,6 +26,7 @@ import org.apache.calcite.rel.core.Join;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilterableTableScan;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
 import org.junit.Test;
@@ -2772,7 +2773,7 @@ public class MergeJoinPlannerTest extends AbstractPlannerTest {
      */
     private IgniteSort sortOnTopOfScan(IgniteRel root, String tableName) {
         List<IgniteSort> sortNodes = findNodes(root, byClass(IgniteSort.class)
-            .and(node -> node.getInputs().size() == 1 && node.getInput(0) instanceof IgniteTableScan
+            .and(node -> node.getInputs().size() == 1 && node.getInput(0) instanceof ProjectableFilterableTableScan
                 && node.getInput(0).getTable().unwrap(TestTable.class).name().equals(tableName)));
 
         if (sortNodes.size() > 1)
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 26fd26d26de..b07711417bf 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
@@ -17,9 +17,11 @@
 
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -30,8 +32,10 @@ import org.apache.calcite.config.CalciteConnectionConfig;
 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.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeImpl;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.rex.RexNode;
@@ -40,6 +44,15 @@ import org.apache.calcite.schema.Statistic;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.cache.query.index.IndexDefinition;
+import org.apache.ignite.internal.cache.query.index.IndexName;
+import org.apache.ignite.internal.cache.query.index.Order;
+import org.apache.ignite.internal.cache.query.index.SortOrder;
+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.client.ClientIndexDefinition;
+import org.apache.ignite.internal.cache.query.index.sorted.client.ClientInlineIndex;
+import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
@@ -51,9 +64,12 @@ import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteStatisticsImpl;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.stat.ObjectStatisticsImpl;
 import org.jetbrains.annotations.Nullable;
 
+import static org.apache.ignite.internal.processors.query.calcite.planner.AbstractPlannerTest.DEFAULT_SCHEMA;
+
 /** */
 public class TestTable implements IgniteCacheTable {
     /** */
@@ -210,7 +226,33 @@ public class TestTable implements IgniteCacheTable {
 
     /** */
     public TestTable addIndex(RelCollation collation, String name) {
-        indexes.put(name, new CacheIndexImpl(collation, name, null, this));
+        LinkedHashMap<String, IndexKeyDefinition> keyDefs = new LinkedHashMap<>();
+
+        RelDataType rowType = protoType.apply(Commons.typeFactory());
+
+        for (RelFieldCollation fc : collation.getFieldCollations()) {
+            RelDataTypeField field = rowType.getFieldList().get(fc.getFieldIndex());
+
+            Type fieldType = Commons.typeFactory().getResultClass(field.getType());
+
+            // For some reason IndexKeyType.forClass throw an exception for char classes, but we have such
+            // classes in tests.
+            IndexKeyType keyType = (fieldType == Character.class || fieldType == char.class) ? IndexKeyType.STRING_FIXED :
+                fieldType instanceof Class ? IndexKeyType.forClass((Class<?>)fieldType) : IndexKeyType.UNKNOWN;
+
+            Order order = new Order(fc.direction.isDescending() ? SortOrder.DESC : SortOrder.ASC, null);
+
+            keyDefs.put(field.getName(), new IndexKeyDefinition(keyType.code(), order, -1));
+        }
+
+        IndexDefinition idxDef = new ClientIndexDefinition(
+            new IndexName(QueryUtils.createTableCacheName(DEFAULT_SCHEMA, this.name), DEFAULT_SCHEMA, this.name, name),
+            keyDefs,
+            -1,
+            -1
+        );
+
+        indexes.put(name, new CacheIndexImpl(collation, name, new ClientInlineIndex(idxDef, -1), this));
 
         return this;
     }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
index bb92571bd83..fb42d1457eb 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
@@ -25,6 +25,7 @@ import org.apache.ignite.internal.processors.query.calcite.planner.HashAggregate
 import org.apache.ignite.internal.processors.query.calcite.planner.HashIndexSpoolPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.IndexRebuildPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.IndexSearchBoundsPlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.InlineIndexScanPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.JoinColocationPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.JoinCommutePlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.JoinWithUsingPlannerTest;
@@ -73,6 +74,7 @@ import org.junit.runners.Suite;
     IndexRebuildPlannerTest.class,
     PlannerTimeoutTest.class,
     IndexSearchBoundsPlannerTest.class,
+    InlineIndexScanPlannerTest.class,
 })
 public class PlannerTestSuite {
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryCriteriaClosure.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryCriteriaClosure.java
index 3bd5a7ac0e4..690b0884a09 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryCriteriaClosure.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryCriteriaClosure.java
@@ -99,7 +99,8 @@ class IndexQueryCriteriaClosure implements BPlusTree.TreeRowClosure<IndexRow, In
             if (inVals != null) {
                 IndexKey key = null;
 
-                if (keyType != null && keyType.type() != JAVA_OBJECT && keyType.inlinedFullValue(pageAddr, off + fieldOff))
+                if (keyType != null && keyType.type() != JAVA_OBJECT
+                    && keyType.inlinedFullValue(pageAddr, off + fieldOff, maxSize))
                     key = keyType.get(pageAddr, off + fieldOff, maxSize);
 
                 if (key == null) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexSingleRangeQuery.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexSingleRangeQuery.java
index 448c778f399..a95470ad19b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexSingleRangeQuery.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexSingleRangeQuery.java
@@ -18,9 +18,9 @@
 package org.apache.ignite.internal.cache.query.index;
 
 import org.apache.ignite.internal.cache.query.RangeIndexQueryCriterion;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexPlainRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRowComparator;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexSearchRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
 import org.jetbrains.annotations.Nullable;
@@ -43,10 +43,10 @@ class IndexSingleRangeQuery {
     private boolean upperAllNulls = true;
 
     /** Lower bound to query underlying index. */
-    private @Nullable IndexSearchRowImpl lower;
+    private @Nullable IndexPlainRowImpl lower;
 
     /** Upper bound to query underlying index. */
-    private @Nullable IndexSearchRowImpl upper;
+    private @Nullable IndexPlainRowImpl upper;
 
     /** */
     IndexSingleRangeQuery(int idxRowSize, int critSize) {
@@ -101,17 +101,17 @@ class IndexSingleRangeQuery {
     }
 
     /** */
-    @Nullable IndexSearchRowImpl lower() {
+    @Nullable IndexPlainRowImpl lower() {
         if (lower == null && !lowerAllNulls)
-            lower = new IndexSearchRowImpl(lowerBounds, null);
+            lower = new IndexPlainRowImpl(lowerBounds, null);
 
         return lower;
     }
 
     /** */
-    @Nullable IndexSearchRowImpl upper() {
+    @Nullable IndexPlainRowImpl upper() {
         if (upper == null && !upperAllNulls)
-            upper = new IndexSearchRowImpl(upperBounds, null);
+            upper = new IndexPlainRowImpl(upperBounds, null);
 
         return upper;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexSearchRowImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexPlainRowImpl.java
similarity index 77%
rename from modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexSearchRowImpl.java
rename to modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexPlainRowImpl.java
index a800da03652..e647773a708 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexSearchRowImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexPlainRowImpl.java
@@ -22,9 +22,9 @@ import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
 import org.apache.ignite.internal.util.typedef.internal.S;
 
 /**
- * Represents a search row that used to find a place in a tree.
+ * Represents a plain row (not bounded to cache data row) that used to find a place in a tree.
  */
-public class IndexSearchRowImpl implements IndexRow {
+public class IndexPlainRowImpl implements IndexRow {
     /** */
     private final IndexKey[] keys;
 
@@ -32,7 +32,7 @@ public class IndexSearchRowImpl implements IndexRow {
     private final InlineIndexRowHandler rowHnd;
 
     /** Constructor. */
-    public IndexSearchRowImpl(IndexKey[] idxKeys, InlineIndexRowHandler rowHnd) {
+    public IndexPlainRowImpl(IndexKey[] idxKeys, InlineIndexRowHandler rowHnd) {
         keys = idxKeys;
         this.rowHnd = rowHnd;
     }
@@ -43,18 +43,18 @@ public class IndexSearchRowImpl implements IndexRow {
     }
 
     /** {@inheritDoc} */
-    @Override public IndexKey[] keys() {
-        return keys;
+    @Override public int keysCount() {
+        return keys.length;
     }
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return S.toString(IndexSearchRowImpl.class, this);
+        return S.toString(IndexPlainRowImpl.class, this);
     }
 
     /** {@inheritDoc} */
     @Override public long link() {
-        assert false : "Should not get link by IndexSearchRowImpl";
+        assert false : "Should not get link by IndexPlainRowImpl";
 
         return 0;
     }
@@ -66,13 +66,13 @@ public class IndexSearchRowImpl implements IndexRow {
 
     /** {@inheritDoc} */
     @Override public CacheDataRow cacheDataRow() {
-        assert false : "Should not cache data row by IndexSearchRowImpl";
+        assert false : "Should not cache data row by IndexPlainRowImpl";
 
         return null;
     }
 
     /** {@inheritDoc} */
-    @Override public boolean indexSearchRow() {
+    @Override public boolean indexPlainRow() {
         return true;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRow.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRow.java
index ac2e8f99699..d6c2e51f252 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRow.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRow.java
@@ -38,9 +38,9 @@ public interface IndexRow extends MvccVersionAware {
     public IndexKey key(int idx);
 
     /**
-     * @return Underlying keys.
+     * @return Keys count.
      */
-    public IndexKey[] keys();
+    public int keysCount();
 
     /**
      * @return Link to a cache row.
@@ -82,5 +82,5 @@ public interface IndexRow extends MvccVersionAware {
     /**
      * @return {@code True} for rows used for index search (as opposed to rows stored in {@link InlineIndexTree}.
      */
-    public boolean indexSearchRow();
+    public boolean indexPlainRow();
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowImpl.java
index e8e22688eba..2b7e8f5fec1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowImpl.java
@@ -73,15 +73,8 @@ public class IndexRowImpl implements IndexRow {
     }
 
     /** {@inheritDoc} */
-    @Override public IndexKey[] keys() {
-        int keysCnt = rowHnd.indexKeyDefinitions().size();
-
-        IndexKey[] keys = new IndexKey[keysCnt];
-
-        for (int i = 0; i < keysCnt; ++i)
-            keys[i] = key(i);
-
-        return keys;
+    @Override public int keysCount() {
+        return rowHnd.indexKeyDefinitions().size();
     }
 
     /** {@inheritDoc} */
@@ -169,7 +162,7 @@ public class IndexRowImpl implements IndexRow {
     }
 
     /** {@inheritDoc} */
-    @Override public boolean indexSearchRow() {
+    @Override public boolean indexPlainRow() {
         return false;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientIndexFactory.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientIndexFactory.java
index 76b40332d63..6f63445550b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientIndexFactory.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientIndexFactory.java
@@ -63,6 +63,6 @@ public class ClientIndexFactory implements IndexFactory {
             log
         );
 
-        return new ClientInlineIndex(def.idxName().idxName(), inlineSize);
+        return new ClientInlineIndex(def, inlineSize);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
index 9ae649ec00c..1f04dcab397 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.cache.query.index.sorted.client;
 
 import java.util.UUID;
+import org.apache.ignite.internal.cache.query.index.IndexDefinition;
 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;
@@ -31,15 +32,15 @@ public class ClientInlineIndex extends AbstractClientIndex implements InlineInde
     /** */
     private final int inlineSize;
 
-    /** Index name. */
-    private final String name;
-
     /** Index id. */
     private final UUID id = UUID.randomUUID();
 
+    /** Index definition. */
+    private final IndexDefinition def;
+
     /** */
-    public ClientInlineIndex(String idxName, int inlineSize) {
-        name = idxName;
+    public ClientInlineIndex(IndexDefinition def, int inlineSize) {
+        this.def = def;
         this.inlineSize = inlineSize;
     }
 
@@ -123,6 +124,11 @@ public class ClientInlineIndex extends AbstractClientIndex implements InlineInde
 
     /** {@inheritDoc} */
     @Override public String name() {
-        return name;
+        return def.idxName().idxName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public IndexDefinition indexDefinition() {
+        return def;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
index 2e27c708c4d..234fbc2f9d7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/IndexQueryContext.java
@@ -30,6 +30,9 @@ public class IndexQueryContext {
     /** Index rows filter. */
     private final BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter;
 
+    /** Index row factory. */
+    private final BPlusTree.TreeRowFactory<IndexRow, IndexRow> rowFactory;
+
     /** */
     private final MvccSnapshot mvccSnapshot;
 
@@ -38,9 +41,20 @@ public class IndexQueryContext {
         IndexingQueryFilter cacheFilter,
         BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter,
         MvccSnapshot mvccSnapshot
+    ) {
+        this(cacheFilter, rowFilter, null, mvccSnapshot);
+    }
+
+    /** */
+    public IndexQueryContext(
+        IndexingQueryFilter cacheFilter,
+        BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter,
+        BPlusTree.TreeRowFactory<IndexRow, IndexRow> rowFactory,
+        MvccSnapshot mvccSnapshot
     ) {
         this.cacheFilter = cacheFilter;
         this.rowFilter = rowFilter;
+        this.rowFactory = rowFactory;
         this.mvccSnapshot = mvccSnapshot;
     }
 
@@ -64,4 +78,11 @@ public class IndexQueryContext {
     public BPlusTree.TreeRowClosure<IndexRow, IndexRow> rowFilter() {
         return rowFilter;
     }
+
+    /**
+     * @return Index row factory.
+     */
+    public BPlusTree.TreeRowFactory<IndexRow, IndexRow> rowFactory() {
+        return rowFactory;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndex.java
index 35a198e1de5..ffff30f57a8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndex.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndex.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted.inline;
 
+import org.apache.ignite.internal.cache.query.index.IndexDefinition;
 import org.apache.ignite.internal.cache.query.index.sorted.SortedSegmentedIndex;
 
 /**
@@ -38,4 +39,9 @@ public interface InlineIndex extends SortedSegmentedIndex {
      * @return Tree segment for specified number.
      */
     public InlineIndexTree segment(int segment);
+
+    /**
+     * @return Index definition.
+     */
+    public IndexDefinition indexDefinition();
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
index 2d65ac03bca..fd374b150d2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
@@ -46,6 +46,7 @@ import org.apache.ignite.internal.metric.IoStatisticsHolderIndex;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
 import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
+import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
 import org.apache.ignite.internal.util.lang.GridCursor;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.spi.indexing.IndexingQueryCacheFilter;
@@ -103,6 +104,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
         IndexQueryContext qryCtx
     ) throws IgniteCheckedException {
         InlineTreeFilterClosure closure = filterClosure(qryCtx);
+        BPlusTree.TreeRowFactory<IndexRow, IndexRow> rowFactory = qryCtx == null ? null : qryCtx.rowFactory();
 
         lock.readLock().lock();
 
@@ -117,7 +119,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
                 return new SingleCursor<>(row);
             }
 
-            return segments[segment].find(lower, upper, lowIncl, upIncl, closure, null);
+            return segments[segment].find(lower, upper, lowIncl, upIncl, closure, rowFactory, null);
         }
         finally {
             lock.readLock().unlock();
@@ -587,10 +589,8 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
             cctx, def.idxName().cacheName(), def.idxName().tableName(), row.key(), row.value());
     }
 
-    /**
-     * @return Index definition.
-     */
-    public SortedIndexDefinition indexDefinition() {
+    /** {@inheritDoc} */
+    @Override public SortedIndexDefinition indexDefinition() {
         return def;
     }
 
@@ -615,7 +615,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
 
                 @Override public int compare(GridCursor<IndexRow> o1, GridCursor<IndexRow> o2) {
                     try {
-                        int keysLen = o1.get().keys().length;
+                        int keysLen = o1.get().keysCount();
 
                         for (int i = 0; i < keysLen; i++) {
                             int cmp = rowComparator.compareRow(o1.get(), o2.get(), i);
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 47b6ea2707a..18f174138ef 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
@@ -108,9 +108,8 @@ public interface InlineIndexKeyType {
      *
      * @param pageAddr Page address.
      * @param off Offset.
-     * @return {@code true} if inline contains full index key. Can be {@code false} for truncated variable lenght types.
+     * @param maxSize Max size.
+     * @return {@code true} if inline contains full index key. Can be {@code false} for truncated variable length types.
      */
-    public default boolean inlinedFullValue(long pageAddr, int off) {
-        return true;
-    }
+    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/InlineIndexTree.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
index b2641fa93ed..6117592324a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexTree.java
@@ -625,7 +625,7 @@ public class InlineIndexTree extends BPlusTree<IndexRow, IndexRow> {
      * @return Comparison result.
      */
     private int mvccCompare(MvccIO io, long pageAddr, int idx, IndexRow row) {
-        if (!mvccEnabled || row.indexSearchRow())
+        if (!mvccEnabled || row.indexPlainRow())
             return 0;
 
         long crd = io.mvccCoordinatorVersion(pageAddr, idx);
@@ -643,7 +643,7 @@ public class InlineIndexTree extends BPlusTree<IndexRow, IndexRow> {
      * @return Comparison result.
      */
     private int mvccCompare(IndexRow r1, IndexRow r2) {
-        if (!mvccEnabled || r2.indexSearchRow() || r1 == r2)
+        if (!mvccEnabled || r2.indexPlainRow() || r1 == r2)
             return 0;
 
         long crdVer1 = r1.mvccCoordinatorVersion();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineRecommender.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineRecommender.java
index 04e2fed1bdb..253a07cc9cf 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineRecommender.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineRecommender.java
@@ -66,7 +66,7 @@ public class InlineRecommender {
     @SuppressWarnings({"ConditionalBreakInInfiniteLoop", "IfMayBeConditional"})
     public void recommend(IndexRow row, int currInlineSize) {
         // Do the check only for put operations.
-        if (row.indexSearchRow())
+        if (row.indexPlainRow())
             return;
 
         long invokeCnt = inlineSizeCalculationCntr.get();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/BytesInlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/BytesInlineIndexKeyType.java
index 5ed154b558d..a813afa87e2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/BytesInlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/BytesInlineIndexKeyType.java
@@ -89,7 +89,7 @@ public class BytesInlineIndexKeyType extends NullableInlineIndexKeyType<BytesInd
 
         int res = Integer.signum(len1 - len2);
 
-        if (inlinedFullValue(pageAddr, off))
+        if (inlinedFullValue(pageAddr, off, VARTYPE_HEADER_SIZE + 1))
             return res;
 
         if (res >= 0)
@@ -143,9 +143,4 @@ public class BytesInlineIndexKeyType extends NullableInlineIndexKeyType<BytesInd
     public boolean compareBinaryUnsigned() {
         return compareBinaryUnsigned;
     }
-
-    /** {@inheritDoc} */
-    @Override public boolean inlinedFullValue(long pageAddr, int off) {
-        return (PageUtils.getShort(pageAddr, off + 1) & 0x8000) == 0;
-    }
 }
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 030a7b4a6db..f3a86e0f604 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
@@ -34,6 +34,9 @@ public abstract class NullableInlineIndexKeyType<T extends IndexKey> implements
     /** Value for comparison meaning 'Compare not supported for given value'. */
     public static final int COMPARE_UNSUPPORTED = Integer.MIN_VALUE;
 
+    /** Size of header for vartypes inlined values. */
+    public static final int VARTYPE_HEADER_SIZE = 3;
+
     /** Type of this key. */
     private final IndexKeyType type;
 
@@ -234,4 +237,20 @@ public abstract class NullableInlineIndexKeyType<T extends IndexKey> implements
 
     /** Return inlined size for specified key. */
     protected abstract int inlineSize0(T key);
+
+    /** {@inheritDoc} */
+    @Override public boolean inlinedFullValue(long pageAddr, int off, int maxSize) {
+        if (maxSize < 1)
+            return false;
+
+        int type = PageUtils.getByte(pageAddr, off);
+
+        if (type == IndexKeyType.NULL.code())
+            return true;
+
+        if (keySize > 0) // For fixed length types.
+            return maxSize >= keySize + 1;
+        else // For variable length types.
+            return maxSize > VARTYPE_HEADER_SIZE && (PageUtils.getShort(pageAddr, off + 1) & 0x8000) == 0;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/ObjectByteArrayInlineIndexKeyType.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/ObjectByteArrayInlineIndexKeyType.java
index 81ada03b74c..02f171049e9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/ObjectByteArrayInlineIndexKeyType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/types/ObjectByteArrayInlineIndexKeyType.java
@@ -69,7 +69,7 @@ public class ObjectByteArrayInlineIndexKeyType extends NullableInlineIndexKeyTyp
     }
 
     /** {@inheritDoc} */
-    @Override public boolean inlinedFullValue(long pageAddr, int offset) {
-        return delegate.inlinedFullValue(pageAddr, offset);
+    @Override public boolean inlinedFullValue(long pageAddr, int offset, int maxSize) {
+        return delegate.inlinedFullValue(pageAddr, offset, maxSize);
     }
 }
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 b7ffa1df680..593e743d428 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
@@ -230,7 +230,7 @@ public class StringInlineIndexKeyType extends NullableInlineIndexKeyType<StringI
 
         int res = cntr1 == len1 && cntr2 == len2 ? 0 : cntr1 == len1 ? -1 : 1;
 
-        if (inlinedFullValue(pageAddr, off))
+        if (inlinedFullValue(pageAddr, off, VARTYPE_HEADER_SIZE + 1))
             return res;
 
         if (res >= 0)
@@ -264,11 +264,6 @@ public class StringInlineIndexKeyType extends NullableInlineIndexKeyType<StringI
         return null;
     }
 
-    /** {@inheritDoc} */
-    @Override public boolean inlinedFullValue(long pageAddr, int off) {
-        return (PageUtils.getShort(pageAddr, off + 1) & 0x8000) == 0;
-    }
-
     /** {@inheritDoc} */
     @Override protected int inlineSize0(StringIndexKey key) {
         return ((String)key.key()).getBytes(CHARSET).length + 3;
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 bcc2ea85f7c..37584b2ea34 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
@@ -55,7 +55,7 @@ public class StringNoCompareInlineIndexKeyType extends NullableInlineIndexKeyTyp
     }
 
     /** {@inheritDoc} */
-    @Override public boolean inlinedFullValue(long pageAddr, int off) {
-        return delegate.inlinedFullValue(pageAddr, off);
+    @Override public boolean inlinedFullValue(long pageAddr, int off, int maxSize) {
+        return delegate.inlinedFullValue(pageAddr, off, maxSize);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
index f65a4bf2769..e4b1bfdb2c2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/BPlusTree.java
@@ -1231,12 +1231,19 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
      * @param upper Upper bound.
      * @param upIncl {@code true} if upper bound is inclusive.
      * @param c Filter closure.
+     * @param rowFactory Row factory or (@code null} for default factory.
      * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
      * @return Cursor.
      * @throws IgniteCheckedException If failed.
      */
-    private GridCursor<T> findLowerUnbounded(L upper, boolean upIncl, TreeRowClosure<L, T> c, Object x) throws IgniteCheckedException {
-        ForwardCursor cursor = new ForwardCursor(upper, upIncl, c, x);
+    private GridCursor<T> findLowerUnbounded(
+        L upper,
+        boolean upIncl,
+        TreeRowClosure<L, T> c,
+        TreeRowFactory<L, T> rowFactory,
+        Object x
+    ) throws IgniteCheckedException {
+        ForwardCursor cursor = new ForwardCursor(upper, upIncl, c, rowFactory, x);
 
         long firstPageId;
 
@@ -1299,7 +1306,7 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
      * @throws IgniteCheckedException If failed.
      */
     public GridCursor<T> find(L lower, L upper, TreeRowClosure<L, T> c, Object x) throws IgniteCheckedException {
-        return find(lower, upper, true, true, c, x);
+        return find(lower, upper, true, true, c, null, x);
     }
 
     /**
@@ -1308,6 +1315,7 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
      * @param lowIncl {@code true} if lower bound is inclusive.
      * @param upIncl {@code true} if upper bound is inclusive.
      * @param c Filter closure.
+     * @param rowFactory Row factory or (@code null} for default factory.
      * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
      * @return Cursor.
      * @throws IgniteCheckedException If failed.
@@ -1318,15 +1326,16 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
         boolean lowIncl,
         boolean upIncl,
         TreeRowClosure<L, T> c,
+        TreeRowFactory<L, T> rowFactory,
         Object x
     ) throws IgniteCheckedException {
         checkDestroyed();
 
-        ForwardCursor cursor = new ForwardCursor(lower, upper, lowIncl, upIncl, c, x);
+        ForwardCursor cursor = new ForwardCursor(lower, upper, lowIncl, upIncl, c, rowFactory, x);
 
         try {
             if (lower == null)
-                return findLowerUnbounded(upper, upIncl, c, x);
+                return findLowerUnbounded(upper, upIncl, c, rowFactory, x);
 
             cursor.find();
 
@@ -6114,16 +6123,20 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
         /** */
         private final TreeRowClosure<L, T> c;
 
+        /** */
+        private final TreeRowFactory<L, T> rowFactory;
+
         /**
          * Lower unbound cursor.
          *
          * @param upperBound Upper bound.
          * @param upIncl {@code true} if upper bound is inclusive.
          * @param c Filter closure.
+         * @param rowFactory Row factory or (@code null} for default factory.
          * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
          */
-        ForwardCursor(L upperBound, boolean upIncl, TreeRowClosure<L, T> c, Object x) {
-            this(null, upperBound, true, upIncl, c, x);
+        ForwardCursor(L upperBound, boolean upIncl, TreeRowClosure<L, T> c, TreeRowFactory<L, T> rowFactory, Object x) {
+            this(null, upperBound, true, upIncl, c, rowFactory, x);
         }
 
         /**
@@ -6132,12 +6145,22 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
          * @param lowIncl {@code true} if lower bound is inclusive.
          * @param upIncl {@code true} if upper bound is inclusive.
          * @param c Filter closure.
+         * @param rowFactory Row factory or (@code null} for default factory.
          * @param x Implementation specific argument, {@code null} always means that we need to return full detached data row.
          */
-        ForwardCursor(L lowerBound, L upperBound, boolean lowIncl, boolean upIncl, TreeRowClosure<L, T> c, Object x) {
+        ForwardCursor(
+            L lowerBound,
+            L upperBound,
+            boolean lowIncl,
+            boolean upIncl,
+            TreeRowClosure<L, T> c,
+            TreeRowFactory<L, T> rowFactory,
+            Object x
+        ) {
             super(lowerBound, upperBound, lowIncl, upIncl);
 
             this.c = c;
+            this.rowFactory = rowFactory;
             this.x = x;
         }
 
@@ -6164,8 +6187,10 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
             int resCnt = 0;
 
             for (int idx = startIdx; idx < cnt; idx++) {
-                if (c == null || c.apply(BPlusTree.this, io, pageAddr, idx))
-                    rows = GridArrays.set(rows, resCnt++, getRow(io, pageAddr, idx, x));
+                if (c == null || c.apply(BPlusTree.this, io, pageAddr, idx)) {
+                    rows = GridArrays.set(rows, resCnt++, rowFactory == null ? getRow(io, pageAddr, idx, x) :
+                        rowFactory.create(BPlusTree.this, io, pageAddr, idx));
+                }
             }
 
             if (resCnt == 0) {
@@ -6372,6 +6397,24 @@ public abstract class BPlusTree<L, T extends L> extends DataStructure implements
             throws IgniteCheckedException;
     }
 
+    /**
+     * Row factory from page memory.
+     */
+    public interface TreeRowFactory<L, T extends L> {
+        /**
+         * Creates row.
+         *
+         * @param tree The tree.
+         * @param io The tree IO object.
+         * @param pageAddr The page address.
+         * @param idx The item index.
+         * @return Created index row.
+         * @throws IgniteCheckedException If failed.
+         */
+        public T create(BPlusTree<L, T> tree, BPlusIO<L> io, long pageAddr, int idx)
+            throws IgniteCheckedException;
+    }
+
     /**
      * A generic visitor-style interface for performing inspection/modification operations on the tree.
      */
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
index 6e2c4945b18..f49acd2c96c 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndex.java
@@ -31,9 +31,9 @@ import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.GridTopic;
 import org.apache.ignite.internal.cache.query.index.Index;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexPlainRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexRowImpl;
-import org.apache.ignite.internal.cache.query.index.sorted.IndexSearchRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexValueCursor;
 import org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandler;
 import org.apache.ignite.internal.cache.query.index.sorted.SortedIndexDefinition;
@@ -315,7 +315,7 @@ public class H2TreeIndex extends H2TreeIndexBase {
                 v.getObject(), v.getType(), cctx.cacheObjectContext(), queryIndex.keyTypeSettings());
         }
 
-        return new IndexSearchRowImpl(keys, rowHnd);
+        return new IndexPlainRowImpl(keys, rowHnd);
     }
 
     /** */