You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by tl...@apache.org on 2021/04/07 13:31:34 UTC

[ignite] branch sql-calcite updated: IGNITE-13546 Calcite integration. Introduce hash index spool

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

tledkov pushed a commit to branch sql-calcite
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/sql-calcite by this push:
     new 6df422f  IGNITE-13546 Calcite integration. Introduce hash index spool
6df422f is described below

commit 6df422fe8c1fdbec810336dab34707d33731a252
Author: tledkov <tl...@gridgain.com>
AuthorDate: Wed Apr 7 16:31:16 2021 +0300

    IGNITE-13546 Calcite integration. Introduce hash index spool
---
 .../query/calcite/exec/LogicalRelImplementor.java  |  25 +++-
 .../query/calcite/exec/RuntimeHashIndex.java       | 112 ++++++++++++++
 .../query/calcite/exec/RuntimeIndex.java           |  28 ++++
 .../query/calcite/exec/RuntimeTreeIndex.java       |  13 +-
 .../query/calcite/exec/rel/IndexSpoolNode.java     |  74 ++++++---
 .../query/calcite/metadata/IgniteMdRowCount.java   |   4 +-
 .../calcite/metadata/IgniteMdSelectivity.java      |  18 ++-
 .../query/calcite/metadata/cost/IgniteCost.java    |   3 +
 .../processors/query/calcite/prepare/Cloner.java   |   9 +-
 .../query/calcite/prepare/IgniteRelShuttle.java    |  10 +-
 .../query/calcite/prepare/PlannerPhase.java        |   6 +-
 .../rel/IgniteCorrelatedNestedLoopJoin.java        |   9 +-
 ...teIndexSpool.java => IgniteHashIndexSpool.java} |  72 ++++-----
 .../query/calcite/rel/IgniteRelVisitor.java        |   7 +-
 .../processors/query/calcite/rel/IgniteSort.java   |   5 +-
 ...IndexSpool.java => IgniteSortedIndexSpool.java} |  42 +++---
 .../query/calcite/rel/IgniteTableSpool.java        |   8 +-
 .../rel/logical/IgniteLogicalIndexScan.java        |  16 +-
 ...a => FilterSpoolMergeToHashIndexSpoolRule.java} |  41 +++--
 ...=> FilterSpoolMergeToSortedIndexSpoolRule.java} |  21 +--
 .../query/calcite/util/IndexConditions.java        |  13 +-
 .../processors/query/calcite/util/RexUtils.java    |  71 ++++++++-
 .../exec/rel/HashIndexSpoolExecutionTest.java      | 166 +++++++++++++++++++++
 ...nTest.java => TreeIndexSpoolExecutionTest.java} |   4 +-
 .../query/calcite/planner/AbstractPlannerTest.java |   2 +
 .../CorrelatedNestedLoopJoinPlannerTest.java       |   4 +-
 ...nerTest.java => HashIndexSpoolPlannerTest.java} |  96 ++++--------
 ...rTest.java => SortedIndexSpoolPlannerTest.java} |  92 ++----------
 .../calcite/planner/TableSpoolPlannerTest.java     |   5 +-
 .../ignite/testsuites/ExecutionTestSuite.java      |   6 +-
 .../apache/ignite/testsuites/PlannerTestSuite.java |   6 +-
 31 files changed, 673 insertions(+), 315 deletions(-)

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 0f891bb..f9cfcf5 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
@@ -59,8 +59,8 @@ import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGr
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
@@ -70,6 +70,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
@@ -375,7 +376,7 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
     }
 
     /** {@inheritDoc} */
-    @Override public Node<Row> visit(IgniteIndexSpool rel) {
+    @Override public Node<Row> visit(IgniteSortedIndexSpool rel) {
         RelCollation collation = rel.collation();
 
         assert rel.indexCondition() != null : rel;
@@ -387,7 +388,7 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
         Supplier<Row> lower = lowerBound == null ? null : expressionFactory.rowSource(lowerBound);
         Supplier<Row> upper = upperBound == null ? null : expressionFactory.rowSource(upperBound);
 
-        IndexSpoolNode<Row> node = new IndexSpoolNode<>(
+        IndexSpoolNode<Row> node = IndexSpoolNode.createTreeSpool(
             ctx,
             rel.getRowType(),
             collation,
@@ -405,6 +406,24 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
     }
 
     /** {@inheritDoc} */
+    @Override public Node<Row> visit(IgniteHashIndexSpool rel) {
+        Supplier<Row> searchRow = expressionFactory.rowSource(rel.searchRow());
+
+        IndexSpoolNode<Row> node = IndexSpoolNode.createHashSpool(
+            ctx,
+            rel.getRowType(),
+            ImmutableBitSet.of(rel.keys()),
+            searchRow
+        );
+
+        Node<Row> input = visit(rel.getInput());
+
+        node.register(input);
+
+        return node;
+    }
+
+    /** {@inheritDoc} */
     @Override public Node<Row> visit(IgniteTableModify rel) {
         switch (rel.getOperation()) {
             case INSERT:
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java
new file mode 100644
index 0000000..1666ba2
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java
@@ -0,0 +1,112 @@
+/*
+ * 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.exec;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.GroupKey;
+import org.apache.ignite.internal.util.typedef.F;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Runtime hash index based on on-heap hash map.
+ */
+public class RuntimeHashIndex<Row> implements RuntimeIndex<Row> {
+    /** */
+    protected final ExecutionContext<Row> ectx;
+
+    /** */
+    private final ImmutableBitSet keys;
+
+    /** Rows. */
+    private HashMap<GroupKey, List<Row>> rows;
+
+    /**
+     *
+     */
+    public RuntimeHashIndex(
+        ExecutionContext<Row> ectx,
+        ImmutableBitSet keys
+    ) {
+        this.ectx = ectx;
+
+        assert !F.isEmpty(keys);
+
+        this.keys = keys;
+        rows = new HashMap<>();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void push(Row r) {
+        List<Row> eqRows = rows.computeIfAbsent(key(r), k -> new ArrayList<>());
+
+        eqRows.add(r);
+    }
+
+    /** */
+    @Override public void close() {
+        rows.clear();
+    }
+
+    /** */
+    public Iterable<Row> scan(Supplier<Row> searchRow) {
+        return new IndexScan(searchRow);
+    }
+
+    /** */
+    private GroupKey key(Row r) {
+        GroupKey.Builder b = GroupKey.builder(keys.cardinality());
+
+        for (Integer field : keys)
+            b.add(ectx.rowHandler().get(field, r));
+
+        return b.build();
+    }
+
+    /**
+     *
+     */
+    private class IndexScan implements Iterable<Row>, AutoCloseable {
+        /** Search row. */
+        private final Supplier<Row> searchRow;
+
+        /**
+         * @param searchRow Search row.
+         */
+        IndexScan(Supplier<Row> searchRow) {
+            this.searchRow = searchRow;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @NotNull @Override public Iterator<Row> iterator() {
+            List<Row> eqRows = rows.get(key(searchRow.get()));
+
+            return eqRows == null ? Collections.emptyIterator() : eqRows.iterator();
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeIndex.java
new file mode 100644
index 0000000..01d257c
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeIndex.java
@@ -0,0 +1,28 @@
+/*
+ * 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.exec;
+
+/**
+ * Runtime index interface.
+ * The temporary index is built and available only on query execution. Not stored at the schema.
+ */
+public interface RuntimeIndex<Row> extends AutoCloseable {
+    /**
+     * Add row to index.
+     */
+    void push(Row r);
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeTreeIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeTreeIndex.java
index cfd5669..541ba60 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeTreeIndex.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeTreeIndex.java
@@ -38,7 +38,7 @@ import org.apache.ignite.internal.util.typedef.F;
 /**
  * Runtime sorted index based on on-heap tree.
  */
-public class RuntimeTreeIndex<Row> implements GridIndex<Row>, AutoCloseable {
+public class RuntimeTreeIndex<Row> implements RuntimeIndex<Row>, GridIndex<Row> {
     /** */
     protected final ExecutionContext<Row> ectx;
 
@@ -68,10 +68,8 @@ public class RuntimeTreeIndex<Row> implements GridIndex<Row>, AutoCloseable {
         rows = new TreeMap<>(comp);
     }
 
-    /**
-     * Add row to index.
-     */
-    public void push(Row r) {
+    /** {@inheritDoc} */
+    @Override public void push(Row r) {
         List<Row> newEqRows = new ArrayList<>();
 
         List<Row> eqRows = rows.putIfAbsent(r, newEqRows);
@@ -82,7 +80,7 @@ public class RuntimeTreeIndex<Row> implements GridIndex<Row>, AutoCloseable {
             newEqRows.add(r);
     }
 
-    /** */
+    /** {@inheritDoc} */
     @Override public void close() {
         rows.clear();
     }
@@ -104,8 +102,7 @@ public class RuntimeTreeIndex<Row> implements GridIndex<Row>, AutoCloseable {
     }
 
     /**
-     * Return an iterable to scan index range from lower to upper bounds inclusive,
-     * filtered by {@code filter} predicate.
+     * Creates iterable on the index.
      */
     public Iterable<Row> scan(
         ExecutionContext<Row> ectx,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolNode.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolNode.java
index 51607d3..6287688 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolNode.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolNode.java
@@ -23,7 +23,10 @@ import java.util.function.Supplier;
 
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.exec.RuntimeHashIndex;
+import org.apache.ignite.internal.processors.query.calcite.exec.RuntimeIndex;
 import org.apache.ignite.internal.processors.query.calcite.exec.RuntimeTreeIndex;
 import org.apache.ignite.internal.util.typedef.F;
 
@@ -35,7 +38,7 @@ public class IndexSpoolNode<Row> extends AbstractNode<Row> implements SingleNode
     private final ScanNode<Row> scan;
 
     /** Runtime index */
-    private final RuntimeTreeIndex<Row> idx;
+    private final RuntimeIndex<Row> idx;
 
     /** */
     private int requested;
@@ -46,30 +49,16 @@ public class IndexSpoolNode<Row> extends AbstractNode<Row> implements SingleNode
     /**
      * @param ctx Execution context.
      */
-    public IndexSpoolNode(
+    private IndexSpoolNode(
         ExecutionContext<Row> ctx,
         RelDataType rowType,
-        RelCollation collation,
-        Comparator<Row> comp,
-        Predicate<Row> filter,
-        Supplier<Row> lowerIdxBound,
-        Supplier<Row> upperIdxBound
+        RuntimeIndex<Row> idx,
+        ScanNode<Row> scan
     ) {
         super(ctx, rowType);
 
-        idx = new RuntimeTreeIndex<>(ctx, collation, comp);
-
-        scan = new ScanNode<>(
-            ctx,
-            rowType,
-            idx.scan(
-                ctx,
-                rowType,
-                filter,
-                lowerIdxBound,
-                upperIdxBound
-            )
-        );
+        this.idx = idx;
+        this.scan = scan;
     }
 
     /** */
@@ -167,4 +156,49 @@ public class IndexSpoolNode<Row> extends AbstractNode<Row> implements SingleNode
     private boolean indexReady() {
         return waiting == -1;
     }
+
+    /** */
+    public static <Row> IndexSpoolNode<Row> createTreeSpool(
+        ExecutionContext<Row> ctx,
+        RelDataType rowType,
+        RelCollation collation,
+        Comparator<Row> comp,
+        Predicate<Row> filter,
+        Supplier<Row> lowerIdxBound,
+        Supplier<Row> upperIdxBound
+    ) {
+        RuntimeTreeIndex<Row> idx = new RuntimeTreeIndex<>(ctx, collation, comp);
+
+        ScanNode<Row> scan = new ScanNode<>(
+            ctx,
+            rowType,
+            idx.scan(
+                ctx,
+                rowType,
+                filter,
+                lowerIdxBound,
+                upperIdxBound
+            )
+        );
+
+        return new IndexSpoolNode<>(ctx, rowType, idx, scan);
+    }
+
+    /** */
+    public static <Row> IndexSpoolNode<Row> createHashSpool(
+        ExecutionContext<Row> ctx,
+        RelDataType rowType,
+        ImmutableBitSet keys,
+        Supplier<Row> searchRow
+    ) {
+        RuntimeHashIndex<Row> idx = new RuntimeHashIndex<>(ctx, keys);
+
+        ScanNode<Row> scan = new ScanNode<>(
+            ctx,
+            rowType,
+            idx.scan(searchRow)
+        );
+
+        return new IndexSpoolNode<>(ctx, rowType, idx, scan);
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdRowCount.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdRowCount.java
index 57cc7b2..8be9f1a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdRowCount.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdRowCount.java
@@ -30,7 +30,7 @@ import org.apache.calcite.util.BuiltInMethod;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Util;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.Nullable;
 
@@ -111,7 +111,7 @@ public class IgniteMdRowCount extends RelMdRowCount {
      * but IndexSpool has internal filter that could filter out some rows,
      * hence we need to estimate it differently.
      */
-    public double getRowCount(IgniteIndexSpool rel, RelMetadataQuery mq) {
+    public double getRowCount(IgniteSortedIndexSpool rel, RelMetadataQuery mq) {
         return rel.estimateRowCount(mq);
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
index 624466c..86ad57a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdSelectivity.java
@@ -31,7 +31,8 @@ import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.util.BuiltInMethod;
 import org.apache.ignite.internal.processors.query.calcite.rel.AbstractIndexScan;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilterableTableScan;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.apache.ignite.internal.util.typedef.F;
@@ -95,7 +96,20 @@ public class IgniteMdSelectivity extends RelMdSelectivity {
     }
 
     /** */
-    public Double getSelectivity(IgniteIndexSpool rel, RelMetadataQuery mq, RexNode predicate) {
+    public Double getSelectivity(IgniteSortedIndexSpool rel, RelMetadataQuery mq, RexNode predicate) {
+        if (predicate != null) {
+            return mq.getSelectivity(rel.getInput(),
+                RelMdUtil.minusPreds(
+                    rel.getCluster().getRexBuilder(),
+                    predicate,
+                    rel.condition()));
+        }
+
+        return mq.getSelectivity(rel.getInput(), rel.condition());
+    }
+
+    /** */
+    public Double getSelectivity(IgniteHashIndexSpool rel, RelMetadataQuery mq, RexNode predicate) {
         if (predicate != null) {
             return mq.getSelectivity(rel.getInput(),
                 RelMdUtil.minusPreds(
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/cost/IgniteCost.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/cost/IgniteCost.java
index 60bd64e..84d40f3 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/cost/IgniteCost.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/cost/IgniteCost.java
@@ -40,6 +40,9 @@ public class IgniteCost implements RelOptCost {
     /** Memory cost of a aggregate call. */
     public static final double AGG_CALL_MEM_COST = 5;
 
+    /** Cost of a lookup at the hash. */
+    public static final double HASH_LOOKUP_COST = 10;
+
     /**
      * With broadcast distribution each row will be sent to the each distination node,
      * thus the total bytes amount will be multiplies of the destination nodes count.
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
index 1b2d530..5ef854a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
@@ -22,8 +22,8 @@ import org.apache.calcite.plan.RelOptCluster;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
@@ -33,6 +33,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
@@ -170,7 +171,7 @@ public class Cloner implements IgniteRelVisitor<IgniteRel> {
     }
 
     /** {@inheritDoc} */
-    @Override public IgniteRel visit(IgniteIndexSpool rel) {
+    @Override public IgniteRel visit(IgniteSortedIndexSpool rel) {
         return rel.clone(cluster, F.asList(visit((IgniteRel) rel.getInput())));
     }
 
@@ -218,6 +219,10 @@ public class Cloner implements IgniteRelVisitor<IgniteRel> {
         return rel.clone(cluster, F.asList(visit((IgniteRel) rel.getInput())));
     }
 
+    @Override public IgniteRel visit(IgniteHashIndexSpool rel) {
+        return rel.clone(cluster, F.asList(visit((IgniteRel) rel.getInput())));
+    }
+
     /** {@inheritDoc} */
     @Override public IgniteRel visit(IgniteRel rel) {
         return rel.accept(this);
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
index 5f05cc6..a069f19 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
@@ -22,8 +22,8 @@ import java.util.List;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
@@ -33,6 +33,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRelVisitor;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
@@ -165,7 +166,12 @@ public class IgniteRelShuttle implements IgniteRelVisitor<IgniteRel> {
     }
 
     /** {@inheritDoc} */
-    @Override public IgniteRel visit(IgniteIndexSpool rel) {
+    @Override public IgniteRel visit(IgniteSortedIndexSpool rel) {
+        return processNode(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteHashIndexSpool rel) {
         return processNode(rel);
     }
 
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
index e108efa..3dced8d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
@@ -41,7 +41,8 @@ import org.apache.calcite.tools.RuleSet;
 import org.apache.calcite.tools.RuleSets;
 import org.apache.ignite.internal.processors.query.calcite.rule.CorrelatedNestedLoopJoinRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.FilterConverterRule;
-import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeToHashIndexSpoolRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeToSortedIndexSpoolRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.HashAggregateConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.LogicalScanConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.MergeJoinConverterRule;
@@ -174,7 +175,8 @@ public enum PlannerPhase {
                     TableModifyConverterRule.INSTANCE,
                     UnionConverterRule.INSTANCE,
                     SortConverterRule.INSTANCE,
-                    FilterSpoolMergeRule.INSTANCE
+                    FilterSpoolMergeToSortedIndexSpoolRule.INSTANCE,
+                    FilterSpoolMergeToHashIndexSpoolRule.INSTANCE
                 )
             );
         }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteCorrelatedNestedLoopJoin.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteCorrelatedNestedLoopJoin.java
index da6cab5..c760dc6 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteCorrelatedNestedLoopJoin.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteCorrelatedNestedLoopJoin.java
@@ -106,7 +106,7 @@ public class IgniteCorrelatedNestedLoopJoin extends AbstractIgniteJoin {
         List<Integer> newRightCollationFields = maxPrefix(rightCollation.getKeys(), joinInfo.leftKeys);
 
         if (F.isEmpty(newRightCollationFields))
-            return ImmutableList.of();
+            return ImmutableList.of(Pair.of(nodeTraits.replace(RelCollations.EMPTY), inputTraits));
 
         // We preserve left edge collation only if batch size == 1
         if (variablesSet.size() == 1)
@@ -138,9 +138,6 @@ public class IgniteCorrelatedNestedLoopJoin extends AbstractIgniteJoin {
     ) {
         RelTraitSet left = inputTraits.get(0), right = inputTraits.get(1);
 
-        // Index lookup (collation) is required for right input.
-        RelCollation rightReplace = RelCollations.of(joinInfo.rightKeys);
-
         // We preserve left edge collation only if batch size == 1
         if (variablesSet.size() == 1) {
             Pair<RelTraitSet, List<RelTraitSet>> baseTraits = super.passThroughCollation(nodeTraits, inputTraits);
@@ -149,13 +146,13 @@ public class IgniteCorrelatedNestedLoopJoin extends AbstractIgniteJoin {
                 baseTraits.getKey(),
                 ImmutableList.of(
                     baseTraits.getValue().get(0),
-                    baseTraits.getValue().get(1).replace(rightReplace)
+                    baseTraits.getValue().get(1)
                 )
             );
         }
 
         return Pair.of(nodeTraits.replace(RelCollations.EMPTY),
-            ImmutableList.of(left.replace(RelCollations.EMPTY), right.replace(rightReplace)));
+            ImmutableList.of(left.replace(RelCollations.EMPTY), right));
     }
 
     /** {@inheritDoc} */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
similarity index 67%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
index dc9e062..055d192 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteHashIndexSpool.java
@@ -18,54 +18,53 @@
 package org.apache.ignite.internal.processors.query.calcite.rel;
 
 import java.util.List;
-import java.util.Objects;
 
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.core.Spool;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
 import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCostFactory;
-import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
+import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
+import org.apache.ignite.internal.util.typedef.F;
 
 /**
- * Relational operator that returns the sorted contents of a table
- * and allow to lookup rows by specified bounds.
+ * Relational operator that returns the hashed contents of a table
+ * and allow to lookup rows by specified keys.
  */
-public class IgniteIndexSpool extends Spool implements IgniteRel {
-    /** */
-    private final RelCollation collation;
+public class IgniteHashIndexSpool extends Spool implements IgniteRel {
+    /** Search row. */
+    private final List<RexNode> searchRow;
 
-    /** Index condition. */
-    private final IndexConditions idxCond;
+    /** Keys (number of the columns at the input row) to build hash index. */
+    private final ImmutableBitSet keys;
 
-    /** Filters. */
-    protected final RexNode condition;
+    /** Condition (used to calculate selectivity). */
+    private final RexNode cond;
 
     /** */
-    public IgniteIndexSpool(
+    public IgniteHashIndexSpool(
         RelOptCluster cluster,
         RelTraitSet traits,
         RelNode input,
-        RelCollation collation,
-        RexNode condition,
-        IndexConditions idxCond
+        List<RexNode> searchRow,
+        RexNode cond
     ) {
         super(cluster, traits, input, Type.LAZY, Type.EAGER);
 
-        assert Objects.nonNull(idxCond);
-        assert Objects.nonNull(condition);
+        assert !F.isEmpty(searchRow);
+
+        this.searchRow = searchRow;
+        this.cond = cond;
 
-        this.idxCond = idxCond;
-        this.condition = condition;
-        this.collation = collation;
+        keys = ImmutableBitSet.of(RexUtils.notNullKeys(searchRow));
     }
 
     /**
@@ -73,13 +72,12 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
      *
      * @param input Serialized representation.
      */
-    public IgniteIndexSpool(RelInput input) {
+    public IgniteHashIndexSpool(RelInput input) {
         this(input.getCluster(),
             input.getTraitSet().replace(IgniteConvention.INSTANCE),
             input.getInputs().get(0),
-            input.getCollation(),
-            input.getExpression("condition"),
-            new IndexConditions(input)
+            input.getExpressionList("searchRow"),
+            null
         );
     }
 
@@ -90,12 +88,12 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
 
     /** */
     @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
-        return new IgniteIndexSpool(cluster, getTraitSet(), inputs.get(0), collation, condition, idxCond);
+        return new IgniteHashIndexSpool(cluster, getTraitSet(), inputs.get(0), searchRow, cond);
     }
 
     /** {@inheritDoc} */
     @Override protected Spool copy(RelTraitSet traitSet, RelNode input, Type readType, Type writeType) {
-        return new IgniteIndexSpool(getCluster(), traitSet, input, collation, condition, idxCond);
+        return new IgniteHashIndexSpool(getCluster(), traitSet, input, searchRow, cond);
     }
 
     /** {@inheritDoc} */
@@ -107,10 +105,7 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
     @Override public RelWriter explainTerms(RelWriter pw) {
         RelWriter writer = super.explainTerms(pw);
 
-        writer.item("condition", condition);
-        writer.item("collation", collation);
-
-        return idxCond.explainTerms(writer);
+        return writer.item("searchRow", searchRow);
     }
 
     /** {@inheritDoc} */
@@ -118,10 +113,7 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
         double rowCnt = mq.getRowCount(getInput());
         double bytesPerRow = getRowType().getFieldCount() * IgniteCost.AVERAGE_FIELD_SIZE;
         double totalBytes = rowCnt * bytesPerRow;
-        double cpuCost = rowCnt * IgniteCost.ROW_PASS_THROUGH_COST;
-
-        if (idxCond.lowerCondition() != null)
-            cpuCost += Math.log(rowCnt) * IgniteCost.ROW_COMPARISON_COST;
+        double cpuCost = IgniteCost.HASH_LOOKUP_COST;
 
         IgniteCostFactory costFactory = (IgniteCostFactory)planner.getCostFactory();
 
@@ -134,17 +126,17 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
     }
 
     /** */
-    public IndexConditions indexCondition() {
-        return idxCond;
+    public List<RexNode> searchRow() {
+        return searchRow;
     }
 
     /** */
-    @Override public RelCollation collation() {
-        return collation;
+    public ImmutableBitSet keys() {
+        return keys;
     }
 
     /** */
     public RexNode condition() {
-        return condition;
+        return cond;
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
index e47dbf6..072b587e 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
@@ -141,7 +141,7 @@ public interface IgniteRelVisitor<T> {
     /**
      * See {@link IgniteRelVisitor#visit(IgniteRel)}
      */
-    T visit(IgniteIndexSpool rel);
+    T visit(IgniteSortedIndexSpool rel);
 
     /**
      * See {@link IgniteRelVisitor#visit(IgniteRel)}
@@ -149,6 +149,11 @@ public interface IgniteRelVisitor<T> {
     T visit(IgniteLimit rel);
 
     /**
+     * See {@link IgniteRelVisitor#visit(IgniteRel)}
+     */
+    T visit(IgniteHashIndexSpool rel);
+
+    /**
      * Visits a relational node and calculates a result on the basis of node meta information.
      * @param rel Relational node.
      * @return Visit result.
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSort.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSort.java
index 964204b..b0cf10e 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSort.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSort.java
@@ -17,6 +17,7 @@
 package org.apache.ignite.internal.processors.query.calcite.rel;
 
 import java.util.List;
+
 import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
@@ -109,12 +110,12 @@ public class IgniteSort extends Sort implements IgniteRel {
     @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
         double rows = mq.getRowCount(getInput());
 
-        double cost = rows * IgniteCost.ROW_PASS_THROUGH_COST + Util.nLogN(rows) * IgniteCost.ROW_COMPARISON_COST;
+        double cpuCost = rows * IgniteCost.ROW_PASS_THROUGH_COST + Util.nLogN(rows) * IgniteCost.ROW_COMPARISON_COST;
         double memory = rows * getRowType().getFieldCount() * IgniteCost.AVERAGE_FIELD_SIZE;
 
         IgniteCostFactory costFactory = (IgniteCostFactory)planner.getCostFactory();
 
-        return costFactory.makeCost(rows, cost, 0, memory, 0);
+        return costFactory.makeCost(rows, cpuCost, 0, memory, 0);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
similarity index 89%
rename from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java
rename to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
index dc9e062..8dfea74 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexSpool.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSortedIndexSpool.java
@@ -39,7 +39,7 @@ import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
  * Relational operator that returns the sorted contents of a table
  * and allow to lookup rows by specified bounds.
  */
-public class IgniteIndexSpool extends Spool implements IgniteRel {
+public class IgniteSortedIndexSpool extends Spool implements IgniteRel {
     /** */
     private final RelCollation collation;
 
@@ -50,7 +50,7 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
     protected final RexNode condition;
 
     /** */
-    public IgniteIndexSpool(
+    public IgniteSortedIndexSpool(
         RelOptCluster cluster,
         RelTraitSet traits,
         RelNode input,
@@ -73,7 +73,7 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
      *
      * @param input Serialized representation.
      */
-    public IgniteIndexSpool(RelInput input) {
+    public IgniteSortedIndexSpool(RelInput input) {
         this(input.getCluster(),
             input.getTraitSet().replace(IgniteConvention.INSTANCE),
             input.getInputs().get(0),
@@ -90,12 +90,12 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
 
     /** */
     @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
-        return new IgniteIndexSpool(cluster, getTraitSet(), inputs.get(0), collation, condition, idxCond);
+        return new IgniteSortedIndexSpool(cluster, getTraitSet(), inputs.get(0), collation, condition, idxCond);
     }
 
     /** {@inheritDoc} */
     @Override protected Spool copy(RelTraitSet traitSet, RelNode input, Type readType, Type writeType) {
-        return new IgniteIndexSpool(getCluster(), traitSet, input, collation, condition, idxCond);
+        return new IgniteSortedIndexSpool(getCluster(), traitSet, input, collation, condition, idxCond);
     }
 
     /** {@inheritDoc} */
@@ -114,21 +114,6 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
     }
 
     /** {@inheritDoc} */
-    @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
-        double rowCnt = mq.getRowCount(getInput());
-        double bytesPerRow = getRowType().getFieldCount() * IgniteCost.AVERAGE_FIELD_SIZE;
-        double totalBytes = rowCnt * bytesPerRow;
-        double cpuCost = rowCnt * IgniteCost.ROW_PASS_THROUGH_COST;
-
-        if (idxCond.lowerCondition() != null)
-            cpuCost += Math.log(rowCnt) * IgniteCost.ROW_COMPARISON_COST;
-
-        IgniteCostFactory costFactory = (IgniteCostFactory)planner.getCostFactory();
-
-        return costFactory.makeCost(rowCnt, cpuCost, 0, totalBytes, 0);
-    }
-
-    /** {@inheritDoc} */
     @Override public double estimateRowCount(RelMetadataQuery mq) {
         return mq.getRowCount(getInput()) * mq.getSelectivity(this, null);
     }
@@ -147,4 +132,21 @@ public class IgniteIndexSpool extends Spool implements IgniteRel {
     public RexNode condition() {
         return condition;
     }
+
+    /** {@inheritDoc} */
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+        double rowCnt = mq.getRowCount(getInput());
+        double bytesPerRow = getRowType().getFieldCount() * IgniteCost.AVERAGE_FIELD_SIZE;
+        double totalBytes = rowCnt * bytesPerRow;
+        double cpuCost;
+
+        if (idxCond.lowerCondition() != null)
+            cpuCost = Math.log(rowCnt) * IgniteCost.ROW_COMPARISON_COST;
+        else
+            cpuCost = rowCnt * IgniteCost.ROW_PASS_THROUGH_COST;
+
+        IgniteCostFactory costFactory = (IgniteCostFactory)planner.getCostFactory();
+
+        return costFactory.makeCost(rowCnt, cpuCost, 0, totalBytes, 0);
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableSpool.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableSpool.java
index da8c441..fc5d095 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableSpool.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteTableSpool.java
@@ -80,13 +80,13 @@ public class IgniteTableSpool extends Spool implements IgniteRel {
 
     /** {@inheritDoc} */
     @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
-        double rowCount = mq.getRowCount(getInput());
+        double rowCnt = mq.getRowCount(getInput());
         double bytesPerRow = getRowType().getFieldCount() * IgniteCost.AVERAGE_FIELD_SIZE;
-        double totalBytes = rowCount * bytesPerRow;
+        double totalBytes = rowCnt * bytesPerRow;
+        double cpuCost = rowCnt * IgniteCost.ROW_PASS_THROUGH_COST;
 
         IgniteCostFactory costFactory = (IgniteCostFactory)planner.getCostFactory();
 
-        return costFactory.makeCost(rowCount,
-            rowCount * IgniteCost.ROW_PASS_THROUGH_COST, 0, totalBytes, 0);
+        return costFactory.makeCost(rowCnt, cpuCost, 0, totalBytes, 0);
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
index a174cf8..a28c59b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
@@ -62,12 +62,16 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
                 collation = TraitUtils.projectCollation(collation, proj, rowType);
         }
 
-        IndexConditions idxCond = RexUtils.buildIndexConditions(
-            cluster,
-            collation,
-            cond,
-            tbl.getRowType(typeFactory),
-            requiredColumns);
+        IndexConditions idxCond = new IndexConditions();
+
+        if (collation != null && !collation.getFieldCollations().isEmpty()) {
+            idxCond = RexUtils.buildSortedIndexConditions(
+                cluster,
+                collation,
+                cond,
+                tbl.getRowType(typeFactory),
+                requiredColumns);
+        }
 
         return new IgniteLogicalIndexScan(
             cluster,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToHashIndexSpoolRule.java
similarity index 76%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToHashIndexSpoolRule.java
index eb989ea..cd1e0bd 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToHashIndexSpoolRule.java
@@ -16,34 +16,36 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
+import java.util.List;
+
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Spool;
+import org.apache.calcite.rex.RexNode;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
 import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTrait;
 import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
-import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.apache.ignite.internal.util.typedef.F;
 
 /**
  * Rule that pushes filter into the spool.
  */
-public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
+public class FilterSpoolMergeToHashIndexSpoolRule extends RelRule<FilterSpoolMergeToHashIndexSpoolRule.Config> {
     /** Instance. */
     public static final RelOptRule INSTANCE = Config.DEFAULT.toRule();
 
     /** */
-    private FilterSpoolMergeRule(Config cfg) {
+    private FilterSpoolMergeToHashIndexSpoolRule(Config cfg) {
         super(cfg);
     }
 
@@ -62,26 +64,21 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
 
         RelNode input = spool.getInput();
 
-        IndexConditions idxCond = RexUtils.buildIndexConditions(
+        List<RexNode> searchRow = RexUtils.buildHashSearchRow(
             cluster,
-            TraitUtils.collation(input),
             filter.getCondition(),
-            spool.getRowType(),
-            null
+            spool.getRowType()
         );
 
-        if (F.isEmpty(idxCond.lowerCondition()) && F.isEmpty(idxCond.upperCondition()))
+        if (F.isEmpty(searchRow))
             return;
 
-        RelCollation collation = TraitUtils.collation(input);
-        
-        RelNode res = new IgniteIndexSpool(
+        RelNode res = new IgniteHashIndexSpool(
             cluster,
-            trait.replace(collation),
-            convert(input, input.getTraitSet().replace(collation)),
-            collation,
-            filter.getCondition(),
-            idxCond
+            trait.replace(RelCollations.EMPTY),
+            input,
+            searchRow,
+            filter.getCondition()
         );
 
         call.transformTo(res);
@@ -93,8 +90,8 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
         /** */
         Config DEFAULT = RelRule.Config.EMPTY
             .withRelBuilderFactory(RelFactories.LOGICAL_BUILDER)
-            .withDescription("FilterSpoolMergeRule")
-            .as(FilterSpoolMergeRule.Config.class)
+            .withDescription("FilterSpoolMergeToHashIndexSpoolRule")
+            .as(FilterSpoolMergeToHashIndexSpoolRule.Config.class)
             .withOperandFor(IgniteFilter.class, IgniteTableSpool.class);
 
         /** Defines an operand tree for the given classes. */
@@ -109,8 +106,8 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
         }
 
         /** {@inheritDoc} */
-        @Override default FilterSpoolMergeRule toRule() {
-            return new FilterSpoolMergeRule(this);
+        @Override default FilterSpoolMergeToHashIndexSpoolRule toRule() {
+            return new FilterSpoolMergeToHashIndexSpoolRule(this);
         }
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
similarity index 83%
rename from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java
rename to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
index eb989ea..a447997 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
@@ -16,6 +16,7 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
+import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
@@ -27,7 +28,7 @@ import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Spool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
 import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTrait;
 import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
@@ -38,12 +39,12 @@ import org.apache.ignite.internal.util.typedef.F;
 /**
  * Rule that pushes filter into the spool.
  */
-public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
+public class FilterSpoolMergeToSortedIndexSpoolRule extends RelRule<FilterSpoolMergeToSortedIndexSpoolRule.Config> {
     /** Instance. */
     public static final RelOptRule INSTANCE = Config.DEFAULT.toRule();
 
     /** */
-    private FilterSpoolMergeRule(Config cfg) {
+    private FilterSpoolMergeToSortedIndexSpoolRule(Config cfg) {
         super(cfg);
     }
 
@@ -62,7 +63,7 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
 
         RelNode input = spool.getInput();
 
-        IndexConditions idxCond = RexUtils.buildIndexConditions(
+        IndexConditions idxCond = RexUtils.buildSortedIndexConditions(
             cluster,
             TraitUtils.collation(input),
             filter.getCondition(),
@@ -73,9 +74,9 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
         if (F.isEmpty(idxCond.lowerCondition()) && F.isEmpty(idxCond.upperCondition()))
             return;
 
-        RelCollation collation = TraitUtils.collation(input);
+        RelCollation collation = TraitUtils.createCollation(ImmutableList.copyOf(idxCond.keys()));
         
-        RelNode res = new IgniteIndexSpool(
+        RelNode res = new IgniteSortedIndexSpool(
             cluster,
             trait.replace(collation),
             convert(input, input.getTraitSet().replace(collation)),
@@ -93,8 +94,8 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
         /** */
         Config DEFAULT = RelRule.Config.EMPTY
             .withRelBuilderFactory(RelFactories.LOGICAL_BUILDER)
-            .withDescription("FilterSpoolMergeRule")
-            .as(FilterSpoolMergeRule.Config.class)
+            .withDescription("FilterSpoolMergeToSortedIndexSpoolRule")
+            .as(FilterSpoolMergeToSortedIndexSpoolRule.Config.class)
             .withOperandFor(IgniteFilter.class, IgniteTableSpool.class);
 
         /** Defines an operand tree for the given classes. */
@@ -109,8 +110,8 @@ public class FilterSpoolMergeRule extends RelRule<FilterSpoolMergeRule.Config> {
         }
 
         /** {@inheritDoc} */
-        @Override default FilterSpoolMergeRule toRule() {
-            return new FilterSpoolMergeRule(this);
+        @Override default FilterSpoolMergeToSortedIndexSpoolRule toRule() {
+            return new FilterSpoolMergeToSortedIndexSpoolRule(this);
         }
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IndexConditions.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IndexConditions.java
index 230652b..b0a0212 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IndexConditions.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IndexConditions.java
@@ -17,13 +17,14 @@
 
 package org.apache.ignite.internal.processors.query.calcite.util;
 
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.jetbrains.annotations.Nullable;
@@ -101,11 +102,11 @@ public class IndexConditions {
     }
 
     /** */
-    public ImmutableIntList keys() {
+    public Set<Integer> keys() {
         if (upperBound == null && lowerBound == null)
-            return ImmutableIntList.of();
+            return Collections.emptySet();
 
-        List<Integer> keys = new ArrayList<>();
+        Set<Integer> keys = new HashSet<>();
 
         int cols = lowerBound != null ? lowerBound.size() : upperBound.size();
 
@@ -115,7 +116,7 @@ public class IndexConditions {
                 keys.add(i);
         }
 
-        return ImmutableIntList.copyOf(keys);
+        return Collections.unmodifiableSet(keys);
     }
 
     /**
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
index da3a94e..047f0c2 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
@@ -32,6 +32,7 @@ import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
@@ -59,6 +60,7 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.ControlFlowException;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Litmus;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.mapping.MappingType;
@@ -159,14 +161,14 @@ public class RexUtils {
     /**
      * Builds index conditions.
      */
-    public static IndexConditions buildIndexConditions(
+    public static IndexConditions buildSortedIndexConditions(
         RelOptCluster cluster,
         RelCollation collation,
         RexNode condition,
         RelDataType rowType,
         ImmutableBitSet requiredColumns
     ) {
-        if (condition == null || collation == null || collation.getFieldCollations().isEmpty())
+        if (condition == null)
             return new IndexConditions();
 
         condition = RexUtil.toCnf(builder(cluster), condition);
@@ -179,6 +181,10 @@ public class RexUtils {
         List<RexNode> lower = new ArrayList<>();
         List<RexNode> upper = new ArrayList<>();
 
+        // Force collation for all fields of the condition.
+        if (collation == null || collation.isDefault())
+            collation = RelCollations.of(ImmutableIntList.copyOf(fieldsToPredicates.keySet()));
+
         for (int i = 0; i < collation.getFieldCollations().size(); i++) {
             RelFieldCollation fc = collation.getFieldCollations().get(i);
 
@@ -275,6 +281,52 @@ public class RexUtils {
         return new IndexConditions(lower, upper, lowerBound, upperBound);
     }
 
+    /**
+     * Builds index conditions.
+     */
+    public static List<RexNode> buildHashSearchRow(
+        RelOptCluster cluster,
+        RexNode condition,
+        RelDataType rowType
+    ) {
+        condition = RexUtil.toCnf(builder(cluster), condition);
+
+        Map<Integer, List<RexCall>> fieldsToPredicates = mapPredicatesToFields(condition, cluster);
+
+        if (F.isEmpty(fieldsToPredicates))
+            return null;
+
+        List<RexNode> searchPreds = null;
+
+        for (int fldIdx : fieldsToPredicates.keySet()) {
+            List<RexCall> collFldPreds = fieldsToPredicates.get(fldIdx);
+
+            if (F.isEmpty(collFldPreds))
+                break;
+
+            for (RexCall pred : collFldPreds) {
+                if (U.assertionsEnabled()) {
+                    RexNode cond = RexUtil.removeCast(pred.operands.get(1));
+
+                    assert idxOpSupports(cond) : cond;
+                }
+
+                if (pred.getOperator().kind != SqlKind.EQUALS)
+                    return null;
+
+                if (searchPreds == null)
+                    searchPreds = new ArrayList<>();
+
+                searchPreds.add(pred);
+            }
+        }
+
+        if (searchPreds == null)
+            return null;
+
+        return asBound(cluster, searchPreds, rowType, null);
+    }
+
     /** */
     private static Map<Integer, List<RexCall>> mapPredicatesToFields(RexNode condition, RelOptCluster cluster) {
         List<RexNode> conjunctions = RelOptUtil.conjunctions(condition);
@@ -469,6 +521,21 @@ public class RexUtils {
     }
 
     /** */
+    public static Set<Integer> notNullKeys(List<RexNode> row) {
+        if (F.isEmpty(row))
+            return Collections.emptySet();
+
+        Set<Integer> keys = new HashSet<>();
+
+        for (int i = 0; i < row.size(); ++i ) {
+            if (isNotNull(row.get(i)))
+                keys.add(i);
+        }
+
+        return keys;
+    }
+
+    /** */
     public static Set<CorrelationId> extractCorrelationIds(List<RexNode> nodes) {
         final Set<CorrelationId> cors = new HashSet<>();
 
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java
new file mode 100644
index 0000000..be17583
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.exec.rel;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+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.GridTuple3;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+@WithSystemProperty(key = "calcite.debug", value = "true")
+public class HashIndexSpoolExecutionTest extends AbstractExecutionTest {
+    /**
+     * @throws Exception If failed.
+     */
+    @Before
+    @Override public void setup() throws Exception {
+        nodesCnt = 1;
+        super.setup();
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testIndexSpool() throws Exception {
+        ExecutionContext<Object[]> ctx = executionContext(F.first(nodes()), UUID.randomUUID(), 0);
+        IgniteTypeFactory tf = ctx.getTypeFactory();
+        RelDataType rowType = TypeUtils.createRowType(tf, int.class, String.class, int.class);
+
+        int inBufSize = U.field(AbstractNode.class, "IN_BUFFER_SIZE");
+
+        int[] sizes = {1, inBufSize / 2 - 1, inBufSize / 2, inBufSize / 2 + 1, inBufSize, inBufSize + 1, inBufSize * 4};
+        int[] eqCnts = {1, 10};
+
+        for (int size : sizes) {
+            for (int eqCnt : eqCnts) {
+                // (filter, search, expected result size)
+                GridTuple3<Predicate<Object[]>, Object[], Integer>[] testBounds;
+
+                if (size == 1) {
+                    testBounds = new GridTuple3[] {
+                        new GridTuple3(null, new Object[] {0, null, null}, eqCnt)
+                    };
+                }
+                else {
+                    testBounds = new GridTuple3[] {
+                        new GridTuple3(
+                            null,
+                            new Object[] {size / 2, null, null},
+                            eqCnt
+                        ),
+                        new GridTuple3(
+                            null,
+                            new Object[] {size / 2 + 1, null, null},
+                            eqCnt
+                        )
+                    };
+                }
+
+                log.info("Check: size=" + size);
+
+                ScanNode<Object[]> scan = new ScanNode<>(ctx, rowType, new TestTable(
+                    size * eqCnt,
+                    rowType,
+                    (rowId) -> rowId / eqCnt,
+                    (rowId) -> "val_" + (rowId % eqCnt),
+                    (rowId) -> rowId % eqCnt
+                ) {
+                    boolean first = true;
+
+                    @Override public @NotNull Iterator<Object[]> iterator() {
+                        assertTrue("Rewind right", first);
+
+                        first = false;
+                        return super.iterator();
+                    }
+                });
+
+                Object[] searchRow = new Object[3];
+                TestPredicate testFilter = new TestPredicate();
+
+                IndexSpoolNode<Object[]> spool = IndexSpoolNode.createHashSpool(
+                    ctx,
+                    rowType,
+                    ImmutableBitSet.of(0),
+                    () -> searchRow
+                );
+
+                spool.register(Arrays.asList(scan));
+
+                RootRewindable<Object[]> root = new RootRewindable<>(ctx, rowType);
+                root.register(spool);
+
+                for (GridTuple3<Predicate<Object[]>, Object[], Integer> bound : testBounds) {
+                    log.info("Check: bound=" + bound);
+
+                    // Set up bounds
+                    testFilter.delegate = bound.get1();
+                    System.arraycopy(bound.get2(), 0, searchRow, 0, searchRow.length);
+
+                    int cnt = 0;
+
+                    while (root.hasNext()) {
+                        root.next();
+
+                        cnt++;
+                    }
+
+                    assertEquals(
+                        "Invalid result size",
+                        (int)bound.get3(),
+                        cnt);
+
+                    root.rewind();
+                }
+
+                root.closeRewindableRoot();
+            }
+        }
+    }
+
+    /** */
+    static class TestPredicate implements Predicate<Object[]> {
+        /** */
+        Predicate<Object[]> delegate;
+
+        /** {@inheritDoc} */
+        @Override public boolean test(Object[] objects) {
+            if (delegate == null)
+                return true;
+            else
+                return delegate.test(objects);
+        }
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolExecutionTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/TreeIndexSpoolExecutionTest.java
similarity index 97%
rename from modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolExecutionTest.java
rename to modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/TreeIndexSpoolExecutionTest.java
index e0b0806..04c2ae7 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/IndexSpoolExecutionTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/TreeIndexSpoolExecutionTest.java
@@ -41,7 +41,7 @@ import org.junit.Test;
  */
 @SuppressWarnings("TypeMayBeWeakened")
 @WithSystemProperty(key = "calcite.debug", value = "true")
-public class IndexSpoolExecutionTest extends AbstractExecutionTest {
+public class TreeIndexSpoolExecutionTest extends AbstractExecutionTest {
     /**
      * @throws Exception If failed.
      */
@@ -132,7 +132,7 @@ public class IndexSpoolExecutionTest extends AbstractExecutionTest {
             Object[] upper = new Object[3];
             TestPredicate testFilter = new TestPredicate();
 
-            IndexSpoolNode<Object[]> spool = new IndexSpoolNode<>(
+            IndexSpoolNode<Object[]> spool = IndexSpoolNode.createTreeSpool(
                 ctx,
                 rowType,
                 RelCollations.of(ImmutableIntList.of(0)),
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 d172fad..03a92bb 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
@@ -264,6 +264,8 @@ public abstract class AbstractPlannerTest extends GridCommonAbstractTest {
             try {
                 IgniteRel res = planner.transform(PlannerPhase.OPTIMIZATION, desired, rel);
 
+//                System.out.println(planner.dump());
+
                 return res;
             }
             catch (Throwable ex) {
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
index 6c1e3bf..e735993 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
@@ -86,9 +86,11 @@ public class CorrelatedNestedLoopJoinPlannerTest extends AbstractPlannerTest {
         IgniteRel phys = physicalPlan(
             sql,
             publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeRule"
+            "MergeJoinConverter", "NestedLoopJoinConverter"
         );
 
+        System.out.println("+++ " + RelOptUtil.toString(phys));
+
         assertNotNull(phys);
 
         checkSplitAndSerialization(phys, publicSchema);
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
similarity index 70%
copy from modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java
copy to modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
index f6d1c37..fa33898 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
@@ -19,15 +19,15 @@ package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.List;
 
+import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexFieldAccess;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
 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.schema.IgniteSchema;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
@@ -38,14 +38,13 @@ import org.junit.Test;
 /**
  *
  */
-@SuppressWarnings({"FieldCanBeLocal"})
-public class IndexSpoolPlannerTest extends AbstractPlannerTest {
+public class HashIndexSpoolPlannerTest extends AbstractPlannerTest {
     /**
      * Check equi-join on not colocated fields.
      * CorrelatedNestedLoopJoinTest is applicable for this case only with IndexSpool.
      */
     @Test
-    public void test() throws Exception {
+    public void testSingleKey() throws Exception {
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
         IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
 
@@ -88,38 +87,28 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
         IgniteRel phys = physicalPlan(
             sql,
             publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter"
+            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeToSortedIndexSpoolRule"
         );
 
-        checkSplitAndSerialization(phys, publicSchema);
-
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
-
-        List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
+        System.out.println("+++\n" + RelOptUtil.toString(phys));
 
-        assertNotNull(lBound);
-        assertEquals(3, lBound.size());
+        checkSplitAndSerialization(phys, publicSchema);
 
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(lBound.get(1) instanceof RexFieldAccess);
+        IgniteHashIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteHashIndexSpool.class));
 
-        List<RexNode> uBound = idxSpool.indexCondition().upperBound();
+        List<RexNode> searchRow = idxSpool.searchRow();
 
-        assertNotNull(uBound);
-        assertEquals(3, uBound.size());
+        assertNotNull(searchRow);
+        assertEquals(3, searchRow.size());
 
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)uBound.get(2)).isNull());
-        assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertTrue(((RexLiteral)searchRow.get(0)).isNull());
+        assertTrue(((RexLiteral)searchRow.get(2)).isNull());
+        assertTrue(searchRow.get(1) instanceof RexFieldAccess);
     }
 
-    /**
-     * Check case when exists index (collation) isn't applied not for whole join condition
-     * but may be used by part of condition.
-     */
+    /** */
     @Test
-    public void testPartialIndexForCondition() throws Exception {
+    public void testMultipleKeys() throws Exception {
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
         IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
 
@@ -163,32 +152,22 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
         IgniteRel phys = physicalPlan(
             sql,
             publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter"
+            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeToSortedIndexSpoolRule"
         );
 
         checkSplitAndSerialization(phys, publicSchema);
 
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
+        IgniteHashIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteHashIndexSpool.class));
 
-        List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
+        List<RexNode> searcRow = idxSpool.searchRow();
 
-        assertNotNull(lBound);
-        assertEquals(4, lBound.size());
+        assertNotNull(searcRow);
+        assertEquals(4, searcRow.size());
 
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(((RexLiteral)lBound.get(3)).isNull());
-        assertTrue(lBound.get(1) instanceof RexFieldAccess);
-
-        List<RexNode> uBound = idxSpool.indexCondition().upperBound();
-
-        assertNotNull(uBound);
-        assertEquals(4, uBound.size());
-
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(((RexLiteral)lBound.get(3)).isNull());
-        assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertTrue(((RexLiteral)searcRow.get(0)).isNull());
+        assertTrue(searcRow.get(1) instanceof RexFieldAccess);
+        assertTrue(searcRow.get(2) instanceof RexFieldAccess);
+        assertTrue(((RexLiteral)searcRow.get(3)).isNull());
     }
 
     /**
@@ -241,26 +220,15 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
 
         checkSplitAndSerialization(phys, publicSchema);
 
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
-
-        assertTrue(idxSpool.getInput() instanceof IgniteSort);
-
-        List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
-
-        assertNotNull(lBound);
-        assertEquals(3, lBound.size());
-
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(lBound.get(1) instanceof RexFieldAccess);
+        IgniteHashIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteHashIndexSpool.class));
 
-        List<RexNode> uBound = idxSpool.indexCondition().upperBound();
+        List<RexNode> searchRow = idxSpool.searchRow();
 
-        assertNotNull(uBound);
-        assertEquals(3, uBound.size());
+        assertNotNull(searchRow);
+        assertEquals(3, searchRow.size());
 
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)uBound.get(2)).isNull());
-        assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertTrue(((RexLiteral)searchRow.get(0)).isNull());
+        assertTrue(((RexLiteral)searchRow.get(2)).isNull());
+        assertTrue(searchRow.get(1) instanceof RexFieldAccess);
     }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
similarity index 69%
rename from modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java
rename to modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
index f6d1c37..f6840b2 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSpoolPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
@@ -19,15 +19,15 @@ package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.List;
 
+import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexFieldAccess;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexSpool;
 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.IgniteSortedIndexSpool;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
@@ -38,14 +38,13 @@ import org.junit.Test;
 /**
  *
  */
-@SuppressWarnings({"FieldCanBeLocal"})
-public class IndexSpoolPlannerTest extends AbstractPlannerTest {
+public class SortedIndexSpoolPlannerTest extends AbstractPlannerTest {
     /**
      * Check equi-join on not colocated fields.
      * CorrelatedNestedLoopJoinTest is applicable for this case only with IndexSpool.
      */
     @Test
-    public void test() throws Exception {
+    public void testNotColocatedEqJoin() throws Exception {
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
         IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
 
@@ -88,12 +87,12 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
         IgniteRel phys = physicalPlan(
             sql,
             publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter"
+            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeToHashIndexSpoolRule"
         );
 
         checkSplitAndSerialization(phys, publicSchema);
 
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
+        IgniteSortedIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteSortedIndexSpool.class));
 
         List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
 
@@ -163,12 +162,14 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
         IgniteRel phys = physicalPlan(
             sql,
             publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter"
+            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeToHashIndexSpoolRule"
         );
 
+        System.out.println("+++ \n" + RelOptUtil.toString(phys));
+
         checkSplitAndSerialization(phys, publicSchema);
 
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
+        IgniteSortedIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteSortedIndexSpool.class));
 
         List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
 
@@ -190,77 +191,4 @@ public class IndexSpoolPlannerTest extends AbstractPlannerTest {
         assertTrue(((RexLiteral)lBound.get(3)).isNull());
         assertTrue(uBound.get(1) instanceof RexFieldAccess);
     }
-
-    /**
-     * Check equi-join on not colocated fields without indexes.
-     */
-    @Test
-    public void testSourceWithoutCollation() throws Exception {
-        IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
-        IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
-        publicSchema.addTable(
-            "T0",
-            new TestTable(
-                new RelDataTypeFactory.Builder(f)
-                    .add("ID", f.createJavaType(Integer.class))
-                    .add("JID", f.createJavaType(Integer.class))
-                    .add("VAL", f.createJavaType(String.class))
-                    .build()) {
-
-                @Override public IgniteDistribution distribution() {
-                    return IgniteDistributions.affinity(0, "T0", "hash");
-                }
-            }
-        );
-
-        publicSchema.addTable(
-            "T1",
-            new TestTable(
-                new RelDataTypeFactory.Builder(f)
-                    .add("ID", f.createJavaType(Integer.class))
-                    .add("JID", f.createJavaType(Integer.class))
-                    .add("VAL", f.createJavaType(String.class))
-                    .build()) {
-
-                @Override public IgniteDistribution distribution() {
-                    return IgniteDistributions.affinity(0, "T1", "hash");
-                }
-            }
-        );
-
-        String sql = "select * " +
-            "from t0 " +
-            "join t1 on t0.jid = t1.jid";
-
-        IgniteRel phys = physicalPlan(
-            sql,
-            publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter"
-        );
-
-        checkSplitAndSerialization(phys, publicSchema);
-
-        IgniteIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteIndexSpool.class));
-
-        assertTrue(idxSpool.getInput() instanceof IgniteSort);
-
-        List<RexNode> lBound = idxSpool.indexCondition().lowerBound();
-
-        assertNotNull(lBound);
-        assertEquals(3, lBound.size());
-
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(lBound.get(1) instanceof RexFieldAccess);
-
-        List<RexNode> uBound = idxSpool.indexCondition().upperBound();
-
-        assertNotNull(uBound);
-        assertEquals(3, uBound.size());
-
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)uBound.get(2)).isNull());
-        assertTrue(uBound.get(1) instanceof RexFieldAccess);
-    }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TableSpoolPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TableSpoolPlannerTest.java
index c472142..a475b10 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TableSpoolPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/TableSpoolPlannerTest.java
@@ -72,7 +72,7 @@ public class TableSpoolPlannerTest extends AbstractPlannerTest {
 
         String sql = "select * " +
             "from t0 " +
-            "join t1 on t0.jid = t1.jid";
+            "join t1 on t0.jid > t1.jid";
 
         RelNode phys = physicalPlan(sql, publicSchema,
             "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeRule");
@@ -127,7 +127,8 @@ public class TableSpoolPlannerTest extends AbstractPlannerTest {
             "join t1 on t0.jid = t1.jid";
 
         RelNode phys = physicalPlan(sql, publicSchema,
-            "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeRule");
+            "MergeJoinConverter", "NestedLoopJoinConverter",
+            "FilterSpoolMergeToHashIndexSpoolRule", "FilterSpoolMergeToSortIndexSpoolRule");
 
         assertNotNull(phys);
 
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/ExecutionTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/ExecutionTestSuite.java
index 2925782..2ab4fcb 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/ExecutionTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/ExecutionTestSuite.java
@@ -21,11 +21,12 @@ import org.apache.ignite.internal.processors.query.calcite.exec.rel.ContinuousEx
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.ExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.HashAggregateExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.HashAggregateSingleGroupExecutionTest;
-import org.apache.ignite.internal.processors.query.calcite.exec.rel.IndexSpoolExecutionTest;
+import org.apache.ignite.internal.processors.query.calcite.exec.rel.HashIndexSpoolExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.MergeJoinExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.NestedLoopJoinExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.SortAggregateExecutionTest;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.TableSpoolExecutionTest;
+import org.apache.ignite.internal.processors.query.calcite.exec.rel.TreeIndexSpoolExecutionTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -39,7 +40,8 @@ import org.junit.runners.Suite;
     MergeJoinExecutionTest.class,
     NestedLoopJoinExecutionTest.class,
     TableSpoolExecutionTest.class,
-    IndexSpoolExecutionTest.class,
+    TreeIndexSpoolExecutionTest.class,
+    HashIndexSpoolExecutionTest.class,
     HashAggregateExecutionTest.class,
     HashAggregateSingleGroupExecutionTest.class,
     SortAggregateExecutionTest.class,
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 50c715f..a6d7767 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
@@ -21,10 +21,11 @@ import org.apache.ignite.internal.processors.query.calcite.planner.AggregateDist
 import org.apache.ignite.internal.processors.query.calcite.planner.AggregatePlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.CorrelatedNestedLoopJoinPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.HashAggregatePlannerTest;
-import org.apache.ignite.internal.processors.query.calcite.planner.IndexSpoolPlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.HashIndexSpoolPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.JoinColocationPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.PlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.SortAggregatePlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.SortedIndexSpoolPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.TableSpoolPlannerTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -37,7 +38,8 @@ import org.junit.runners.Suite;
     PlannerTest.class,
     CorrelatedNestedLoopJoinPlannerTest.class,
     TableSpoolPlannerTest.class,
-    IndexSpoolPlannerTest.class,
+    SortedIndexSpoolPlannerTest.class,
+    HashIndexSpoolPlannerTest.class,
     AggregatePlannerTest.class,
     AggregateDistinctPlannerTest.class,
     HashAggregatePlannerTest.class,