You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by na...@apache.org on 2021/11/09 09:30:55 UTC

[ignite] branch ignite-2.12 updated (92e56ea -> 21dcf47)

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

namelchev pushed a change to branch ignite-2.12
in repository https://gitbox.apache.org/repos/asf/ignite.git.


    from 92e56ea  IGNITE-15807 Java thin: follow user-defined endpoint order, try default port first (#9522)
     new c39c56b  IGNITE-15530 IndexQuery uses MergeSort (#9540)
     new 21dcf47  IGNITE-15745 Add docs for IndexQuery (#9545)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 docs/_data/toc.yaml                                |   4 +-
 docs/_docs/code-deployment/peer-class-loading.adoc |   4 +-
 .../apache/ignite/snippets/UsingScanQueries.java   |  75 +++++
 .../collocated-computations.adoc                   |   2 +-
 docs/_docs/events/events.adoc                      |   6 +-
 ...-scan-queries.adoc => using-cache-queries.adoc} |  60 +++-
 docs/_docs/thin-clients/java-thin-client.adoc      |   6 +-
 .../examples/datagrid/CacheQueryExample.java       |  50 +++-
 .../org/apache/ignite/examples/model/Person.java   |   9 +-
 .../org/apache/ignite/cache/query/IndexQuery.java  |   9 +-
 .../cache/query/index/IndexQueryProcessor.java     | 215 ++++++++------
 .../query/index/IndexQueryResult.java}             |  36 ++-
 .../cache/query/index/IndexQueryResultMeta.java    |  90 ++++++
 .../query/index/sorted/IndexKeyDefinition.java     |  35 ++-
 .../query/index/sorted/IndexKeyTypeSettings.java   |  26 +-
 .../query/index/sorted/IndexRowCompartorImpl.java  |   8 -
 .../query/index/sorted/keys/BooleanIndexKey.java   |   4 +-
 .../query/index/sorted/keys/ByteIndexKey.java      |   4 +-
 .../query/index/sorted/keys/BytesIndexKey.java     |   4 +-
 .../query/index/sorted/keys/DoubleIndexKey.java    |   4 +-
 .../query/index/sorted/keys/FloatIndexKey.java     |   4 +-
 .../query/index/sorted/keys/IntegerIndexKey.java   |   4 +-
 .../query/index/sorted/keys/LongIndexKey.java      |   4 +-
 .../query/index/sorted/keys/ShortIndexKey.java     |   4 +-
 .../index/sorted/keys/SignedBytesIndexKey.java     |   4 +-
 .../GridCacheDistributedFieldsQueryFuture.java     |   4 +-
 .../query/GridCacheDistributedQueryFuture.java     |  31 ++-
 .../query/GridCacheDistributedQueryManager.java    |  30 +-
 .../query/GridCacheLocalFieldsQueryFuture.java     |   4 +-
 .../cache/query/GridCacheLocalQueryManager.java    |   8 +-
 .../cache/query/GridCacheQueryFutureAdapter.java   |  32 ++-
 .../cache/query/GridCacheQueryManager.java         |  73 ++++-
 .../cache/query/GridCacheQueryResponse.java        |  42 ++-
 .../cache/query/reducer/CacheQueryReducer.java     |   7 +-
 .../cache/query/reducer/IndexQueryReducer.java     | 149 ++++++++++
 .../query/reducer/MergeSortCacheQueryReducer.java  |  20 +-
 .../cache/query/reducer/TextQueryReducer.java}     |  31 ++-
 .../processors/query/GridQueryProcessor.java       |   7 +-
 .../processors/query/h2/index/H2RowComparator.java |   7 +-
 .../ignite/cache/query/IndexQueryAliasTest.java    |  30 +-
 .../ignite/cache/query/IndexQueryAllTypesTest.java |  25 +-
 .../ignite/cache/query/IndexQueryFilterTest.java   |  89 ++++--
 .../cache/query/IndexQueryKeepBinaryTest.java      |  52 +++-
 .../cache/query/IndexQueryQueryEntityTest.java     |  19 +-
 .../ignite/cache/query/IndexQueryRangeTest.java    | 246 ++++++----------
 .../ignite/cache/query/IndexQuerySqlIndexTest.java |  35 ++-
 .../ignite/cache/query/MultiTableIndexQuery.java   |   4 +-
 .../cache/query/MultifieldIndexQueryTest.java      | 309 +++++++--------------
 .../cache/query/RepeatedFieldIndexQueryTest.java   |  10 +-
 .../cache/GridCacheFullTextQueryPagesTest.java     |  11 +-
 50 files changed, 1237 insertions(+), 709 deletions(-)
 rename docs/_docs/key-value-api/{using-scan-queries.adoc => using-cache-queries.adoc} (62%)
 copy modules/core/src/main/java/org/apache/ignite/internal/{pagemem/wal/WALIterator.java => cache/query/index/IndexQueryResult.java} (56%)
 create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResultMeta.java
 create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
 copy modules/core/src/main/java/org/apache/ignite/internal/{cluster/NodeOrderLegacyComparator.java => processors/cache/query/reducer/TextQueryReducer.java} (54%)

[ignite] 02/02: IGNITE-15745 Add docs for IndexQuery (#9545)

Posted by na...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

namelchev pushed a commit to branch ignite-2.12
in repository https://gitbox.apache.org/repos/asf/ignite.git

commit 21dcf477459ce9f336eae7fbb288b60ed22bbb75
Author: Maksim Timonin <ti...@gmail.com>
AuthorDate: Tue Nov 9 12:28:35 2021 +0300

    IGNITE-15745 Add docs for IndexQuery (#9545)
    
    (cherry picked from commit 025cc9fbf5553e7fbc81f8e94326a34743ad73fc)
---
 docs/_data/toc.yaml                                |  4 +-
 docs/_docs/code-deployment/peer-class-loading.adoc |  4 +-
 .../apache/ignite/snippets/UsingScanQueries.java   | 75 ++++++++++++++++++++++
 .../collocated-computations.adoc                   |  2 +-
 docs/_docs/events/events.adoc                      |  6 +-
 ...-scan-queries.adoc => using-cache-queries.adoc} | 60 ++++++++++++++++-
 docs/_docs/thin-clients/java-thin-client.adoc      |  6 +-
 .../examples/datagrid/CacheQueryExample.java       | 50 ++++++++++++++-
 .../org/apache/ignite/examples/model/Person.java   |  9 ++-
 .../org/apache/ignite/cache/query/IndexQuery.java  |  9 ++-
 10 files changed, 206 insertions(+), 19 deletions(-)

diff --git a/docs/_data/toc.yaml b/docs/_data/toc.yaml
index a4881c7..e57c9fb 100644
--- a/docs/_data/toc.yaml
+++ b/docs/_data/toc.yaml
@@ -177,8 +177,8 @@
       url: key-value-api/basic-cache-operations
     - title: Working with Binary Objects
       url: key-value-api/binary-objects
-    - title: Using Scan Queries
-      url: key-value-api/using-scan-queries
+    - title: Using Cache Queries
+      url: key-value-api/using-cache-queries
     - title: Read Repair
       url: read-repair
 - title: Performing Transactions
diff --git a/docs/_docs/code-deployment/peer-class-loading.adoc b/docs/_docs/code-deployment/peer-class-loading.adoc
index 0dd7d18..3781faa 100644
--- a/docs/_docs/code-deployment/peer-class-loading.adoc
+++ b/docs/_docs/code-deployment/peer-class-loading.adoc
@@ -28,12 +28,12 @@ If you develop C# and .NET applications, then refer to the link:net-specific/net
 page for details on how to set up and use the peer-class-loading feature with that type of applications.
 ====
 
-For example, when link:key-value-api/using-scan-queries[querying data] with a custom transformer, you only need to define your tasks on the client node that initiates the computation, and Ignite loads the classes to the server nodes.
+For example, when link:key-value-api/using-cache-queries[querying data] with a custom transformer, you just need to define your tasks on the client node that initiates the computation, and Ignite will upload the classes to the server nodes.
 
 When enabled, peer class loading is used to deploy the following classes:
 
 * Tasks and jobs submitted via the link:distributed-computing/distributed-computing[compute interface].
-* Transformers and filters used with link:key-value-api/using-scan-queries[scan queries] and link:key-value-api/continuous-queries[continuous queries].
+* Transformers and filters used with link:key-value-api/using-cache-queries[cache queries] and link:key-value-api/continuous-queries[continuous queries].
 * Stream transformers, receivers and visitors used with link:data-streaming#data-streamers[data streamers].
 * link:distributed-computing/collocated-computations#entry-processor[Entry processors].
 
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/UsingScanQueries.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/UsingScanQueries.java
index 49a9f53..e14668b 100644
--- a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/UsingScanQueries.java
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/UsingScanQueries.java
@@ -16,17 +16,28 @@
  */
 package org.apache.ignite.snippets;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.QueryIndexType;
+import org.apache.ignite.cache.query.IndexQuery;
 import org.apache.ignite.cache.query.QueryCursor;
 import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.lang.IgniteClosure;
 import org.junit.jupiter.api.Test;
 
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.eq;
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gt;
+
 public class UsingScanQueries {
 
     @Test
@@ -84,4 +95,68 @@ public class UsingScanQueries {
             System.out.println("Transformer example output:" + keys.get(0));
         }
     }
+
+    @Test
+    void executingIndexQueriesExample() {
+        try (Ignite ignite = Ignition.start()) {
+            //tag::idxQry[]
+            // Create index by 2 fields (orgId, salary).
+            QueryEntity personEntity = new QueryEntity(Integer.class, Person.class)
+                .setFields(new LinkedHashMap<String, String>() {{
+                    put("orgId", Integer.class.getName());
+                    put("salary", Integer.class.getName());
+                }})
+                .setIndexes(Collections.singletonList(
+                    new QueryIndex(Arrays.asList("orgId", "salary"), QueryIndexType.SORTED)
+                        .setName("ORG_SALARY_IDX")
+                ));
+
+            CacheConfiguration<Integer, Person> ccfg = new CacheConfiguration<Integer, Person>("entityCache")
+                .setQueryEntities(Collections.singletonList(personEntity));
+
+            IgniteCache<Integer, Person> cache = ignite.getOrCreateCache(ccfg);
+
+            //end::idxQry[]
+            {
+            //tag::idxQry[]
+            // Find the persons who work in Organization 1.
+            QueryCursor<Cache.Entry<Integer, Person>> cursor = cache.query(
+                new IndexQuery<Integer, Person>(Person.class, "ORG_SALARY_IDX")
+                    .setCriteria(eq("orgId", 1))
+            );
+            //end::idxQry[]
+            }
+
+            {
+                //tag::idxQryMultipleCriteria[]
+                // Find the persons who work in Organization 1 and have salary more than 1,000.
+                QueryCursor<Cache.Entry<Integer, Person>> cursor = cache.query(
+                    new IndexQuery<Integer, Person>(Person.class, "ORG_SALARY_IDX")
+                        .setCriteria(eq("orgId", 1), gt("salary", 1000))
+                );
+                //end::idxQryMultipleCriteria[]
+            }
+
+            {
+                //tag::idxQryNoIdxName[]
+                // Ignite finds suitable index "ORG_SALARY_IDX" by specified criterion field "orgId".
+                QueryCursor<Cache.Entry<Integer, Person>> cursor = cache.query(
+                    new IndexQuery<Integer, Person>(Person.class)
+                        .setCriteria(eq("orgId", 1))
+                );
+                //end::idxQryNoIdxName[]
+            }
+
+            {
+                //tag::idxQryFilter[]
+                // Find the persons who work in Organization 1 and whose name contains 'Vasya'.
+                QueryCursor<Cache.Entry<Integer, Person>> cursor = cache.query(
+                    new IndexQuery<Integer, Person>(Person.class)
+                        .setCriteria(eq("orgId", 1))
+                        .setFilter((k, v) -> v.getName().contains("Vasya"))
+                );
+                //end::idxQryFilter[]
+            }
+        }
+    }
 }
diff --git a/docs/_docs/distributed-computing/collocated-computations.adoc b/docs/_docs/distributed-computing/collocated-computations.adoc
index 47bd72f..03daeb6 100644
--- a/docs/_docs/distributed-computing/collocated-computations.adoc
+++ b/docs/_docs/distributed-computing/collocated-computations.adoc
@@ -105,7 +105,7 @@ tab:C++[unsupported]
 ====
 [discrete]
 === Performance Considerations
-Colocated computations yield performance benefits when the amount of the data you want to process is sufficiently large. In some cases, when the amount of data is small, a link:key-value-api/using-scan-queries[scan query] may perform better.
+Colocated computations yield performance benefits when the amount of the data you want to process is sufficiently large. In some cases, when the amount of data is small, a link:key-value-api/using-cache-queries[scan query] may perform better.
 
 ====
 
diff --git a/docs/_docs/events/events.adoc b/docs/_docs/events/events.adoc
index 3f7da30..cc08413 100644
--- a/docs/_docs/events/events.adoc
+++ b/docs/_docs/events/events.adoc
@@ -128,9 +128,9 @@ Cache events are also generated when you use DML commands.
 
 | EVT_CACHE_OBJECT_READ
 | An object is read from a cache.
-This event is not emitted when you use link:key-value-api/using-scan-queries[scan queries] (use <<Cache Query Events>> to monitor scan queries).
+This event is not emitted when you use link:key-value-api/using-cache-queries[scan queries] (use <<Cache Query Events>> to monitor scan queries).
 | The node where read operation is executed.
-It can be either the primary or backup node (the latter case is only possible when link:configuring-caches/configuration-overview#readfrombackup[reading from backups is enabled]).
+It can be either the primary or backup node (the latter case is only possible when link:configuring-caches/configuration-overview#readfrombackup[reading from backups] is enabled).
 In transactional caches, the event can be generated on both the primary and backup nodes depending on the concurrency and isolation levels.
 
 | EVT_CACHE_OBJECT_REMOVED | An object is removed from a cache. |The primary and backup nodes for the entry.
@@ -171,7 +171,7 @@ There are two types of events that are related to cache queries:
 [cols="2,5,3",opts="header"]
 |===
 | Event Type | Event Description | Where Event Is Fired
-| EVT_CACHE_QUERY_OBJECT_READ | An object is read as part of a query execution. This event is generated for every object that matches the link:key-value-api/using-scan-queries#executing-scan-queries[query filter]. | The primary node of the object that is read.
+| EVT_CACHE_QUERY_OBJECT_READ | An object is read as part of a query execution. This event is generated for every object that matches the link:key-value-api/using-cache-queries#executing-scan-queries[query filter]. | The primary node of the object that is read.
 | EVT_CACHE_QUERY_EXECUTED  |  This event is generated when a query is executed. | All server nodes that host the cache.
 |===
 
diff --git a/docs/_docs/key-value-api/using-scan-queries.adoc b/docs/_docs/key-value-api/using-cache-queries.adoc
similarity index 62%
rename from docs/_docs/key-value-api/using-scan-queries.adoc
rename to docs/_docs/key-value-api/using-cache-queries.adoc
index 5463e6f..f7f8035 100644
--- a/docs/_docs/key-value-api/using-scan-queries.adoc
+++ b/docs/_docs/key-value-api/using-cache-queries.adoc
@@ -12,7 +12,7 @@
 // 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.
-= Using Scan Queries
+= Using Cache Queries
 
 :javaFile: {javaCodeDir}/UsingScanQueries.java
 :dotnetFile: code-snippets/dotnet/UsingScanQueries.cs
@@ -20,6 +20,7 @@
 
 == Overview
 `IgniteCache` has several query methods, all of which receive a subclass of the `Query` class and return a `QueryCursor`.
+Available types of queries: `ScanQuery`, `IndexQuery`, `TextQuery`.
 
 A `Query` represents an abstract paginated query to be executed on a cache.
 The page size is configurable via the `Query.setPageSize(...)` method (default is 1024).
@@ -118,7 +119,64 @@ include::code-snippets/cpp/src/scan_query.cpp[tag=set-local,indent=0]
 ----
 --
 
+== Executing Index Queries
+
+[WARNING]
+====
+[discrete]
+Experimental API. Introduced since Apache Ignite 2.12. Only Java API is supported. Please send your questions and bug reports
+to user@ignite.apache.org.
+====
+
+Index queries work over distributed indexes and retrieve cache entries that match the specified query. `QueryCursor`
+delivers sorted cache entries by the order defined for queried index. `IndexQuery` can be used if a low amount of data
+matches filtering criteria. For such cases, `ScanQuery` usage is not optimal: it firstly extracts all cache entries and
+then applies a filter to them. `IndexQuery` relies on index tree structure and filters most of the entries without extracting.
+
+[source,java]
+----
+include::{javaFile}[tag=idxQry,indent=0]
+----
+
+Index query criteria are defined in `IndexQueryCriteriaBuilder`. The goal of the criteria is to build a valid range to
+traverse the index tree. For this reason, criteria fields have to match the specified index. For example, if there is an
+index defined with (A, B) set, then valid criteria sets are (A) and (A, B). Criteria with the single (B) field are invalid
+because the field (B) is not a prefix set of the specified index fields, and it's impossible to build a narrow index range
+with it.
+
+[NOTE]
+====
+Criteria are joined by the AND operator. It is also possible to use multiple criteria for the same field.
+====
+
+[source,java]
+----
+include::{javaFile}[tag=idxQryMultipleCriteria,indent=0]
+----
+
+The index name is an optional parameter. In this case, Ignite tries to figure out the index by itself using specified criteria fields.
+
+[source,java]
+----
+include::{javaFile}[tag=idxQryNoIdxName,indent=0]
+----
+
+For the empty criteria list, a full scan of the specified index is performed. If index name is also not specified, then the
+PrimaryKey index is used.
+
+=== Additional filtering
+
+`IndexQuery` also supports an optional predicate, the same as `ScanQuery` has. It's suitable for additional cache entry
+filtering in cases when a filter doesn't match an index tree range. For example, it contains some logic, the "OR"
+operations, or fields that are not the part of the index.
+
+[source,java]
+----
+include::{javaFile}[tag=idxQryFilter,indent=0]
+----
+
 == Related Topics
 
 * link:restapi#sql-scan-query-execute[Execute scan query via REST API]
 * link:events/events#cache-query-events[Cache Query Events]
+* link:SQL/indexes[Defining Indexes]
diff --git a/docs/_docs/thin-clients/java-thin-client.adoc b/docs/_docs/thin-clients/java-thin-client.adoc
index b71d15a..2f71720 100644
--- a/docs/_docs/thin-clients/java-thin-client.adoc
+++ b/docs/_docs/thin-clients/java-thin-client.adoc
@@ -132,9 +132,9 @@ include::{sourceCodeFile}[tag=key-value-operations,indent=0]
 -------------------------------------------------------------------------------
 
 === Executing Scan Queries
-Use the `ScanQuery<K, V>` class to get a set of entries that satisfy a given condition. The thin client sends the query to the cluster node where it is executed as a normal link:key-value-api/using-scan-queries[scan query].
+Use the `ScanQuery<K, V>` class to get a set of entries that satisfy a given condition. The thin client sends the query to the cluster node where it is executed as a regular link:key-value-api/using-cache-queries[scan query].
 
-The query condition is specified by an `IgniteBiPredicate<K, V>` object that is passed to the query constructor as an argument. The predicate is applied on the server side. If you don't provide any predicate, the query returns all cache entries.
+The query condition is specified by an `IgniteBiPredicate<K, V>` object that is passed to the query constructor as an argument. The predicate is applied on the server side. If there is no predicate provided, the query returns all cache entries.
 
 NOTE: The classes of the predicates must be available on the server nodes of the cluster.
 
@@ -379,4 +379,4 @@ include::{sourceCodeFile}[tag=async-api,indent=0]
 
 * Async methods do not block the calling thread
 * Async methods return `IgniteClientFuture<T>` which is a combination of `Future<T>` and `CompletionStage<T>`.
-* Async continuations are executed using `ClientConfiguration.AsyncContinuationExecutor`, which defaults to `ForkJoinPool#commonPool()`. For example, `cache.getAsync(1).thenAccept(val -> System.out.println(val))` will execute the `println` call using a thread from the `commonPool`.
\ No newline at end of file
+* Async continuations are executed using `ClientConfiguration.AsyncContinuationExecutor`, which defaults to `ForkJoinPool#commonPool()`. For example, `cache.getAsync(1).thenAccept(val -> System.out.println(val))` will execute the `println` call using a thread from the `commonPool`.
diff --git a/examples/src/main/java/org/apache/ignite/examples/datagrid/CacheQueryExample.java b/examples/src/main/java/org/apache/ignite/examples/datagrid/CacheQueryExample.java
index c486a5c..0a9838a 100644
--- a/examples/src/main/java/org/apache/ignite/examples/datagrid/CacheQueryExample.java
+++ b/examples/src/main/java/org/apache/ignite/examples/datagrid/CacheQueryExample.java
@@ -24,6 +24,7 @@ import org.apache.ignite.Ignition;
 import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cache.affinity.AffinityKey;
+import org.apache.ignite.cache.query.IndexQuery;
 import org.apache.ignite.cache.query.QueryCursor;
 import org.apache.ignite.cache.query.ScanQuery;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
@@ -34,8 +35,11 @@ import org.apache.ignite.examples.model.Organization;
 import org.apache.ignite.examples.model.Person;
 import org.apache.ignite.lang.IgniteBiPredicate;
 
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.eq;
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gt;
+
 /**
- * Cache queries example. This example demonstrates TEXT and FULL SCAN
+ * Cache queries example. This example demonstrates TEXT, FULL SCAN and INDEX
  * queries over cache.
  * <p>
  * Example also demonstrates usage of fields queries that return only required
@@ -104,6 +108,9 @@ public class CacheQueryExample {
 
                 // Example for TEXT-based querying for a given string in peoples resumes.
                 textQuery();
+
+                // Example for INDEX-based query with index criteria.
+                indexQuery();
             }
             finally {
                 // Distributed cache could be removed from cluster only by Ignite.destroyCache() call.
@@ -153,6 +160,47 @@ public class CacheQueryExample {
     }
 
     /**
+     * Example for query indexes with criteria and binary objects.
+     */
+    private static void indexQuery() {
+        IgniteCache<Long, Person> cache = Ignition.ignite().cache(PERSON_CACHE);
+
+        // Query for all people who work in the organization "ApacheIgnite".
+        QueryCursor<Cache.Entry<Long, Person>> igniters = cache.query(
+            new IndexQuery<Long, Person>(Person.class)
+                .setCriteria(eq("orgId", 1L))
+        );
+
+        print("Following people work in the 'ApacheIgnite' organization (queried with INDEX query): ",
+            igniters.getAll());
+
+        // Query for all people who work in the organization "Other" and have salary more than 1,500.
+        QueryCursor<Cache.Entry<Long, Person>> others = cache.query(
+            new IndexQuery<Long, Person>(Person.class)  // Index name {@link Person#ORG_SALARY_IDX} is optional.
+                .setCriteria(eq("orgId", 2L), gt("salary", 1500.0)));
+
+        print("Following people work in the 'Other' organizations and have salary more than 1500 (queried with INDEX query): ",
+            others.getAll());
+
+        // Query for all people who have salary more than 1,500 using BinaryObject.
+        QueryCursor<Cache.Entry<BinaryObject, BinaryObject>> rich = cache.withKeepBinary().query(
+            new IndexQuery<BinaryObject, BinaryObject>(Person.class.getName())
+                .setCriteria(gt("salary", 1500.0)));
+
+        print("Following people have salary more than 1500 (queried with INDEX query and using binary objects): ",
+            rich.getAll());
+
+        // Query for all people who have salary more than 1,500 and have 'Master Degree' in their resumes.
+        QueryCursor<Cache.Entry<BinaryObject, BinaryObject>> richMasters = cache.withKeepBinary().query(
+            new IndexQuery<BinaryObject, BinaryObject>(Person.class.getName())
+                .setCriteria(gt("salary", 1500.0))
+                .setFilter((k, v) -> v.<String>field("resume").contains("Master")));
+
+        print("Following people have salary more than 1500 and Master degree (queried with INDEX query): ",
+            richMasters.getAll());
+    }
+
+    /**
      * Populate cache with test data.
      */
     private static void initialize() {
diff --git a/examples/src/main/java/org/apache/ignite/examples/model/Person.java b/examples/src/main/java/org/apache/ignite/examples/model/Person.java
index 6d3a6df..29a2d9d 100644
--- a/examples/src/main/java/org/apache/ignite/examples/model/Person.java
+++ b/examples/src/main/java/org/apache/ignite/examples/model/Person.java
@@ -30,12 +30,15 @@ public class Person implements Serializable {
     /** */
     private static final AtomicLong ID_GEN = new AtomicLong();
 
+    /** Name of index by two fields (orgId, salary). */
+    public static final String ORG_SALARY_IDX = "ORG_SALARY_IDX";
+
     /** Person ID (indexed). */
     @QuerySqlField(index = true)
     public Long id;
 
     /** Organization ID (indexed). */
-    @QuerySqlField(index = true)
+    @QuerySqlField(index = true, orderedGroups = @QuerySqlField.Group(name = ORG_SALARY_IDX, order = 0))
     public Long orgId;
 
     /** First name (not-indexed). */
@@ -51,10 +54,10 @@ public class Person implements Serializable {
     public String resume;
 
     /** Salary (indexed). */
-    @QuerySqlField(index = true)
+    @QuerySqlField(index = true, orderedGroups = @QuerySqlField.Group(name = ORG_SALARY_IDX, order = 1))
     public double salary;
 
-    /** Custom cache key to guarantee that person is always collocated with its organization. */
+    /** Custom cache key to guarantee that person is always colocated with its organization. */
     private transient AffinityKey<Long> key;
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/cache/query/IndexQuery.java b/modules/core/src/main/java/org/apache/ignite/cache/query/IndexQuery.java
index 25f7807..1c059eb 100644
--- a/modules/core/src/main/java/org/apache/ignite/cache/query/IndexQuery.java
+++ b/modules/core/src/main/java/org/apache/ignite/cache/query/IndexQuery.java
@@ -29,12 +29,15 @@ import org.apache.ignite.lang.IgniteExperimental;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Index query runs over internal index structure and returns cache entries for index rows.
+ * Index queries work over distributed indexes and retrieve cache entries that match the specified criteria.
+ * {@code QueryCursor} delivers sorted cache entries by the order defined for queried index.
  *
- * {@code IndexQuery} has to be initialized with cache value class or type. The algorithm of discovering index is as following:
+ * {@code IndexQuery} has to be initialized with cache value class or type. The algorithm of discovering index is as follows:
  * 1. If {@link #idxName} is set, then use it.
  * 2. If {@link #idxName} is not set, then find an index that matches criteria fields.
- * 3. If neither of {@link #idxName} or {@link #setCriteria(List)} used, then perform index scan over PK index for specified Value type.
+ * 3. If neither {@link #idxName}, nor {@link #setCriteria(List)} is used, then perform index scan over PK index for specified Value type.
+ *
+ * Conjuction of items in {@link #criteria} has to represent a valid range to traverse the index tree.
  */
 @IgniteExperimental
 public final class IndexQuery<K, V> extends Query<Cache.Entry<K, V>> {

[ignite] 01/02: IGNITE-15530 IndexQuery uses MergeSort (#9540)

Posted by na...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

namelchev pushed a commit to branch ignite-2.12
in repository https://gitbox.apache.org/repos/asf/ignite.git

commit c39c56bad99c95166446191fe4c32866b5ad007e
Author: Maksim Timonin <ti...@gmail.com>
AuthorDate: Tue Nov 9 12:02:35 2021 +0300

    IGNITE-15530 IndexQuery uses MergeSort (#9540)
    
    (cherry picked from commit 19ac10dc49ca56b549b0cc728959ba49334d1ae9)
---
 .../cache/query/index/IndexQueryProcessor.java     | 215 ++++++++------
 .../BooleanIndexKey.java => IndexQueryResult.java} |  40 ++-
 .../cache/query/index/IndexQueryResultMeta.java    |  90 ++++++
 .../query/index/sorted/IndexKeyDefinition.java     |  35 ++-
 .../query/index/sorted/IndexKeyTypeSettings.java   |  26 +-
 .../query/index/sorted/IndexRowCompartorImpl.java  |   8 -
 .../query/index/sorted/keys/BooleanIndexKey.java   |   4 +-
 .../query/index/sorted/keys/ByteIndexKey.java      |   4 +-
 .../query/index/sorted/keys/BytesIndexKey.java     |   4 +-
 .../query/index/sorted/keys/DoubleIndexKey.java    |   4 +-
 .../query/index/sorted/keys/FloatIndexKey.java     |   4 +-
 .../query/index/sorted/keys/IntegerIndexKey.java   |   4 +-
 .../query/index/sorted/keys/LongIndexKey.java      |   4 +-
 .../query/index/sorted/keys/ShortIndexKey.java     |   4 +-
 .../index/sorted/keys/SignedBytesIndexKey.java     |   4 +-
 .../GridCacheDistributedFieldsQueryFuture.java     |   4 +-
 .../query/GridCacheDistributedQueryFuture.java     |  31 ++-
 .../query/GridCacheDistributedQueryManager.java    |  30 +-
 .../query/GridCacheLocalFieldsQueryFuture.java     |   4 +-
 .../cache/query/GridCacheLocalQueryManager.java    |   8 +-
 .../cache/query/GridCacheQueryFutureAdapter.java   |  32 ++-
 .../cache/query/GridCacheQueryManager.java         |  73 ++++-
 .../cache/query/GridCacheQueryResponse.java        |  42 ++-
 .../cache/query/reducer/CacheQueryReducer.java     |   7 +-
 .../cache/query/reducer/IndexQueryReducer.java     | 149 ++++++++++
 .../query/reducer/MergeSortCacheQueryReducer.java  |  20 +-
 .../cache/query/reducer/TextQueryReducer.java      |  47 ++++
 .../processors/query/GridQueryProcessor.java       |   7 +-
 .../processors/query/h2/index/H2RowComparator.java |   7 +-
 .../ignite/cache/query/IndexQueryAliasTest.java    |  30 +-
 .../ignite/cache/query/IndexQueryAllTypesTest.java |  25 +-
 .../ignite/cache/query/IndexQueryFilterTest.java   |  89 ++++--
 .../cache/query/IndexQueryKeepBinaryTest.java      |  52 +++-
 .../cache/query/IndexQueryQueryEntityTest.java     |  19 +-
 .../ignite/cache/query/IndexQueryRangeTest.java    | 246 ++++++----------
 .../ignite/cache/query/IndexQuerySqlIndexTest.java |  35 ++-
 .../ignite/cache/query/MultiTableIndexQuery.java   |   4 +-
 .../cache/query/MultifieldIndexQueryTest.java      | 309 +++++++--------------
 .../cache/query/RepeatedFieldIndexQueryTest.java   |  10 +-
 .../cache/GridCacheFullTextQueryPagesTest.java     |  11 +-
 40 files changed, 1057 insertions(+), 684 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
index bd9496b..a88a45d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryProcessor.java
@@ -21,6 +21,8 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
@@ -38,17 +40,18 @@ import org.apache.ignite.internal.cache.query.index.sorted.IndexRowComparator;
 import org.apache.ignite.internal.cache.query.index.sorted.IndexSearchRowImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandler;
 import org.apache.ignite.internal.cache.query.index.sorted.SortedIndexDefinition;
+import org.apache.ignite.internal.cache.query.index.sorted.SortedSegmentedIndex;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexImpl;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKeyFactory;
+import org.apache.ignite.internal.processors.cache.CacheObject;
 import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.cache.CacheObjectUtils;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.query.IndexQueryDesc;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.GridCloseableIteratorAdapter;
-import org.apache.ignite.internal.util.lang.GridCloseableIterator;
 import org.apache.ignite.internal.util.lang.GridCursor;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.T2;
@@ -70,20 +73,30 @@ public class IndexQueryProcessor {
         this.idxProc = idxProc;
     }
 
-    /** Run query on local node. */
-    public <K, V> GridCloseableIterator<IgniteBiTuple<K, V>> queryLocal(
+    /**
+     * Run query on local node.
+     *
+     * @return Query result that contains data iterator and related metadata.
+     */
+    public <K, V> IndexQueryResult<K, V> queryLocal(
         GridCacheContext<K, V> cctx,
         IndexQueryDesc idxQryDesc,
         @Nullable IgniteBiPredicate<K, V> filter,
         IndexQueryContext qryCtx,
         boolean keepBinary
     ) throws IgniteCheckedException {
-        Index idx = index(cctx, idxQryDesc);
+        SortedSegmentedIndex idx = findSortedIndex(cctx, idxQryDesc);
+
+        IndexRangeQuery qry = prepareQuery(idx, idxQryDesc);
 
-        GridCursor<IndexRow> cursor = query(cctx, idx, idxQryDesc, qryCtx);
+        GridCursor<IndexRow> cursor = querySortedIndex(cctx, idx, qryCtx, qry);
+
+        SortedIndexDefinition def = (SortedIndexDefinition)idxProc.indexDefinition(idx.id());
+
+        IndexQueryResultMeta meta = new IndexQueryResultMeta(def, qry.criteria.length);
 
         // Map IndexRow to Cache Key-Value pair.
-        return new GridCloseableIteratorAdapter<IgniteBiTuple<K, V>>() {
+        return new IndexQueryResult<>(meta, new GridCloseableIteratorAdapter<IgniteBiTuple<K, V>>() {
             private IgniteBiTuple<K, V> currVal;
 
             private final CacheObjectContext coctx = cctx.cacheObjectContext();
@@ -96,11 +109,16 @@ public class IndexQueryProcessor {
                 while (currVal == null && cursor.next()) {
                     IndexRow r = cursor.get();
 
-                    K k = (K)CacheObjectUtils.unwrapBinaryIfNeeded(coctx, r.cacheDataRow().key(), keepBinary, false);
-                    V v = (V)CacheObjectUtils.unwrapBinaryIfNeeded(coctx, r.cacheDataRow().value(), keepBinary, false);
+                    K k = unwrap(r.cacheDataRow().key(), true);
+                    V v = unwrap(r.cacheDataRow().value(), true);
 
-                    if (filter != null && !filter.apply(k, v))
-                        continue;
+                    if (filter != null) {
+                        K k0 = keepBinary ? k : unwrap(r.cacheDataRow().key(), false);
+                        V v0 = keepBinary ? v : unwrap(r.cacheDataRow().value(), false);
+
+                        if (!filter.apply(k0, v0))
+                            continue;
+                    }
 
                     currVal = new IgniteBiTuple<>(k, v);
                 }
@@ -120,16 +138,21 @@ public class IndexQueryProcessor {
 
                 return row;
             }
-        };
+
+            /** */
+            private <T> T unwrap(CacheObject o, boolean keepBinary) {
+                return (T)CacheObjectUtils.unwrapBinaryIfNeeded(coctx, o, keepBinary, false);
+            }
+        });
     }
 
     /**
-     * Finds index to run query and validates that criteria fields match a prefix of fields.
+     * Finds sorted index to run query by specified description.
      *
      * @return Index to run query by specified description.
      * @throws IgniteCheckedException If index not found.
      */
-    private Index index(GridCacheContext<?, ?> cctx, IndexQueryDesc idxQryDesc) throws IgniteCheckedException {
+    private SortedSegmentedIndex findSortedIndex(GridCacheContext<?, ?> cctx, IndexQueryDesc idxQryDesc) throws IgniteCheckedException {
         final String tableName = cctx.kernalContext().query().tableName(cctx.name(), idxQryDesc.valType());
 
         if (tableName == null)
@@ -158,56 +181,38 @@ public class IndexQueryProcessor {
             return indexByCriteria(cctx, critFlds, tableName, idxQryDesc);
 
         // If index name isn't specified and criteria aren't set then use the PK index.
-        String idxName = idxQryDesc.idxName() == null ? QueryUtils.PRIMARY_KEY_INDEX : idxQryDesc.idxName();
+        String name = idxQryDesc.idxName() == null ? QueryUtils.PRIMARY_KEY_INDEX : idxQryDesc.idxName();
 
-        return indexByName(cctx, idxName, tableName, idxQryDesc, critFlds);
+        IndexName idxName = new IndexName(cctx.name(), cctx.kernalContext().query().schemaName(cctx), tableName, name);
+
+        return indexByName(idxName, idxQryDesc, critFlds);
     }
 
     /**
-     * @return Index found by name.
-     * @throws IgniteCheckedException If index not found.
+     * @return Sorted index found by name.
+     * @throws IgniteCheckedException If index not found or specified index doesn't match query criteria.
      */
-    private Index indexByName(
-        GridCacheContext<?, ?> cctx,
-        String idxName,
-        String tableName,
+    private SortedSegmentedIndex indexByName(
+        IndexName idxName,
         IndexQueryDesc idxQryDesc,
         final Map<String, String> criteriaFlds
     ) throws IgniteCheckedException {
-        String schema = cctx.kernalContext().query().schemaName(cctx);
-
-        IndexName name = new IndexName(cctx.name(), schema, tableName, idxName);
-
-        Index idx = getAndValidateIndex(name, idxQryDesc, criteriaFlds, false);
-
-        if (idx != null)
-            return idx;
+        SortedSegmentedIndex idx = assertSortedIndex(idxProc.index(idxName), idxQryDesc);
 
-        String normIdxName = idxName;
+        if (idx == null && !QueryUtils.PRIMARY_KEY_INDEX.equals(idxName.idxName())) {
+            String normIdxName = QueryUtils.normalizeObjectName(idxName.idxName(), false);
 
-        if (!QueryUtils.PRIMARY_KEY_INDEX.equals(idxName))
-            normIdxName = QueryUtils.normalizeObjectName(idxName, false);
+            idxName = new IndexName(idxName.cacheName(), idxName.schemaName(), idxName.tableName(), normIdxName);
 
-        name = new IndexName(cctx.name(), schema, tableName, normIdxName);
+            idx = assertSortedIndex(idxProc.index(idxName), idxQryDesc);
+        }
 
-        return getAndValidateIndex(name, idxQryDesc, criteriaFlds, true);
-    }
+        if (idx == null)
+            throw failIndexQuery("No index found for name: " + idxName.idxName(), null, idxQryDesc);
 
-    /** */
-    private @Nullable Index getAndValidateIndex(
-        IndexName name,
-        IndexQueryDesc idxQryDesc,
-        final Map<String, String> critFlds,
-        boolean failOnNotFound
-    ) throws IgniteCheckedException {
-        Index idx = idxProc.index(name);
-
-        if (idx != null && !critFlds.isEmpty() && !checkIndex(idxProc.indexDefinition(idx.id()), critFlds))
+        if (!checkIndex(idx, idxName.tableName(), criteriaFlds))
             throw failIndexQuery("Index doesn't match criteria", null, idxQryDesc);
 
-        if (idx == null && failOnNotFound)
-            throw failIndexQuery("No index found for name: " + name.idxName(), null, idxQryDesc);
-
         return idx;
     }
 
@@ -215,7 +220,7 @@ public class IndexQueryProcessor {
      * @return Index found by list of criteria fields.
      * @throws IgniteCheckedException if suitable index not found.
      */
-    private Index indexByCriteria(
+    private SortedSegmentedIndex indexByCriteria(
         GridCacheContext<?, ?> cctx,
         final Map<String, String> criteriaFlds,
         String tableName,
@@ -224,24 +229,40 @@ public class IndexQueryProcessor {
         Collection<Index> idxs = idxProc.indexes(cctx);
 
         for (Index idx: idxs) {
-            IndexDefinition idxDef = idxProc.indexDefinition(idx.id());
-
-            if (!tableName.equals(idxDef.idxName().tableName()))
-                continue;
+            SortedSegmentedIndex sortedIdx = assertSortedIndex(idx, idxQryDesc);
 
-            if (checkIndex(idxDef, criteriaFlds))
-                return idx;
+            if (checkIndex(sortedIdx, tableName, criteriaFlds))
+                return sortedIdx;
         }
 
         throw failIndexQuery("No index found for criteria", null, idxQryDesc);
     }
 
+    /** Assert if specified index is not an instance of {@link SortedSegmentedIndex}. */
+    private SortedSegmentedIndex assertSortedIndex(Index idx, IndexQueryDesc idxQryDesc) throws IgniteCheckedException {
+        if (idx == null)
+            return null;
+
+        if (!(idx instanceof SortedSegmentedIndex))
+            throw failIndexQuery("IndexQuery is not supported for index: " + idx.name(), null, idxQryDesc);
+
+        return (SortedSegmentedIndex)idx;
+    }
+
     /**
-     * Checks that specified index matches index query criteria.
+     * Checks that specified sorted index matches index query criteria.
      *
      * Criteria fields have to match to a prefix of the index. Order of fields in criteria doesn't matter.
      */
-    private boolean checkIndex(IndexDefinition idxDef, Map<String, String> criteriaFlds) {
+    private boolean checkIndex(SortedSegmentedIndex idx, String tblName, Map<String, String> criteriaFlds) {
+        IndexDefinition idxDef = idxProc.indexDefinition(idx.id());
+
+        if (!tblName.equals(idxDef.idxName().tableName()))
+            return false;
+
+        if (F.isEmpty(criteriaFlds))
+            return true;
+
         Map<String, String> flds = new HashMap<>(criteriaFlds);
 
         for (String idxFldName: idxDef.indexKeyDefinitions().keySet()) {
@@ -430,40 +451,35 @@ public class IndexQueryProcessor {
     }
 
     /**
-     * Runs an index query.
+     * Prepare index query.
      *
-     * @return Result cursor over index segments.
+     * @return Prepared query for index range.
      */
-    private GridCursor<IndexRow> query(GridCacheContext<?, ?> cctx, Index idx, IndexQueryDesc idxQryDesc, IndexQueryContext qryCtx)
-        throws IgniteCheckedException {
+    private IndexRangeQuery prepareQuery(SortedSegmentedIndex idx, IndexQueryDesc idxQryDesc) throws IgniteCheckedException {
+        SortedIndexDefinition idxDef = (SortedIndexDefinition) idxProc.indexDefinition(idx.id());
 
-        IndexQueryCriterion c = F.isEmpty(idxQryDesc.criteria()) ? null : idxQryDesc.criteria().get(0);
+        // For PK indexes will serialize _KEY column.
+        if (F.isEmpty(idxQryDesc.criteria()))
+            return new IndexRangeQuery(1);
 
-        if (c == null || c instanceof RangeIndexQueryCriterion)
-            return querySortedIndex(cctx, (InlineIndexImpl) idx, idxQryDesc, qryCtx);
+        InlineIndexImpl sortedIdx = (InlineIndexImpl)idx;
 
-        throw new IllegalStateException("Doesn't support index query criteria: " + c.getClass().getName());
+        Map<String, RangeIndexQueryCriterion> merged = mergeIndexQueryCriteria(sortedIdx, idxDef, idxQryDesc);
+
+        return alignCriteriaWithIndex(sortedIdx, merged, idxDef);
     }
 
     /**
-     * Runs an index query for single {@code segment}.
+     * Runs an index query.
      *
-     * @return Result cursor over segment.
+     * @return Result cursor.
      */
-    private GridCursor<IndexRow> querySortedIndex(GridCacheContext<?, ?> cctx, InlineIndexImpl idx, IndexQueryDesc idxQryDesc,
-        IndexQueryContext qryCtx) throws IgniteCheckedException {
-        SortedIndexDefinition idxDef = (SortedIndexDefinition) idxProc.indexDefinition(idx.id());
-
-        IndexRangeQuery qry;
-
-        if (!F.isEmpty(idxQryDesc.criteria())) {
-            Map<String, RangeIndexQueryCriterion> merged = mergeIndexQueryCriteria(idx, idxDef, idxQryDesc);
-
-            qry = alignCriteriaWithIndex(idx, merged, idxDef);
-        }
-        else
-            qry = new IndexRangeQuery(0);
-
+    private GridCursor<IndexRow> querySortedIndex(
+        GridCacheContext<?, ?> cctx,
+        SortedSegmentedIndex idx,
+        IndexQueryContext qryCtx,
+        IndexRangeQuery qry
+    ) throws IgniteCheckedException {
         int segmentsCnt = cctx.isPartitioned() ? cctx.config().getQueryParallelism() : 1;
 
         if (segmentsCnt == 1)
@@ -475,8 +491,7 @@ public class IndexQueryProcessor {
         for (int i = 0; i < segmentsCnt; i++)
             segmentCursors[i] = treeIndexRange(idx, i, qry, qryCtx);
 
-        return new SegmentedIndexCursor(
-            segmentCursors, ((SortedIndexDefinition) idxProc.indexDefinition(idx.id())).rowComparator());
+        return new SegmentedIndexCursor(segmentCursors, (SortedIndexDefinition)idxProc.indexDefinition(idx.id()));
     }
 
     /**
@@ -489,10 +504,10 @@ public class IndexQueryProcessor {
      * 2. To apply criteria on non-first index fields. Tree apply boundaries field by field, if first field match
      * a boundary, then second field isn't checked within traversing.
      */
-    private GridCursor<IndexRow> treeIndexRange(InlineIndexImpl idx, int segment, IndexRangeQuery qry, IndexQueryContext qryCtx)
+    private GridCursor<IndexRow> treeIndexRange(SortedSegmentedIndex idx, int segment, IndexRangeQuery qry, IndexQueryContext qryCtx)
         throws IgniteCheckedException {
 
-        InlineIndexRowHandler hnd = idx.segment(segment).rowHandler();
+        LinkedHashMap<String, IndexKeyDefinition> idxDef = idxProc.indexDefinition(idx.id()).indexKeyDefinitions();
 
         // Step 1. Traverse index.
         GridCursor<IndexRow> findRes = idx.find(qry.lower, qry.upper, segment, qryCtx);
@@ -534,7 +549,7 @@ public class IndexQueryProcessor {
                 for (int i = 0; i < criteriaKeysCnt; i++) {
                     RangeIndexQueryCriterion c = qry.criteria[i];
 
-                    boolean descOrder = hnd.indexKeyDefinitions().get(i).order().sortOrder() == DESC;
+                    boolean descOrder = idxDef.get(c.field()).order().sortOrder() == DESC;
 
                     if (low != null && low.key(i) != null) {
                         int cmp = rowCmp.compareRow(row, low, i);
@@ -576,7 +591,7 @@ public class IndexQueryProcessor {
         return key;
     }
 
-    /** Single cursor over multiple segments. Next value is choose with the index row comparator. */
+    /** Single cursor over multiple segments. The next value is chosen with the index row comparator. */
     private static class SegmentedIndexCursor implements GridCursor<IndexRow> {
         /** Cursors over segments. */
         private final PriorityQueue<GridCursor<IndexRow>> cursors;
@@ -588,14 +603,30 @@ public class IndexQueryProcessor {
         private IndexRow head;
 
         /** */
-        SegmentedIndexCursor(GridCursor<IndexRow>[] cursors, IndexRowComparator rowCmp) throws IgniteCheckedException {
+        SegmentedIndexCursor(GridCursor<IndexRow>[] cursors, SortedIndexDefinition idxDef) throws IgniteCheckedException {
             cursorComp = new Comparator<GridCursor<IndexRow>>() {
                 @Override public int compare(GridCursor<IndexRow> o1, GridCursor<IndexRow> o2) {
                     try {
-                        return rowCmp.compareRow(o1.get(), o2.get(), 0);
-                    }
-                    catch (IgniteCheckedException e) {
-                        throw new IgniteException(e);
+                        int keysLen = o1.get().keys().length;
+
+                        Iterator<IndexKeyDefinition> it = idxDef.indexKeyDefinitions().values().iterator();
+
+                        for (int i = 0; i < keysLen; i++) {
+                            int cmp = idxDef.rowComparator().compareRow(o1.get(), o2.get(), i);
+
+                            IndexKeyDefinition def = it.next();
+
+                            if (cmp != 0) {
+                                boolean desc = def.order().sortOrder() == SortOrder.DESC;
+
+                                return desc ? -cmp : cmp;
+                            }
+                        }
+
+                        return 0;
+
+                    } catch (IgniteCheckedException e) {
+                        throw new IgniteException("Failed to sort remote index rows", e);
                     }
                 }
             };
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResult.java
similarity index 52%
copy from modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java
copy to modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResult.java
index 28bc04d..9b9d2df 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResult.java
@@ -15,34 +15,32 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cache.query.index.sorted.keys;
+package org.apache.ignite.internal.cache.query.index;
 
-import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypes;
+import org.apache.ignite.internal.util.lang.GridCloseableIterator;
+import org.apache.ignite.lang.IgniteBiTuple;
 
-/** */
-public class BooleanIndexKey implements IndexKey {
-    /** */
-    private final boolean key;
+/** Represents result of local execution of IndexQuery. */
+public class IndexQueryResult<K, V> {
+    /** Result data iterator. */
+    private final GridCloseableIterator<IgniteBiTuple<K, V>> iter;
 
-    /** */
-    public BooleanIndexKey(boolean key) {
-        this.key = key;
-    }
+    /** Result metadata. */
+    private final IndexQueryResultMeta metadata;
 
-    /** {@inheritDoc} */
-    @Override public Object key() {
-        return key;
+    /** */
+    public IndexQueryResult(IndexQueryResultMeta metadata, GridCloseableIterator<IgniteBiTuple<K, V>> iter) {
+        this.iter = iter;
+        this.metadata = metadata;
     }
 
-    /** {@inheritDoc} */
-    @Override public int type() {
-        return IndexKeyTypes.BOOLEAN;
+    /** */
+    public GridCloseableIterator<IgniteBiTuple<K, V>> iter() {
+        return iter;
     }
 
-    /** {@inheritDoc} */
-    @Override public int compare(IndexKey o) {
-        boolean okey = (boolean) o.key();
-
-        return Boolean.compare(key, okey);
+    /** */
+    public IndexQueryResultMeta metadata() {
+        return metadata;
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResultMeta.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResultMeta.java
new file mode 100644
index 0000000..96e5ff7
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/IndexQueryResultMeta.java
@@ -0,0 +1,90 @@
+/*
+ * 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.cache.query.index;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings;
+import org.apache.ignite.internal.cache.query.index.sorted.MetaPageInfo;
+import org.apache.ignite.internal.cache.query.index.sorted.SortedIndexDefinition;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Metadata for IndexQuery response. This information is required to be sent to a node that initiated a query.
+ * Thick client nodes may have irrelevant information about index structure, {@link MetaPageInfo}.
+ */
+public class IndexQueryResultMeta implements Externalizable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Index key settings. */
+    private IndexKeyTypeSettings keyTypeSettings;
+
+    /** Index key definitions. */
+    private LinkedHashMap<String, IndexKeyDefinition> keyDefs;
+
+    /** */
+    public IndexQueryResultMeta() {
+        // No-op.
+    }
+
+    /** */
+    public IndexQueryResultMeta(SortedIndexDefinition def, int critSize) {
+        keyTypeSettings = def.keyTypeSettings();
+
+        keyDefs = new LinkedHashMap<>();
+
+        Iterator<Map.Entry<String, IndexKeyDefinition>> keys = def.indexKeyDefinitions().entrySet().iterator();
+
+        for (int i = 0; i < critSize; i++) {
+            Map.Entry<String, IndexKeyDefinition> key = keys.next();
+
+            keyDefs.put(key.getKey(), key.getValue());
+        }
+    }
+
+    /** */
+    public IndexKeyTypeSettings keyTypeSettings() {
+        return keyTypeSettings;
+    }
+
+    /** */
+    public LinkedHashMap<String, IndexKeyDefinition> keyDefinitions() {
+        return keyDefs;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeObject(keyTypeSettings);
+
+        U.writeMap(out, keyDefs);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        keyTypeSettings = (IndexKeyTypeSettings)in.readObject();
+
+        keyDefs = U.readLinkedMap(in);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
index c0e51db..e1699de 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyDefinition.java
@@ -17,22 +17,36 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import org.apache.ignite.internal.cache.query.index.Order;
+import org.apache.ignite.internal.cache.query.index.SortOrder;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
 import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
+import org.apache.ignite.internal.util.typedef.internal.U;
 
 /**
  * Defines a signle index key.
  */
-public class IndexKeyDefinition {
+public class IndexKeyDefinition implements Externalizable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
     /** Index key type. {@link IndexKeyTypes}. */
-    private final int idxType;
+    private int idxType;
 
     /** Order. */
-    private final Order order;
+    private Order order;
 
     /** Precision for variable length key types. */
-    private final int precision;
+    private int precision;
+
+    /** */
+    public IndexKeyDefinition() {
+        // No-op.
+    }
 
     /** */
     public IndexKeyDefinition(int idxType, Order order, long precision) {
@@ -70,4 +84,17 @@ public class IndexKeyDefinition {
 
         return idxType == key.type();
     }
+
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        // Send only required info for using in MergeSort algorithm.
+        out.writeInt(idxType);
+        U.writeEnum(out, order.sortOrder());
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        idxType = in.readInt();
+        order = new Order(U.readEnum(in, SortOrder.class), null);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyTypeSettings.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyTypeSettings.java
index 09e2092..095974a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyTypeSettings.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexKeyTypeSettings.java
@@ -17,10 +17,18 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
 /**
  * List of settings that affects key types of index keys.
  */
-public class IndexKeyTypeSettings {
+public class IndexKeyTypeSettings implements Externalizable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
     /** Whether inlining POJO keys as hash is supported. */
     private boolean inlineObjHash = true;
 
@@ -78,4 +86,20 @@ public class IndexKeyTypeSettings {
 
         return this;
     }
+
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeBoolean(inlineObjHash);
+        out.writeBoolean(inlineObjSupported);
+        out.writeBoolean(strOptimizedCompare);
+        out.writeBoolean(binaryUnsigned);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        inlineObjHash = in.readBoolean();
+        inlineObjSupported = in.readBoolean();
+        strOptimizedCompare = in.readBoolean();
+        binaryUnsigned = in.readBoolean();
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
index 916fe25..f42a9ce 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/IndexRowCompartorImpl.java
@@ -33,14 +33,6 @@ import static org.apache.ignite.internal.cache.query.index.sorted.inline.types.N
  * 2. Comparison of different types is not supported.
  */
 public class IndexRowCompartorImpl implements IndexRowComparator {
-    /** Key type settings for this index. */
-    protected final IndexKeyTypeSettings keyTypeSettings;
-
-    /** */
-    public IndexRowCompartorImpl(IndexKeyTypeSettings keyTypeSettings) {
-        this.keyTypeSettings = keyTypeSettings;
-    }
-
     /** {@inheritDoc} */
     @Override public int compareKey(long pageAddr, int off, int maxSize, IndexKey key, int curType) {
         if (curType == IndexKeyTypes.UNKNOWN)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java
index 28bc04d..75fcbd3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BooleanIndexKey.java
@@ -41,8 +41,6 @@ public class BooleanIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        boolean okey = (boolean) o.key();
-
-        return Boolean.compare(key, okey);
+        return Boolean.compare(key, ((BooleanIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ByteIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ByteIndexKey.java
index cd1842b..aed895f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ByteIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ByteIndexKey.java
@@ -41,8 +41,6 @@ public class ByteIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        byte okey = (byte) o.key();
-
-        return Byte.compare(key, okey);
+        return Byte.compare(key, ((ByteIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
index 8ebf857..ed9a2db 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/BytesIndexKey.java
@@ -41,8 +41,6 @@ public class BytesIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        byte[] okey = (byte[]) o.key();
-
-        return BytesCompareUtils.compareNotNullUnsigned(key, okey);
+        return BytesCompareUtils.compareNotNullUnsigned(key, ((BytesIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/DoubleIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/DoubleIndexKey.java
index 9967189d..69d2a9a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/DoubleIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/DoubleIndexKey.java
@@ -41,8 +41,6 @@ public class DoubleIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        double okey = (double) o.key();
-
-        return Double.compare(key, okey);
+        return Double.compare(key, ((DoubleIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/FloatIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/FloatIndexKey.java
index 32986a6..4384e98 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/FloatIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/FloatIndexKey.java
@@ -41,8 +41,6 @@ public class FloatIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        float okey = (float) o.key();
-
-        return Float.compare(key, okey);
+        return Float.compare(key, ((FloatIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/IntegerIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/IntegerIndexKey.java
index 9f7acf9..0164119 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/IntegerIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/IntegerIndexKey.java
@@ -41,8 +41,6 @@ public class IntegerIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        int okey = (int) o.key();
-
-        return Integer.compare(key, okey);
+        return Integer.compare(key, ((IntegerIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/LongIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/LongIndexKey.java
index 1a5a4e9..59c13d1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/LongIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/LongIndexKey.java
@@ -41,8 +41,6 @@ public class LongIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        long okey = (long) o.key();
-
-        return Long.compare(key, okey);
+        return Long.compare(key, ((LongIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ShortIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ShortIndexKey.java
index afd6b09..f688053 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ShortIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/ShortIndexKey.java
@@ -41,9 +41,7 @@ public class ShortIndexKey implements IndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        short okey = (short) o.key();
-
         // Keep old logic there.
-        return Integer.compare(key, okey);
+        return Integer.compare(key, ((ShortIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
index 56d4565..0e52409 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/keys/SignedBytesIndexKey.java
@@ -26,8 +26,6 @@ public class SignedBytesIndexKey extends BytesIndexKey {
 
     /** {@inheritDoc} */
     @Override public int compare(IndexKey o) {
-        byte[] okey = (byte[]) o.key();
-
-        return BytesCompareUtils.compareNotNullSigned(key, okey);
+        return BytesCompareUtils.compareNotNullSigned(key, ((BytesIndexKey)o).key);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedFieldsQueryFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedFieldsQueryFuture.java
index a730347..7235d3f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedFieldsQueryFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedFieldsQueryFuture.java
@@ -59,12 +59,12 @@ public class GridCacheDistributedFieldsQueryFuture
      * @param err Error.
      * @param finished Finished or not.
      */
-    public void onPage(@Nullable UUID nodeId, @Nullable List<GridQueryFieldMetadata> metaData,
+    public void onFieldsPage(@Nullable UUID nodeId, @Nullable List<GridQueryFieldMetadata> metaData,
         @Nullable Collection<Map<String, Object>> data, @Nullable Throwable err, boolean finished) {
         if (!metaFut.isDone() && metaData != null)
             metaFut.onDone(metaData);
 
-        onPage(nodeId, data, err, finished);
+        onPage(nodeId, null, data, err, finished);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryFuture.java
index 1efc895..b782a96 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryFuture.java
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -29,14 +30,17 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteIllegalStateException;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
-import org.apache.ignite.internal.processors.cache.query.reducer.MergeSortCacheQueryReducer;
+import org.apache.ignite.internal.processors.cache.query.reducer.IndexQueryReducer;
 import org.apache.ignite.internal.processors.cache.query.reducer.NodePageStream;
+import org.apache.ignite.internal.processors.cache.query.reducer.TextQueryReducer;
 import org.apache.ignite.internal.processors.cache.query.reducer.UnsortedCacheQueryReducer;
 import org.apache.ignite.internal.util.lang.GridPlainCallable;
 import org.apache.ignite.internal.util.typedef.internal.U;
 
+import static org.apache.ignite.internal.processors.cache.query.GridCacheQueryType.INDEX;
 import static org.apache.ignite.internal.processors.cache.query.GridCacheQueryType.TEXT;
 
 /**
@@ -61,6 +65,9 @@ public class GridCacheDistributedQueryFuture<K, V, R> extends GridCacheQueryFutu
     /** Set of nodes that deliver their first page. */
     private Set<UUID> rcvdFirstPage = ConcurrentHashMap.newKeySet();
 
+    /** Metadata for IndexQuery. */
+    private final CompletableFuture<IndexQueryResultMeta> idxQryMetaFut;
+
     /**
      * @param ctx Cache context.
      * @param reqId Request ID.
@@ -89,9 +96,16 @@ public class GridCacheDistributedQueryFuture<K, V, R> extends GridCacheQueryFutu
 
         Map<UUID, NodePageStream<R>> streamsMap = Collections.unmodifiableMap(streams);
 
-        reducer = qry.query().type() == TEXT ?
-            new MergeSortCacheQueryReducer<>(streamsMap)
-            : new UnsortedCacheQueryReducer<>(streamsMap);
+        if (qry.query().type() == INDEX) {
+            idxQryMetaFut = new CompletableFuture<>();
+
+            reducer = new IndexQueryReducer<>(qry.query().idxQryDesc().valType(), streamsMap, cctx, idxQryMetaFut);
+        }
+        else {
+            idxQryMetaFut = null;
+
+            reducer = qry.query().type() == TEXT ? new TextQueryReducer<>(streamsMap) : new UnsortedCacheQueryReducer<>(streamsMap);
+        }
     }
 
     /** {@inheritDoc} */
@@ -150,6 +164,12 @@ public class GridCacheDistributedQueryFuture<K, V, R> extends GridCacheQueryFutu
     }
 
     /** {@inheritDoc} */
+    @Override protected void onMeta(IndexQueryResultMeta metaData) {
+        if (metaData != null)
+            idxQryMetaFut.complete(metaData);
+    }
+
+    /** {@inheritDoc} */
     @Override public void awaitFirstItemAvailable() throws IgniteCheckedException {
         U.await(firstPageLatch);
 
@@ -270,6 +290,9 @@ public class GridCacheDistributedQueryFuture<K, V, R> extends GridCacheQueryFutu
         if (onDone(err)) {
             streams.values().forEach(s -> s.cancel(err));
 
+            if (idxQryMetaFut != null && !idxQryMetaFut.isDone())
+                idxQryMetaFut.completeExceptionally(err);
+
             firstPageLatch.countDown();
         }
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryManager.java
index 6fb0907..25a145f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheDistributedQueryManager.java
@@ -32,6 +32,7 @@ import org.apache.ignite.events.Event;
 import org.apache.ignite.internal.IgniteClientDisconnectedCheckedException;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.managers.communication.GridIoPolicy;
 import org.apache.ignite.internal.managers.eventstorage.GridLocalEventListener;
@@ -58,6 +59,7 @@ import static org.apache.ignite.cache.CacheMode.LOCAL;
 import static org.apache.ignite.events.EventType.EVT_NODE_FAILED;
 import static org.apache.ignite.events.EventType.EVT_NODE_LEFT;
 import static org.apache.ignite.internal.GridTopic.TOPIC_CACHE;
+import static org.apache.ignite.internal.processors.cache.query.GridCacheQueryType.INDEX;
 import static org.apache.ignite.internal.processors.cache.query.GridCacheQueryType.SCAN;
 
 /**
@@ -139,7 +141,7 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
         for (Map.Entry<Long, GridCacheDistributedQueryFuture<?, ?, ?>> e : futs.entrySet()) {
             GridCacheDistributedQueryFuture<?, ?, ?> fut = e.getValue();
 
-            fut.onPage(null, null, err, true);
+            fut.onPage(null, null, null, err, true);
 
             futs.remove(e.getKey(), fut);
         }
@@ -377,14 +379,14 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
 
         if (fut != null)
             if (res.fields())
-                ((GridCacheDistributedFieldsQueryFuture)fut).onPage(
+                ((GridCacheDistributedFieldsQueryFuture)fut).onFieldsPage(
                     sndId,
                     res.metadata(),
                     (Collection<Map<String, Object>>)((Collection)res.data()),
                     res.error(),
                     res.isFinished());
             else
-                fut.onPage(sndId, res.data(), res.error(), res.isFinished());
+                fut.onPage(sndId, res.idxQryMetadata(), res.data(), res.error(), res.isFinished());
         else if (!cancelled.contains(res.requestId()))
             U.warn(log, "Received response for finished or unknown query [rmtNodeId=" + sndId +
                 ", res=" + res + ']');
@@ -411,8 +413,14 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
     }
 
     /** {@inheritDoc} */
-    @Override protected boolean onPageReady(boolean loc, GridCacheQueryInfo qryInfo,
-        Collection<?> data, boolean finished, Throwable e) {
+    @Override protected boolean onPageReady(
+        boolean loc,
+        GridCacheQueryInfo qryInfo,
+        IndexQueryResultMeta idxQryMetadata,
+        Collection<?> data,
+        boolean finished,
+        Throwable e
+    ) {
         GridCacheLocalQueryFuture<?, ?, ?> fut = qryInfo.localQueryFuture();
 
         if (loc)
@@ -420,7 +428,7 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
 
         if (e != null) {
             if (loc)
-                fut.onPage(null, null, e, true);
+                fut.onPage(null, null, null, e, true);
             else
                 sendQueryResponse(qryInfo.senderId(),
                     new GridCacheQueryResponse(cctx.cacheId(), qryInfo.requestId(), e, cctx.deploymentEnabled()),
@@ -430,13 +438,15 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
         }
 
         if (loc)
-            fut.onPage(null, data, null, finished);
+            fut.onPage(null, null, data, null, finished);
         else {
             GridCacheQueryResponse res = new GridCacheQueryResponse(cctx.cacheId(), qryInfo.requestId(),
-                /*finished*/false, /*fields*/false, cctx.deploymentEnabled());
+                finished, /*fields*/false, cctx.deploymentEnabled());
+
+            if (qryInfo.query().type() == INDEX)
+                res.idxQryMetadata((IndexQueryResultMeta)idxQryMetadata);
 
             res.data(data);
-            res.finished(finished);
 
             if (!sendQueryResponse(qryInfo.senderId(), res, qryInfo.query().timeout()))
                 return false;
@@ -471,7 +481,7 @@ public class GridCacheDistributedQueryManager<K, V> extends GridCacheQueryManage
         if (loc) {
             GridCacheLocalFieldsQueryFuture fut = (GridCacheLocalFieldsQueryFuture)qryInfo.localQueryFuture();
 
-            fut.onPage(null, metadata, data, null, finished);
+            fut.onFieldsPage(null, metadata, data, null, finished);
         }
         else {
             GridCacheQueryResponse res = new GridCacheQueryResponse(cctx.cacheId(), qryInfo.requestId(),
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalFieldsQueryFuture.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalFieldsQueryFuture.java
index 20e4066..55057b2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalFieldsQueryFuture.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalFieldsQueryFuture.java
@@ -58,9 +58,9 @@ public class GridCacheLocalFieldsQueryFuture
      * @param err Error.
      * @param finished Finished or not.
      */
-    public void onPage(@Nullable UUID nodeId, @Nullable List<GridQueryFieldMetadata> metaData,
+    public void onFieldsPage(@Nullable UUID nodeId, @Nullable List<GridQueryFieldMetadata> metaData,
         @Nullable Collection<?> data, @Nullable Throwable err, boolean finished) {
-        onPage(nodeId, data, err, finished);
+        onPage(nodeId, null, data, err, finished);
 
         if (!metaFut.isDone())
             metaFut.onDone(metaData);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalQueryManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalQueryManager.java
index 4d1e20e..1e1f2f0 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalQueryManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheLocalQueryManager.java
@@ -22,6 +22,7 @@ import java.util.List;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
 import org.apache.ignite.internal.util.lang.GridCloseableIterator;
 import org.jetbrains.annotations.Nullable;
@@ -37,6 +38,7 @@ public class GridCacheLocalQueryManager<K, V> extends GridCacheQueryManager<K, V
     @Override protected boolean onPageReady(
         boolean loc,
         GridCacheQueryInfo qryInfo,
+        IndexQueryResultMeta metadata,
         Collection<?> data,
         boolean finished, Throwable e) {
         GridCacheQueryFutureAdapter fut = qryInfo.localQueryFuture();
@@ -44,9 +46,9 @@ public class GridCacheLocalQueryManager<K, V> extends GridCacheQueryManager<K, V
         assert fut != null;
 
         if (e != null)
-            fut.onPage(null, null, e, true);
+            fut.onPage(null, null, null, e, true);
         else
-            fut.onPage(null, data, null, finished);
+            fut.onPage(null, metadata, data, null, finished);
 
         return true;
     }
@@ -68,7 +70,7 @@ public class GridCacheLocalQueryManager<K, V> extends GridCacheQueryManager<K, V
         if (e != null)
             fut.onPage(null, null, null, e, true);
         else
-            fut.onPage(null, metaData, data, null, finished);
+            fut.onFieldsPage(null, metaData, data, null, finished);
 
         return true;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryFutureAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryFutureAdapter.java
index 691ea1c..b40c134 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryFutureAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryFutureAdapter.java
@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.processors.cache.CacheObjectUtils;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.query.reducer.CacheQueryReducer;
@@ -149,7 +150,7 @@ public abstract class GridCacheQueryFutureAdapter<K, V, R> extends GridFutureAda
             R next = null;
 
             if (reducer.hasNextX()) {
-                next = unmaskNull(reducer.nextX());
+                next = unmaskNull(unwrapIfNeeded(reducer.nextX()));
 
                 if (!limitDisabled) {
                     cnt++;
@@ -207,11 +208,18 @@ public abstract class GridCacheQueryFutureAdapter<K, V, R> extends GridFutureAda
      * Entrypoint for handling query result page from remote node.
      *
      * @param nodeId Sender node.
+     * @param metadata Query response metadata.
      * @param data Page data.
      * @param err Error (if was).
      * @param lastPage Whether it is the last page for sender node.
      */
-    public void onPage(@Nullable UUID nodeId, @Nullable Collection<?> data, @Nullable Throwable err, boolean lastPage) {
+    public void onPage(
+        @Nullable UUID nodeId,
+        @Nullable IndexQueryResultMeta metadata,
+        @Nullable Collection<?> data,
+        @Nullable Throwable err,
+        boolean lastPage
+    ) {
         if (isCancelled())
             return;
 
@@ -260,8 +268,13 @@ public abstract class GridCacheQueryFutureAdapter<K, V, R> extends GridFutureAda
 
                     data = unwrapped;
 
-                } else
+                } else if (qry.query().type() != GridCacheQueryType.INDEX) {
+                    // For IndexQuery BinaryObjects are used for sorting algorithm.
                     data = cctx.unwrapBinariesIfNeeded((Collection<Object>)data, qry.query().keepBinary());
+                }
+
+                if (query().query().type() == GridCacheQueryType.INDEX)
+                    onMeta(metadata);
 
                 onPage(nodeId, (Collection<R>) data, lastPage);
 
@@ -283,6 +296,11 @@ public abstract class GridCacheQueryFutureAdapter<K, V, R> extends GridFutureAda
     /** Handles new data page from query node. */
     protected abstract void onPage(UUID nodeId, Collection<R> data, boolean lastPage);
 
+    /** Handles query meta data from query node. */
+    protected void onMeta(IndexQueryResultMeta meta) {
+        // No-op.
+    }
+
     /** {@inheritDoc} */
     @Override public boolean onDone(Collection<R> res, Throwable err) {
         boolean done = super.onDone(res, err);
@@ -328,6 +346,14 @@ public abstract class GridCacheQueryFutureAdapter<K, V, R> extends GridFutureAda
         return obj != NULL ? obj : null;
     }
 
+    /** */
+    private R unwrapIfNeeded(R obj) {
+        if (qry.query().type() == GridCacheQueryType.INDEX)
+            return (R)cctx.unwrapBinaryIfNeeded(obj, qry.query().keepBinary(), false, null);
+
+        return obj;
+    }
+
     /**
      * Clears future.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryManager.java
index d02316b..ced4e4a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryManager.java
@@ -38,6 +38,7 @@ import java.util.Queue;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.BiFunction;
@@ -61,6 +62,8 @@ import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.IgniteKernal;
 import org.apache.ignite.internal.NodeStoppingException;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResult;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.managers.eventstorage.GridLocalEventListener;
 import org.apache.ignite.internal.metric.IoStatisticsHolder;
 import org.apache.ignite.internal.metric.IoStatisticsQueryHelper;
@@ -626,8 +629,11 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
                     break;
 
                 case INDEX:
-                    iter = qryProc.queryIndex(cacheName, qry.queryClassName(), qry.idxQryDesc(), qry.scanFilter(),
-                        filter(qry), qry.keepBinary());
+                    IndexQueryResult<K, V> idxQryRes = qryProc.queryIndex(cacheName, qry.queryClassName(), qry.idxQryDesc(),
+                        qry.scanFilter(), filter(qry), qry.keepBinary());
+
+                    iter = idxQryRes.iter();
+                    res.metadata(idxQryRes.metadata());
 
                     break;
 
@@ -988,7 +994,7 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
 
                     // Query is cancelled.
                     if (row == null) {
-                        onPageReady(qryInfo.local(), qryInfo, null, true, null);
+                        onPageReady(qryInfo.local(), qryInfo, null, null, true, null);
 
                         break;
                     }
@@ -1191,7 +1197,7 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
 
                     // Query is cancelled.
                     if (row0 == null) {
-                        onPageReady(loc, qryInfo, null, true, null);
+                        onPageReady(loc, qryInfo, null, null, true, null);
 
                         break;
                     }
@@ -1295,7 +1301,7 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
 
                             // Reduce.
                             if (!rdc.collect(entry) || !iter.hasNext()) {
-                                onPageReady(loc, qryInfo, Collections.singletonList(rdc.reduce()), true, null);
+                                onPageReady(loc, qryInfo, null, Collections.singletonList(rdc.reduce()), true, null);
 
                                 pageSent = true;
 
@@ -1317,10 +1323,12 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
                         if (++cnt == pageSize || !iter.hasNext()) {
                             boolean finished = !iter.hasNext();
 
-                            onPageReady(loc, qryInfo, data, finished, null);
+                            onPageReady(loc, qryInfo, res.metadata(), data, finished, null);
 
                             pageSent = true;
 
+                            res.onPageSend();
+
                             if (!finished)
                                 rmvIter = false;
 
@@ -1337,9 +1345,11 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
 
                 if (!pageSent) {
                     if (rdc == null)
-                        onPageReady(loc, qryInfo, data, true, null);
+                        onPageReady(loc, qryInfo, res.metadata(), data, true, null);
                     else
-                        onPageReady(loc, qryInfo, Collections.singletonList(rdc.reduce()), true, null);
+                        onPageReady(loc, qryInfo, res.metadata(), Collections.singletonList(rdc.reduce()), true, null);
+
+                    res.onPageSend();
                 }
             }
             catch (Throwable e) {
@@ -1358,7 +1368,7 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
                 if (!X.hasCause(e, GridDhtUnreservedPartitionException.class))
                     U.error(log, "Failed to run query [qry=" + qryInfo + ", node=" + cctx.nodeId() + "]", e);
 
-                onPageReady(loc, qryInfo, null, true, e);
+                onPageReady(loc, qryInfo, null, null, true, e);
 
                 if (e instanceof Error)
                     throw (Error)e;
@@ -1709,12 +1719,13 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
      *
      * @param loc Local query or not.
      * @param qryInfo Query info.
+     * @param metaData Meta data.
      * @param data Result data.
      * @param finished Last page or not.
      * @param e Exception in case of error.
      * @return {@code true} if page was processed right.
      */
-    protected abstract boolean onPageReady(boolean loc, GridCacheQueryInfo qryInfo,
+    protected abstract boolean onPageReady(boolean loc, GridCacheQueryInfo qryInfo, @Nullable IndexQueryResultMeta metaData,
         @Nullable Collection<?> data, boolean finished, @Nullable Throwable e);
 
     /**
@@ -2395,11 +2406,14 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
      */
     public static class QueryResult<K, V> extends CachedResult<IgniteBiTuple<K, V>> {
         /** */
-        private static final long serialVersionUID = 0L;
-
-        /** */
         private final GridCacheQueryType type;
 
+        /** Future of query result metadata. Completed when query actually started. */
+        private final CompletableFuture<IndexQueryResultMeta> metadata;
+
+        /** Flag shows whether first result page was delivered to user. */
+        private volatile boolean sentFirst;
+
         /**
          * @param type Query type.
          * @param rcpt ID of the recipient.
@@ -2408,6 +2422,8 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
             super(rcpt);
 
             this.type = type;
+
+            metadata = type == INDEX ? new CompletableFuture<>() : null;
         }
 
         /**
@@ -2416,6 +2432,37 @@ public abstract class GridCacheQueryManager<K, V> extends GridCacheManagerAdapte
         public GridCacheQueryType type() {
             return type;
         }
+
+        /** */
+        public IndexQueryResultMeta metadata() {
+            if (sentFirst || metadata == null)
+                return null;
+
+            assert metadata.isDone() : "QueryResult metadata isn't completed yet.";
+
+            return metadata.getNow(null);
+        }
+
+        /** */
+        public void metadata(IndexQueryResultMeta metadata) {
+            if (this.metadata != null)
+                this.metadata.complete(metadata);
+        }
+
+        /** Callback to invoke, when next data page was delivered to user. */
+        public void onPageSend() {
+            sentFirst = true;
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean onDone(@Nullable IgniteSpiCloseableIterator<IgniteBiTuple<K, V>> res, @Nullable Throwable err) {
+            boolean done = super.onDone(res, err);
+
+            if (done && err != null && metadata != null)
+                metadata.completeExceptionally(err);
+
+            return done;
+        }
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryResponse.java
index 8e05c02..99de925 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryResponse.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryResponse.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.GridDirectCollection;
 import org.apache.ignite.internal.GridDirectTransient;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
 import org.apache.ignite.internal.processors.cache.CacheObjectContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.GridCacheDeployable;
@@ -44,7 +45,7 @@ import org.apache.ignite.plugin.extensions.communication.MessageWriter;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Query request.
+ * Page of cache query response.
  */
 public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCacheDeployable {
     /** */
@@ -76,6 +77,13 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
     private List<GridQueryFieldMetadata> metadata;
 
     /** */
+    @GridDirectTransient
+    private IndexQueryResultMeta idxQryMetadata;
+
+    /** */
+    private byte[] idxQryMetadataBytes;
+
+    /** */
     @GridDirectCollection(byte[].class)
     private Collection<byte[]> dataBytes;
 
@@ -133,6 +141,9 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
         if (metaDataBytes == null && metadata != null)
             metaDataBytes = marshalCollection(metadata, cctx);
 
+        if (idxQryMetadataBytes == null && idxQryMetadata != null)
+            idxQryMetadataBytes = U.marshal(ctx, idxQryMetadata);
+
         if (dataBytes == null && data != null)
             dataBytes = marshalCollection(data, cctx);
 
@@ -158,6 +169,9 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
         if (metadata == null)
             metadata = unmarshalCollection(metaDataBytes, ctx, ldr);
 
+        if (idxQryMetadataBytes != null && idxQryMetadata == null)
+            idxQryMetadata = U.unmarshal(ctx, idxQryMetadataBytes, U.resolveClassLoader(ldr, ctx.gridConfig()));
+
         if (data == null)
             data = unmarshalCollection0(dataBytes, ctx, ldr);
     }
@@ -218,6 +232,13 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
     }
 
     /**
+     * @return IndexQuery metadata.
+     */
+    public IndexQueryResultMeta idxQryMetadata() {
+        return idxQryMetadata;
+    }
+
+    /**
      * @param metadata Metadata.
      */
     public void metadata(@Nullable List<GridQueryFieldMetadata> metadata) {
@@ -225,6 +246,13 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
     }
 
     /**
+     * @param idxQryMetadata IndexQuery metadata.
+     */
+    public void idxQryMetadata(IndexQueryResultMeta idxQryMetadata) {
+        this.idxQryMetadata = idxQryMetadata;
+    }
+
+    /**
      * @return Query data.
      */
     public Collection<Object> data() {
@@ -322,6 +350,11 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
 
                 writer.incrementState();
 
+            case 10:
+                if (!writer.writeByteArray("idxQryMetadataBytes", idxQryMetadataBytes))
+                    return false;
+
+                writer.incrementState();
         }
 
         return true;
@@ -386,6 +419,13 @@ public class GridCacheQueryResponse extends GridCacheIdMessage implements GridCa
 
                 reader.incrementState();
 
+            case 10:
+                idxQryMetadataBytes = reader.readByteArray("idxQryMetadataBytes");
+
+                if (!reader.isLastRead())
+                    return false;
+
+                reader.incrementState();
         }
 
         return reader.afterMessageRead(GridCacheQueryResponse.class);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/CacheQueryReducer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/CacheQueryReducer.java
index abbe949..b827c2f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/CacheQueryReducer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/CacheQueryReducer.java
@@ -47,11 +47,12 @@ public abstract class CacheQueryReducer<T> extends GridIteratorAdapter<T> {
     }
 
     /**
-     * @return Page with query results data from specified stream.
+     * @return Object that completed the specified future.
+     * @throws IgniteCheckedException for all failures.
      */
-    public static <T> NodePage<T> get(CompletableFuture<?> pageFut) throws IgniteCheckedException {
+    public static <T> T get(CompletableFuture<?> fut) throws IgniteCheckedException {
         try {
-            return (NodePage<T>) pageFut.get();
+            return (T) fut.get();
         }
         catch (InterruptedException e) {
             Thread.currentThread().interrupt();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
new file mode 100644
index 0000000..75ac0c4
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/IndexQueryReducer.java
@@ -0,0 +1,149 @@
+/*
+ * 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.cache.query.reducer;
+
+import java.io.Serializable;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRowComparator;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRowCompartorImpl;
+import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
+import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKeyFactory;
+import org.apache.ignite.internal.processors.cache.GridCacheContext;
+import org.apache.ignite.internal.processors.query.GridQueryProperty;
+import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.lang.IgniteBiTuple;
+
+import static org.apache.ignite.internal.cache.query.index.SortOrder.DESC;
+
+/**
+ * Reducer for {@code IndexQuery} results.
+ */
+public class IndexQueryReducer<R> extends MergeSortCacheQueryReducer<R> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Future that will be completed with first page response. */
+    private final CompletableFuture<IndexQueryResultMeta> metaFut;
+
+    /** */
+    private final String valType;
+
+    /** Cache context. */
+    private final GridCacheContext<?, ?> cctx;
+
+    /** */
+    public IndexQueryReducer(
+        final String valType,
+        final Map<UUID, NodePageStream<R>> pageStreams,
+        final GridCacheContext<?, ?> cctx,
+        final CompletableFuture<IndexQueryResultMeta> meta
+    ) {
+        super(pageStreams);
+
+        this.valType = valType;
+        this.metaFut = meta;
+        this.cctx = cctx;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected CompletableFuture<Comparator<NodePage<R>>> pageComparator() {
+        return metaFut.thenApply(m -> {
+            LinkedHashMap<String, IndexKeyDefinition> keyDefs = m.keyDefinitions();
+
+            GridQueryTypeDescriptor typeDesc = cctx.kernalContext().query().typeDescriptor(cctx.name(), QueryUtils.typeName(valType));
+
+            return new IndexedNodePageComparator(m, typeDesc, keyDefs);
+        });
+    }
+
+    /** Comparing rows by indexed keys. */
+    private class IndexedNodePageComparator implements Comparator<NodePage<R>>, Serializable {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Index key defintiions in case of IndexQuery. */
+        private final LinkedHashMap<String, IndexKeyDefinition> keyDefs;
+
+        /** Description of value type for IndexQuery. */
+        private final GridQueryTypeDescriptor typeDesc;
+
+        /** IndexQuery meta. */
+        private final IndexQueryResultMeta meta;
+
+        /** Every node will return the same key types for the same index, then it's possible to use simple comparator. */
+        private final IndexRowComparator idxRowComp = new IndexRowCompartorImpl();
+
+        /** */
+        IndexedNodePageComparator(
+            IndexQueryResultMeta meta,
+            GridQueryTypeDescriptor typeDesc,
+            LinkedHashMap<String, IndexKeyDefinition> keyDefs
+        ) {
+            this.meta = meta;
+            this.typeDesc = typeDesc;
+            this.keyDefs = keyDefs;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int compare(NodePage<R> o1, NodePage<R> o2) {
+            IgniteBiTuple<?, ?> e1 = (IgniteBiTuple<?, ?>)o1.head();
+            IgniteBiTuple<?, ?> e2 = (IgniteBiTuple<?, ?>)o2.head();
+
+            Iterator<Map.Entry<String, IndexKeyDefinition>> defs = keyDefs.entrySet().iterator();
+
+            try {
+                while (defs.hasNext()) {
+                    Map.Entry<String, IndexKeyDefinition> d = defs.next();
+
+                    IndexKey k1 = key(d.getKey(), d.getValue().idxType(), e1);
+                    IndexKey k2 = key(d.getKey(), d.getValue().idxType(), e2);
+
+                    int cmp = idxRowComp.compareKey(k1, k2);
+
+                    if (cmp != 0)
+                        return d.getValue().order().sortOrder() == DESC ? -cmp : cmp;
+                }
+
+                return 0;
+
+            } catch (IgniteCheckedException e) {
+                throw new IgniteException("Failed to sort remote index rows", e);
+            }
+        }
+
+        /** */
+        private IndexKey key(String key, int type, IgniteBiTuple<?, ?> entry) throws IgniteCheckedException {
+            GridQueryProperty prop = typeDesc.property(key);
+
+            // PrimaryKey field.
+            Object o = prop == null ? entry.getKey() : prop.value(entry.getKey(), entry.getValue());
+
+            return IndexKeyFactory.wrap(o, type, cctx.cacheObjectContext(), meta.keyTypeSettings());
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/MergeSortCacheQueryReducer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/MergeSortCacheQueryReducer.java
index d2b3a3b..3024a07 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/MergeSortCacheQueryReducer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/MergeSortCacheQueryReducer.java
@@ -22,14 +22,14 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.IgniteCheckedException;
-import org.apache.ignite.internal.processors.cache.query.ScoredCacheEntry;
 
 /**
  * Reducer of cache query results that sort result through all nodes. Note that it's assumed that every node
  * returns pre-sorted collection of data.
  */
-public class MergeSortCacheQueryReducer<R> extends CacheQueryReducer<R> {
+abstract class MergeSortCacheQueryReducer<R> extends CacheQueryReducer<R> {
     /** */
     private static final long serialVersionUID = 0L;
 
@@ -43,19 +43,19 @@ public class MergeSortCacheQueryReducer<R> extends CacheQueryReducer<R> {
     private UUID pendingNodeId;
 
     /** */
-    public MergeSortCacheQueryReducer(final Map<UUID, NodePageStream<R>> pageStreams) {
+    protected MergeSortCacheQueryReducer(final Map<UUID, NodePageStream<R>> pageStreams) {
         super(pageStreams);
     }
 
+    /** @return Comparator for pages from nodes. */
+    protected abstract CompletableFuture<Comparator<NodePage<R>>> pageComparator();
+
     /** {@inheritDoc} */
     @Override public boolean hasNextX() throws IgniteCheckedException {
         // Initial sort.
         if (nodePages == null) {
             // Compares head pages from all nodes to get the lowest value at the moment.
-            Comparator<NodePage<R>> pageCmp = (o1, o2) -> textResultComparator.compare(
-                (ScoredCacheEntry<?, ?>)o1.head(), (ScoredCacheEntry<?, ?>)o2.head());
-
-            nodePages = new PriorityQueue<>(pageStreams.size(), pageCmp);
+            nodePages = new PriorityQueue<>(pageStreams.size(), get(pageComparator()));
 
             for (NodePageStream<R> s : pageStreams.values()) {
                 NodePage<R> p = get(s.headPage());
@@ -76,6 +76,8 @@ public class MergeSortCacheQueryReducer<R> extends CacheQueryReducer<R> {
                 if (p != null && p.hasNext())
                     nodePages.add(p);
             }
+
+            pendingNodeId = null;
         }
 
         return !nodePages.isEmpty();
@@ -97,8 +99,4 @@ public class MergeSortCacheQueryReducer<R> extends CacheQueryReducer<R> {
 
         return o;
     }
-
-    /** Compares rows for {@code TextQuery} results for ordering results in MergeSort reducer. */
-    private static final Comparator<ScoredCacheEntry<?, ?>> textResultComparator = (c1, c2) ->
-        Float.compare(c2.score(), c1.score());
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/TextQueryReducer.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/TextQueryReducer.java
new file mode 100644
index 0000000..5ad5a04
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/reducer/TextQueryReducer.java
@@ -0,0 +1,47 @@
+/*
+ * 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.cache.query.reducer;
+
+import java.util.Comparator;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.internal.processors.cache.query.ScoredCacheEntry;
+
+/**
+ * Reducer for {@code TextQuery} results.
+ */
+public class TextQueryReducer<R> extends MergeSortCacheQueryReducer<R> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    public TextQueryReducer(final Map<UUID, NodePageStream<R>> pageStreams) {
+        super(pageStreams);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected CompletableFuture<Comparator<NodePage<R>>> pageComparator() {
+        CompletableFuture<Comparator<NodePage<R>>> f = new CompletableFuture<>();
+
+        f.complete((o1, o2) -> -Float.compare(
+            ((ScoredCacheEntry<?, ?>)o1.head()).score(), ((ScoredCacheEntry<?, ?>)o2.head()).score()));
+
+        return f;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
index 654adf6..5ada406 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
@@ -63,6 +63,7 @@ import org.apache.ignite.internal.NodeStoppingException;
 import org.apache.ignite.internal.binary.BinaryMetadata;
 import org.apache.ignite.internal.cache.query.index.IndexProcessor;
 import org.apache.ignite.internal.cache.query.index.IndexQueryProcessor;
+import org.apache.ignite.internal.cache.query.index.IndexQueryResult;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.managers.communication.GridMessageListener;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
@@ -3374,7 +3375,7 @@ public class GridQueryProcessor extends GridProcessorAdapter {
      * @return Key/value rows.
      * @throws IgniteCheckedException If failed.
      */
-    public <K, V> GridCloseableIterator<IgniteBiTuple<K, V>> queryIndex(
+    public <K, V> IndexQueryResult<K, V> queryIndex(
         String cacheName,
         String valCls,
         final IndexQueryDesc idxQryDesc,
@@ -3389,8 +3390,8 @@ public class GridQueryProcessor extends GridProcessorAdapter {
             final GridCacheContext<K, V> cctx = (GridCacheContext<K, V>) ctx.cache().internalCache(cacheName).context();
 
             return executeQuery(GridCacheQueryType.INDEX, valCls, cctx,
-                new IgniteOutClosureX<GridCloseableIterator<IgniteBiTuple<K, V>>>() {
-                    @Override public GridCloseableIterator<IgniteBiTuple<K, V>> applyx() throws IgniteCheckedException {
+                new IgniteOutClosureX<IndexQueryResult<K, V>>() {
+                    @Override public IndexQueryResult<K, V> applyx() throws IgniteCheckedException {
                         IndexQueryContext qryCtx = new IndexQueryContext(filters, null);
 
                         return idxQryPrc.queryLocal(cctx, idxQryDesc, filter, qryCtx, keepBinary);
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
index 5530b85..d9722fd 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/index/H2RowComparator.java
@@ -49,11 +49,14 @@ public class H2RowComparator extends IndexRowCompartorImpl {
     /** Ignite H2 session. */
     private final SessionInterface ses;
 
+    /** Key type settings for this index. */
+    private final IndexKeyTypeSettings keyTypeSettings;
+
     /** */
     public H2RowComparator(GridH2Table table, IndexKeyTypeSettings keyTypeSettings) {
-        super(keyTypeSettings);
-
         this.table = table;
+        this.keyTypeSettings = keyTypeSettings;
+
         coctx = table.rowDescriptor().context().cacheObjectContext();
         ses = table.rowDescriptor().indexing().connections().jdbcConnection().getSession();
     }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAliasTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAliasTest.java
index 9b49518..6e49453 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAliasTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAliasTest.java
@@ -23,9 +23,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
@@ -34,7 +31,9 @@ import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.cache.QueryIndex;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -120,13 +119,13 @@ public class IndexQueryAliasTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("asId", pivot));
 
-        check(cache.query(qry), 0, pivot);
+        check(cache.query(qry), 0, pivot, false);
 
         // Lt, desc index.
         IndexQuery<Long, Person> descQry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
             .setCriteria(lt("asDescId", pivot));
 
-        check(cache.query(descQry), 0, pivot);
+        check(cache.query(descQry), 0, pivot, true);
     }
 
     /** */
@@ -140,7 +139,7 @@ public class IndexQueryAliasTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, idIdx)
             .setCriteria(lt("ASID", pivot));
 
-        check(cache.query(qry), 0, pivot);
+        check(cache.query(qry), 0, pivot, false);
 
         String idDescIdx = qryDescIdx != null ? qryDescIdx.toLowerCase() : null;
 
@@ -148,35 +147,33 @@ public class IndexQueryAliasTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> descQry = new IndexQuery<Long, Person>(Person.class, idDescIdx)
             .setCriteria(lt("ASDESCID", pivot));
 
-        check(cache.query(descQry), 0, pivot);
+        check(cache.query(descQry), 0, pivot, true);
     }
 
     /**
      * @param left First cache key, inclusive.
      * @param right Last cache key, exclusive.
      */
-    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right) {
+    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right, boolean desc) {
         List<Cache.Entry<Long, Person>> all = cursor.getAll();
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
-
         for (int i = 0; i < all.size(); i++) {
-            Cache.Entry<Long, Person> entry = all.get(i);
-
-            assertTrue(expKeys.remove(entry.getKey()));
+            int expKey = desc ? right - 1 - i : i;
 
-            assertEquals(new Person(entry.getKey().intValue()), all.get(i).getValue());
+            assertEquals(new Person(expKey), all.get(i).getValue());
         }
     }
 
     /** */
     private static class Person {
         /** */
+        @GridToStringInclude
         final int id;
 
         /** */
+        @GridToStringInclude
         final int descId;
 
         /** */
@@ -201,5 +198,10 @@ public class IndexQueryAliasTest extends GridCommonAbstractTest {
         @Override public int hashCode() {
             return Objects.hash(id, descId);
         }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Person.class, this);
+        }
     }
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAllTypesTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAllTypesTest.java
index d450cbd..a98b8bc 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAllTypesTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryAllTypesTest.java
@@ -48,6 +48,7 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.eq;
 import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gt;
 import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.gte;
 import static org.apache.ignite.cache.query.IndexQueryCriteriaBuilder.lt;
@@ -259,7 +260,7 @@ public class IndexQueryAllTypesTest extends GridCommonAbstractTest {
     /** Also checks duplicate indexed values. */
     @Test
     public void testBoolField() {
-        Function<Integer, Boolean> valGen = i -> i <= CNT / 2;
+        Function<Integer, Boolean> valGen = i -> i > CNT / 2;
 
         Function<Boolean, Person> persGen = i -> person("boolId", i);
 
@@ -272,29 +273,17 @@ public class IndexQueryAllTypesTest extends GridCommonAbstractTest {
         // All.
         check(cache.query(qry), 0, CNT, valGen, persGen);
 
-        // Lt.
+        // Eq true.
         qry = new IndexQuery<Long, Person>(Person.class, boolIdx)
-            .setCriteria(lt("boolId", true));
+            .setCriteria(eq("boolId", true));
 
         check(cache.query(qry), CNT / 2 + 1, CNT, valGen, persGen);
 
-        // Lte.
-        qry = new IndexQuery<Long, Person>(Person.class, boolIdx)
-            .setCriteria(lte("boolId", true));
-
-        check(cache.query(qry), 0, CNT, valGen, persGen);
-
-        // Gt.
+        // Eq false.
         qry = new IndexQuery<Long, Person>(Person.class, boolIdx)
-            .setCriteria(gt("boolId", false));
+            .setCriteria(eq("boolId", false));
 
         check(cache.query(qry), 0, CNT / 2 + 1, valGen, persGen);
-
-        // Gte.
-        qry = new IndexQuery<Long, Person>(Person.class, boolIdx)
-            .setCriteria(gte("boolId", false));
-
-        check(cache.query(qry), 0, CNT, valGen, persGen);
     }
 
     /** */
@@ -368,7 +357,7 @@ public class IndexQueryAllTypesTest extends GridCommonAbstractTest {
 
             assertTrue(expKeys.remove(entry.getKey()));
 
-            assertEquals(persGen.apply(valGen.apply(entry.getKey().intValue())), all.get(i).getValue());
+            assertEquals(persGen.apply(valGen.apply(left + i)), all.get(i).getValue());
         }
 
         assertTrue(expKeys.isEmpty());
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryFilterTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryFilterTest.java
index cf00e12..c965d67 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryFilterTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryFilterTest.java
@@ -17,12 +17,15 @@
 
 package org.apache.ignite.cache.query;
 
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
-import java.util.stream.Collectors;
+import java.util.Set;
+import java.util.TreeMap;
 import javax.cache.Cache;
 import javax.cache.CacheException;
 import org.apache.ignite.Ignite;
@@ -31,7 +34,9 @@ import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lang.IgniteBiPredicate;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -111,18 +116,18 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
             .setCriteria(lt("age", MAX_AGE))
             .setFilter(nameFilter);
 
-        check(cache.query(qry), nameFilter);
+        check(qry, nameFilter);
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setCriteria(lt("age", 18))
             .setFilter(nameFilter);
 
-        check(cache.query(qry), (k, v) -> v.age < 18 && nameFilter.apply(k, v));
+        check(qry, (k, v) -> v.age < 18 && nameFilter.apply(k, v));
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setFilter(nameFilter);
 
-        check(cache.query(qry), nameFilter);
+        check(qry, nameFilter);
     }
 
     /** */
@@ -134,7 +139,7 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
             .setCriteria(lt("age", MAX_AGE))
             .setFilter(ageFilter);
 
-        check(cache.query(qry), ageFilter);
+        check(qry, ageFilter);
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setCriteria(lt("age", 18))
@@ -145,7 +150,7 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setFilter(ageFilter);
 
-        check(cache.query(qry), ageFilter);
+        check(qry, ageFilter);
     }
 
     /** */
@@ -157,18 +162,18 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
             .setCriteria(lt("age", MAX_AGE))
             .setFilter(keyFilter);
 
-        check(cache.query(qry), keyFilter);
+        check(qry, keyFilter);
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setCriteria(lt("age", 18))
             .setFilter(keyFilter);
 
-        check(cache.query(qry), (k, v) -> v.age < 18 && keyFilter.apply(k, v));
+        check(qry, (k, v) -> v.age < 18 && keyFilter.apply(k, v));
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setFilter(keyFilter);
 
-        check(cache.query(qry), keyFilter);
+        check(qry, keyFilter);
     }
 
     /** */
@@ -181,12 +186,12 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
             .setCriteria(lt("age", MAX_AGE))
             .setFilter(valFilter);
 
-        check(cache.query(qry), valFilter);
+        check(qry, valFilter);
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setFilter(valFilter);
 
-        check(cache.query(qry), valFilter);
+        check(qry, valFilter);
     }
 
     /** */
@@ -202,7 +207,7 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
             .setCriteria(lt("age", 18))
             .setFilter((k, v) -> true);
 
-        check(cache.query(qry), (k, v) -> v.age < 18);
+        check(qry, (k, v) -> v.age < 18);
 
         qry = new IndexQuery<Integer, Person>(Person.class, idxName)
             .setCriteria(lt("age", MAX_AGE))
@@ -235,38 +240,73 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
     }
 
     /** */
-    private void check(QueryCursor<Cache.Entry<Integer, Person>> cursor, IgniteBiPredicate<Integer, Person> filter) {
-        Map<Integer, Person> expected = persons.entrySet().stream()
-            .filter(e -> filter.apply(e.getKey(), e.getValue()))
-            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    private void check(IndexQuery<Integer, Person> qry, IgniteBiPredicate<Integer, Person> filter) {
+        boolean pk = qry.getIndexName() == null && F.isEmpty(qry.getCriteria());
 
-        List<Cache.Entry<Integer, Person>> all = cursor.getAll();
+        TreeMap<Integer, Set<Person>> expected = pk ? pkPersons(filter) : ageIndexedPersons(filter);
 
-        assertEquals(expected.size(), all.size());
+        List<Cache.Entry<Integer, Person>> all = cache.query(qry).getAll();
 
         for (int i = 0; i < all.size(); i++) {
-            Cache.Entry<Integer, Person> entry = all.get(i);
+            Map.Entry<Integer, Set<Person>> exp = expected.firstEntry();
 
-            Person p = expected.remove(entry.getKey());
+            Cache.Entry<Integer, Person> entry = all.get(i);
 
-            assertNotNull(p);
+            assertTrue(exp.getValue().remove(entry.getValue()));
 
-            assertEquals(p, entry.getValue());
+            if (exp.getValue().isEmpty())
+                expected.remove(exp.getKey());
         }
 
         assertTrue(expected.isEmpty());
     }
 
     /** */
+    private TreeMap<Integer, Set<Person>> ageIndexedPersons(IgniteBiPredicate<Integer, Person> filter) {
+        return persons.entrySet().stream()
+            .filter(e -> filter.apply(e.getKey(), e.getValue()))
+            .collect(TreeMap::new, (m, e) -> {
+                int age = e.getValue().age;
+
+                m.computeIfAbsent(age, a -> new HashSet<>());
+
+                m.get(age).add(e.getValue());
+
+            }, (l, r) -> {
+                r.forEach((k, v) -> {
+                    int age = ((Person)v).age;
+
+                    l.computeIfAbsent(age, a -> new HashSet<>());
+
+                    l.get(age).add((Person)v);
+                });
+            });
+    }
+
+    /** */
+    private TreeMap<Integer, Set<Person>> pkPersons(IgniteBiPredicate<Integer, Person> filter) {
+        return persons.entrySet().stream()
+            .filter(e -> filter.apply(e.getKey(), e.getValue()))
+            .collect(
+                TreeMap::new,
+                (m, e) -> m.put(e.getKey(), new HashSet<>(Collections.singleton(e.getValue()))),
+                TreeMap::putAll
+            );
+    }
+
+    /** */
     private static class Person {
         /** */
+        @GridToStringInclude
         final int id;
 
         /** */
+        @GridToStringInclude
         @QuerySqlField(orderedGroups = @QuerySqlField.Group(name = IDX, order = 0))
         final int age;
 
         /** */
+        @GridToStringInclude
         final String name;
 
         /** */
@@ -293,5 +333,10 @@ public class IndexQueryFilterTest extends GridCommonAbstractTest {
         @Override public int hashCode() {
             return Objects.hash(id, age, name);
         }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Person.class, this);
+        }
     }
 }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryKeepBinaryTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryKeepBinaryTest.java
index e37971a..fa4e7f9 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryKeepBinaryTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryKeepBinaryTest.java
@@ -19,9 +19,7 @@ package org.apache.ignite.cache.query;
 
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
+import java.util.Random;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
@@ -97,6 +95,35 @@ public class IndexQueryKeepBinaryTest extends GridCommonAbstractTest {
         check(cache.withKeepBinary().query(qry), CNT / 4 + 1, CNT / 2);
     }
 
+    /** */
+    @Test
+    public void testComplexSqlPrimaryKey() {
+        String valType = "MY_VALUE_TYPE";
+        String tblCacheName = "MY_TABLE_CACHE";
+
+        SqlFieldsQuery qry = new SqlFieldsQuery("create table my_table (id1 int, id2 int, id3 int," +
+            " PRIMARY KEY(id1, id2)) with \"VALUE_TYPE=" + valType + ",CACHE_NAME=" + tblCacheName + "\";");
+
+        cache.query(qry);
+
+        qry = new SqlFieldsQuery("insert into my_table(id1, id2, id3) values(?, ?, ?);");
+
+        for (int i = 0; i < CNT; i++) {
+            qry.setArgs(i, i, i);
+
+            cache.query(qry);
+        }
+
+        int pivot = new Random().nextInt(CNT);
+
+        IgniteCache<BinaryObject, BinaryObject> tblCache = grid(0).cache(tblCacheName);
+
+        IndexQuery<BinaryObject, BinaryObject> idxQry = new IndexQuery<BinaryObject, BinaryObject>(valType)
+            .setCriteria(lt("id1", pivot));
+
+        checkBinary(tblCache.withKeepBinary().query(idxQry), 0, pivot);
+    }
+
     /**
      * @param left First cache key, inclusive.
      * @param right Last cache key, exclusive.
@@ -106,19 +133,30 @@ public class IndexQueryKeepBinaryTest extends GridCommonAbstractTest {
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
-
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, ?> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
+            assertEquals(left + i, entry.getKey().intValue());
 
             BinaryObject o = all.get(i).getValue();
 
             assertEquals(new Person(entry.getKey().intValue()), o.deserialize());
         }
+    }
+
+    /** */
+    private void checkBinary(QueryCursor cursor, int left, int right) {
+        List<Cache.Entry<BinaryObject, BinaryObject>> all = cursor.getAll();
 
-        assertTrue(expKeys.isEmpty());
+        assertEquals(right - left, all.size());
+
+        for (int i = 0; i < all.size(); i++) {
+            Cache.Entry<BinaryObject, BinaryObject> entry = all.get(i);
+
+            assertEquals(left + i, (int)entry.getKey().field("id1"));
+            assertEquals(left + i, (int)entry.getKey().field("id2"));
+            assertEquals(left + i, (int)entry.getValue().field("id3"));
+        }
     }
 
     /** */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryQueryEntityTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryQueryEntityTest.java
index 7bc90f7..d8db058 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryQueryEntityTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryQueryEntityTest.java
@@ -23,9 +23,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
@@ -180,13 +177,13 @@ public class IndexQueryQueryEntityTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", pivot));
 
-        check(cache.query(qry), 0, pivot);
+        check(cache.query(qry), 0, pivot, false);
 
         // Lt, desc index.
         IndexQuery<Long, Person> descQry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
             .setCriteria(lt("descId", pivot));
 
-        check(cache.query(descQry), 0, pivot);
+        check(cache.query(descQry), 0, pivot, true);
     }
 
     /** */
@@ -200,30 +197,30 @@ public class IndexQueryQueryEntityTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", pivot));
 
-        check(cacheTblName.query(qry), 0, pivot);
+        check(cacheTblName.query(qry), 0, pivot, false);
 
         // Lt, desc index.
         IndexQuery<Long, Person> descQry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
             .setCriteria(lt("descId", pivot));
 
-        check(cacheTblName.query(descQry), 0, pivot);
+        check(cacheTblName.query(descQry), 0, pivot, true);
     }
 
     /**
      * @param left First cache key, inclusive.
      * @param right Last cache key, exclusive.
      */
-    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right) {
+    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right, boolean desc) {
         List<Cache.Entry<Long, Person>> all = cursor.getAll();
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
-
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, Person> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
+            int exp = desc ? right - i - 1 : left + i;
+
+            assertEquals(exp, entry.getKey().intValue());
 
             assertEquals(new Person(entry.getKey().intValue()), all.get(i).getValue());
         }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryRangeTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryRangeTest.java
index 31dcfa7..aca4efd 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryRangeTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQueryRangeTest.java
@@ -17,14 +17,17 @@
 
 package org.apache.ignite.cache.query;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntUnaryOperator;
+import java.util.stream.Stream;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
@@ -35,6 +38,7 @@ import org.apache.ignite.cache.CacheWriteSynchronizationMode;
 import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,9 +74,6 @@ public class IndexQueryRangeTest extends GridCommonAbstractTest {
     private Ignite crd;
 
     /** */
-    private int duplicates;
-
-    /** */
     private IgniteCache<Long, Person> cache;
 
     /** */
@@ -96,40 +97,31 @@ public class IndexQueryRangeTest extends GridCommonAbstractTest {
     public int backups;
 
     /** */
-    @Parameterized.Parameters(name = "qryPar={0} atomicity={1} mode={2} node={3} backups={4}")
+    @Parameterized.Parameter(5)
+    public String idxName;
+
+    /** Number of duplicates of indexed value. */
+    @Parameterized.Parameter(6)
+    public int duplicates;
+
+    /** */
+    @Parameterized.Parameters(name = "qryPar={0} atomicity={1} mode={2} node={3} backups={4} idxName={5} duplicates={6}")
     public static Collection<Object[]> testParams() {
-        return Arrays.asList(
-            new Object[] {1, TRANSACTIONAL, REPLICATED, "CRD", 0},
-            new Object[] {1, TRANSACTIONAL, PARTITIONED, "CRD", 0},
-            new Object[] {4, TRANSACTIONAL, PARTITIONED, "CRD", 0},
-
-            new Object[] {1, ATOMIC, REPLICATED, "CRD", 0},
-            new Object[] {1, ATOMIC, PARTITIONED, "CRD", 0},
-            new Object[] {4, ATOMIC, PARTITIONED, "CRD", 0},
-
-            new Object[] {1, TRANSACTIONAL, REPLICATED, "CLN", 0},
-            new Object[] {1, TRANSACTIONAL, PARTITIONED, "CLN", 0},
-            new Object[] {4, TRANSACTIONAL, PARTITIONED, "CLN", 0},
-
-            new Object[] {1, ATOMIC, REPLICATED, "CLN", 0},
-            new Object[] {1, ATOMIC, PARTITIONED, "CLN", 0},
-            new Object[] {4, ATOMIC, PARTITIONED, "CLN", 0},
-
-            new Object[] {1, TRANSACTIONAL, REPLICATED, "CRD", 2},
-            new Object[] {1, TRANSACTIONAL, PARTITIONED, "CRD", 2},
-            new Object[] {4, TRANSACTIONAL, PARTITIONED, "CRD", 2},
-
-            new Object[] {1, ATOMIC, REPLICATED, "CRD", 2},
-            new Object[] {1, ATOMIC, PARTITIONED, "CRD", 2},
-            new Object[] {4, ATOMIC, PARTITIONED, "CRD", 2},
-
-            new Object[] {1, TRANSACTIONAL, REPLICATED, "CLN", 2},
-            new Object[] {1, TRANSACTIONAL, PARTITIONED, "CLN", 2},
-            new Object[] {4, TRANSACTIONAL, PARTITIONED, "CLN", 2},
-
-            new Object[] {1, ATOMIC, REPLICATED, "CLN", 2},
-            new Object[] {1, ATOMIC, PARTITIONED, "CLN", 2},
-            new Object[] {4, ATOMIC, PARTITIONED, "CLN", 2});
+        List<Object[]> params = new ArrayList<>();
+
+        Stream.of("CRD", "CLN").forEach(node ->
+            Stream.of(0, 2).forEach(backups ->
+                Stream.of(1, 10).forEach(duplicates ->
+                    Stream.of(IDX, DESC_IDX).forEach(idx -> {
+                        params.add(new Object[] {1, TRANSACTIONAL, REPLICATED, node, backups, idx, duplicates});
+                        params.add(new Object[] {1, TRANSACTIONAL, PARTITIONED, node, backups, idx, duplicates});
+                        params.add(new Object[] {4, TRANSACTIONAL, PARTITIONED, node, backups, idx, duplicates});
+                    })
+                )
+            )
+        );
+
+        return params;
     }
 
     /** {@inheritDoc} */
@@ -172,174 +164,104 @@ public class IndexQueryRangeTest extends GridCommonAbstractTest {
 
     /** */
     @Test
-    public void testRangeQueries() {
-        duplicates = 1;
-
-        checkRangeQueries();
-    }
-
-    /** */
-    @Test
-    public void testRangeDescQueries() {
-        duplicates = 1;
-
-        checkRangeDescQueries();
-    }
-
-    /** */
-    @Test
-    public void testRangeQueriesWithDuplicatedData() {
-        duplicates = 10;
-
-        checkRangeQueries();
-    }
-
-    /** */
-    @Test
-    public void testRangeDescQueriesWithDuplicatedData() {
-        duplicates = 10;
-
-        checkRangeDescQueries();
-    }
-
-    /** */
-    public void checkRangeQueries() {
+    public void testRangeQueries() throws Exception {
         // Query empty cache.
-        IndexQuery<Long, Person> qry = new IndexQuery<>(Person.class, IDX);
+        IndexQuery<Long, Person> qry = new IndexQuery<>(Person.class, idxName);
 
         assertTrue(cache.query(qry).getAll().isEmpty());
 
         // Add data
         insertData();
 
-        qry = new IndexQuery<>(Person.class, IDX);
+        qry = new IndexQuery<>(Person.class, idxName);
 
-        check(cache.query(qry), 0, CNT);
+        check(qry, 0, CNT);
 
         // Range queries.
+        String fld = idxName.equals(IDX) ? "id" : "descId";
+
         int pivot = new Random().nextInt(CNT);
 
         // Eq.
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(eq("id", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq(fld, pivot));
 
-        check(cache.query(qry), pivot, pivot + 1);
+        check(qry, pivot, pivot + 1);
 
         // Lt.
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(lt("id", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lt(fld, pivot));
 
-        check(cache.query(qry), 0, pivot);
+        check(qry, 0, pivot);
 
         // Lte.
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(lte("id", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lte(fld, pivot));
 
-        check(cache.query(qry), 0, pivot + 1);
+        check(qry, 0, pivot + 1);
 
         // Gt.
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(gt("id", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gt(fld, pivot));
 
-        check(cache.query(qry), pivot + 1, CNT);
+        check(qry, pivot + 1, CNT);
 
         // Gte.
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(gte("id", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gte(fld, pivot));
 
-        check(cache.query(qry), pivot, CNT);
+        check(qry, pivot, CNT);
 
         // Between.
         int lower = new Random().nextInt(CNT / 2);
         int upper = lower + CNT / 20;
 
-        qry = new IndexQuery<Long, Person>(Person.class, IDX)
-            .setCriteria(between("id", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between(fld, lower, upper));
 
-        check(cache.query(qry), lower, upper + 1);
-    }
-
-    /** */
-    public void checkRangeDescQueries() {
-        // Query empty cache.
-        IndexQuery<Long, Person> qry = new IndexQuery<>(Person.class, DESC_IDX);
-
-        assertTrue(cache.query(qry).getAll().isEmpty());
-
-        // Add data
-        insertData();
-
-        qry = new IndexQuery<>(Person.class, DESC_IDX);
-
-        check(cache.query(qry), 0, CNT);
-
-        // Range queries.
-        int pivot = new Random().nextInt(CNT);
-
-        // Eq.
-        qry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(eq("descId", pivot));
-
-        check(cache.query(qry), pivot, pivot + 1);
-
-        // Lt, desc index.
-        IndexQuery<Long, Person> descQry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(lt("descId", pivot));
-
-        check(cache.query(descQry), 0, pivot);
-
-        // Lte, desc index.
-        descQry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(lte("descId", pivot));
-
-        check(cache.query(descQry), 0, pivot + 1);
-
-        // Gt, desc index.
-        descQry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(gt("descId", pivot));
-
-        check(cache.query(descQry), pivot + 1, CNT);
-
-        // Gte, desc index.
-        descQry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(gte("descId", pivot));
-
-        check(cache.query(descQry), pivot, CNT);
-
-        // Between, desc index.
-        int lower = new Random().nextInt(CNT / 2);
-        int upper = lower + CNT / 20;
-
-        descQry = new IndexQuery<Long, Person>(Person.class, DESC_IDX)
-            .setCriteria(between("descId", lower, upper));
-
-        check(cache.query(descQry), lower, upper + 1);
+        check(qry, lower, upper + 1);
     }
 
     /**
      * @param left First cache key, inclusive.
      * @param right Last cache key, exclusive.
      */
-    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right) {
-        List<Cache.Entry<Long, Person>> all = cursor.getAll();
+    private void check(Query<Cache.Entry<Long, Person>> qry, int left, int right) throws Exception {
+        QueryCursor<Cache.Entry<Long, Person>> cursor = cache.query(qry);
 
-        assertFalse(all.isEmpty());
+        int expSize = (right - left) * duplicates;
 
-        assertEquals((right - left) * duplicates, all.size());
+        Set<Long> expKeys = new HashSet<>(expSize);
+        List<Integer> expOrderedValues = new LinkedList<>();
 
-        Set<Long> expKeys = LongStream
-            .range((long) left * duplicates, (long) right * duplicates).boxed()
-            .collect(Collectors.toSet());
+        boolean desc = idxName.equals(DESC_IDX);
 
-        for (int i = 0; i < all.size(); i++) {
-            Cache.Entry<Long, Person> entry = all.get(i);
+        int from = desc ? right - 1 : left;
+        int to = desc ? left - 1 : right;
+        IntUnaryOperator op = (i) -> desc ? i - 1 : i + 1;
+
+        for (int i = from; i != to; i = op.applyAsInt(i)) {
+            for (int j = 0; j < duplicates; j++) {
+                expOrderedValues.add(i);
+                expKeys.add((long) CNT * j + i);
+            }
+        }
+
+        AtomicInteger actSize = new AtomicInteger();
+
+        ((QueryCursorEx<Cache.Entry<Long, Person>>)cursor).getAll(entry -> {
+            assertEquals(expOrderedValues.remove(0).intValue(), entry.getValue().id);
 
             assertTrue(expKeys.remove(entry.getKey()));
 
-            int persId = entry.getKey().intValue() / duplicates;
+            int persId = entry.getKey().intValue() % CNT;
 
-            assertEquals(new Person(persId), all.get(i).getValue());
-        }
+            assertEquals(new Person(persId), entry.getValue());
+
+            actSize.incrementAndGet();
+        });
+
+        assertEquals(expSize, actSize.get());
 
         assertTrue(expKeys.isEmpty());
     }
@@ -350,7 +272,7 @@ public class IndexQueryRangeTest extends GridCommonAbstractTest {
             for (int persId = 0; persId < CNT; persId++) {
                 // Create duplicates of data.
                 for (int i = 0; i < duplicates; i++)
-                    streamer.addData((long) persId * duplicates + i, new Person(persId));
+                    streamer.addData((long) CNT * i + persId, new Person(persId));
             }
         }
     }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQuerySqlIndexTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQuerySqlIndexTest.java
index 1825f86..54fb289 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQuerySqlIndexTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/IndexQuerySqlIndexTest.java
@@ -22,15 +22,13 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
@@ -83,7 +81,7 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
 
     /** {@inheritDoc} */
     @Override protected void beforeTest() throws Exception {
-        crd = startGrids(1);
+        crd = startGrids(2);
 
         cache = crd.createCache(new CacheConfiguration<>().setName(CACHE));
     }
@@ -150,16 +148,16 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class.getName(), qryDescIdxName)
             .setCriteria(lt("descId", pivot));
 
-        check(tblCache.query(qry), 0, pivot);
+        check(qry, 0, pivot);
 
         qry = new IndexQuery<Long, Person>(Person.class.getName(), qryDescIdxName)
             .setCriteria(lt("DESCID", pivot));
 
-        check(tblCache.query(qry), 0, pivot);
+        check(qry, 0, pivot);
 
         qry = new IndexQuery<>(Person.class.getName(), qryDescIdxName);
 
-        check(tblCache.query(qry), 0, CNT);
+        check(qry, 0, CNT);
     }
 
     /** Should support only original field. */
@@ -174,7 +172,7 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class.getName(), qryDescIdxName)
             .setCriteria(lt("descId", pivot));
 
-        check(tblCache.query(qry), 0, pivot);
+        check(qry, 0, pivot);
 
         String errMsg = qryDescIdxName != null ? "Index doesn't match criteria." : "No index found for criteria.";
 
@@ -201,7 +199,7 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class.getName(), idx)
             .setCriteria(lt("descId", pivot));
 
-        check(tblCache.query(qry), 0, pivot);
+        check(qry, 0, pivot);
 
         if (qryDescIdxName != null) {
             GridTestUtils.assertThrowsAnyCause(null, () -> {
@@ -256,24 +254,26 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class.getName(), qryDescIdxName)
             .setCriteria(eq("_KEY", (long) pivot), lte("descId", pivot));
 
-        check(tblCache.query(qry), pivot, pivot + 1);
+        check(qry, pivot, pivot + 1);
     }
 
     /**
      * @param left First cache key, inclusive.
      * @param right Last cache key, exclusive.
      */
-    private void check(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right) {
-        List<Cache.Entry<Long, Person>> all = cursor.getAll();
+    private void check(IndexQuery<Long, Person> qry, int left, int right) {
+        boolean pk = qry.getIndexName() == null && F.isEmpty(qry.getCriteria());
 
-        assertEquals(right - left, all.size());
+        List<Cache.Entry<Long, Person>> all = tblCache.query(qry).getAll();
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
+        assertEquals(right - left, all.size());
 
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, Person> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
+            int exp = pk ? left + i : right - i - 1;
+
+            assertEquals(exp, entry.getKey().intValue());
 
             assertEquals(new Person(entry.getKey().intValue()), all.get(i).getValue());
         }
@@ -288,13 +288,10 @@ public class IndexQuerySqlIndexTest extends GridCommonAbstractTest {
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
-
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, BinaryObject> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
-
+            assertEquals(right - 1 - i, entry.getKey().intValue());
             assertEquals(entry.getKey().intValue(), (int) entry.getValue().field("id"));
             assertEquals(entry.getKey().intValue(), (int) entry.getValue().field("descId"));
         }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultiTableIndexQuery.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultiTableIndexQuery.java
index 20c7db4..3dd7578 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultiTableIndexQuery.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultiTableIndexQuery.java
@@ -260,12 +260,10 @@ public class MultiTableIndexQuery extends GridCommonAbstractTest {
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
-
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, SecondPerson> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
+            assertEquals(left + i, entry.getKey().intValue());
 
             assertEquals(new SecondPerson(entry.getKey().intValue()), all.get(i).getValue());
         }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
index 931fffb..3064414 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/MultifieldIndexQueryTest.java
@@ -132,7 +132,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryKeyPKIdx)
             .setCriteria(lt("_KEY", (long) pivot));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, false);
     }
 
     /** */
@@ -190,7 +190,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", 1));
 
-        checkPerson(cache.query(qry), 0, CNT);
+        checkPerson(qry, 0, CNT, false);
 
         // Checks the same with query for DESC_IDX.
         qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
@@ -201,7 +201,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
             .setCriteria(lt("id", 1));
 
-        checkPerson(cache.query(qry), 0, CNT);
+        checkPerson(qry, 0, CNT, qryDescIdx != null);
     }
 
     /** */
@@ -221,7 +221,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", 1), lt("secId", CNT));
 
-        checkPerson(cache.query(qry), 0, CNT);
+        checkPerson(qry, 0, CNT, false);
 
         // Should return part of data, as ID equals to inserted data ID field.
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
@@ -233,7 +233,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", 1), lt("secId", pivot));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, false);
     }
 
     /** */
@@ -253,7 +253,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("secId", CNT), lt("id", 1));
 
-        checkPerson(cache.query(qry), 0, CNT);
+        checkPerson(qry, 0, CNT, false);
 
         // Should return part of data, as ID equals to inserted data ID field.
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
@@ -265,264 +265,145 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("secId", pivot), lt("id", 1));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, false);
     }
 
     /** */
     @Test
-    public void testLegalDifferentCriteria() {
+    public void testLegalDifferentCriteriaAscIndex() {
+        testLegalDifferentCriteria(qryIdx, "secId", false);
+    }
+
+    /** */
+    @Test
+    public void testLegalDifferentCriteriaWithDescIdx() {
+        testLegalDifferentCriteria(qryDescIdx, "descId", true);
+    }
+
+    /** */
+    public void testLegalDifferentCriteria(String idxName, String fldName, boolean desc) {
         insertData();
 
         int pivot = new Random().nextInt(CNT);
 
         // Eq as first criterion.
-        IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(eq("id", 1), lt("secId", pivot));
+        IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq("id", 1), lt(fldName, pivot));
 
         assertTrue(cache.query(qry).getAll().isEmpty());
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(eq("id", 0), lte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq("id", 0), lte(fldName, pivot));
 
-        checkPerson(cache.query(qry), 0, pivot + 1);
+        checkPerson(qry, 0, pivot + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(eq("id", 0), gt("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq("id", 0), gt(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot + 1, CNT);
+        checkPerson(qry, pivot + 1, CNT, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(eq("id", 0), gte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq("id", 0), gte(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, CNT);
+        checkPerson(qry, pivot, CNT, desc);
 
         int lower = new Random().nextInt(CNT / 2);
         int upper = lower + new Random().nextInt(CNT / 2);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(eq("id", 0), between("secId", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(eq("id", 0), between(fldName, lower, upper));
 
-        checkPerson(cache.query(qry), lower, upper + 1);
+        checkPerson(qry, lower, upper + 1, desc);
 
         // Lt as first criterion.
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lt("id", 1), lte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lt("id", 1), lte(fldName, pivot));
 
-        checkPerson(cache.query(qry), 0, pivot + 1);
+        checkPerson(qry, 0, pivot + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lt("id", 1), eq("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lt("id", 1), eq(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, pivot + 1);
+        checkPerson(qry, pivot, pivot + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lt("id", 1), between("secId", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lt("id", 1), between(fldName, lower, upper));
 
-        checkPerson(cache.query(qry), lower, upper + 1);
+        checkPerson(qry, lower, upper + 1, desc);
 
         // Lte as first criterion.
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lte("id", 0), lt("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lte("id", 0), lt(fldName, pivot));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lte("id", 1), between("secId", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lte("id", 1), between(fldName, lower, upper));
 
-        checkPerson(cache.query(qry), lower, upper + 1);
+        checkPerson(qry, lower, upper + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(lte("id", 0), eq("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(lte("id", 0), eq(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, pivot + 1);
+        checkPerson(qry, pivot, pivot + 1, desc);
 
         // Gt as first criterion.
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gt("id", -1), gte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gt("id", -1), gte(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, CNT);
+        checkPerson(qry, pivot, CNT, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gt("id", -1), eq("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gt("id", -1), eq(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, pivot + 1);
+        checkPerson(qry, pivot, pivot + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gt("id", -1), between("secId", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gt("id", -1), between(fldName, lower, upper));
 
-        checkPerson(cache.query(qry), lower, upper + 1);
+        checkPerson(qry, lower, upper + 1, desc);
 
         // Gte as first criterion.
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gte("id", 0), gt("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gte("id", 0), gt(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot + 1, CNT);
+        checkPerson(qry, pivot + 1, CNT, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gte("id", 0), between("secId", lower, upper));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gte("id", 0), between(fldName, lower, upper));
 
-        checkPerson(cache.query(qry), lower, upper + 1);
+        checkPerson(qry, lower, upper + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(gte("id", 0), eq("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(gte("id", 0), eq(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, pivot + 1);
+        checkPerson(qry, pivot, pivot + 1, desc);
 
         // Between as first criterion.
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(between("id", -1, 1), lt("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between("id", -1, 1), lt(fldName, pivot));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(between("id", -1, 1), lte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between("id", -1, 1), lte(fldName, pivot));
 
-        checkPerson(cache.query(qry), 0, pivot + 1);
+        checkPerson(qry, 0, pivot + 1, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(between("id", -1, 1), gt("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between("id", -1, 1), gt(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot + 1, CNT);
+        checkPerson(qry, pivot + 1, CNT, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(between("id", -1, 1), gte("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between("id", -1, 1), gte(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, CNT);
+        checkPerson(qry, pivot, CNT, desc);
 
-        qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
-            .setCriteria(between("id", -1, 1), eq("secId", pivot));
+        qry = new IndexQuery<Long, Person>(Person.class, idxName)
+            .setCriteria(between("id", -1, 1), eq(fldName, pivot));
 
-        checkPerson(cache.query(qry), pivot, pivot + 1);
-    }
-
-    /** */
-    @Test
-    public void testLegalDifferentCriteriaWithDescIdx() {
-        insertData();
-
-        int pivot = new Random().nextInt(CNT);
-        int lower = new Random().nextInt(CNT / 2);
-        int upper = lower + new Random().nextInt(CNT / 2);
-
-        // Eq as first criteria.
-        IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 1), lt("descId", pivot));
-
-        assertTrue(cache.query(qry).getAll().isEmpty());
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 0), lt("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 0), lte("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 0), gt("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot + 1, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 0), gte("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(eq("id", 0), between("descId", lower, upper));
-
-        checkPerson(cache.query(qry), lower, upper + 1);
-
-        // Lt as first criteria.
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lt("id", 1), gt("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot + 1, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lt("id", 1), gte("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lt("id", 1), between("descId", lower, upper));
-
-        checkPerson(cache.query(qry), lower, upper + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lt("id", 1), eq("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, pivot + 1);
-
-        // Lte as first criteria.
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lte("id", 0), gt("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot + 1, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lte("id", 0), gte("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lte("id", 0), between("descId", lower, upper));
-
-        checkPerson(cache.query(qry), lower, upper + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(lte("id", 0), eq("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, pivot + 1);
-
-        // Gte as first criteria.
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(gte("id", 0), lt("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(gte("id", 0), lte("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(gte("id", 0), between("descId", lower, upper));
-
-        checkPerson(cache.query(qry), lower, upper + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(gte("id", 0), eq("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, pivot + 1);
-
-        // Between as first criteria.
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(between("id", -1, 1), lt("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(between("id", -1, 1), lte("descId", pivot));
-
-        checkPerson(cache.query(qry), 0, pivot + 1);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(between("id", -1, 1), gt("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot + 1, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(between("id", -1, 1), gte("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, CNT);
-
-        qry = new IndexQuery<Long, Person>(Person.class, qryDescIdx)
-            .setCriteria(between("id", -1, 1), eq("descId", pivot));
-
-        checkPerson(cache.query(qry), pivot, pivot + 1);
+        checkPerson(qry, pivot, pivot + 1, desc);
     }
 
     /** */
@@ -535,12 +416,12 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", 1), gt("secId", pivot));
 
-        checkPerson(cache.query(qry), pivot + 1, CNT);
+        checkPerson(qry, pivot + 1, CNT, false);
 
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(lt("id", 1), gte("secId", pivot));
 
-        checkPerson(cache.query(qry), pivot, CNT);
+        checkPerson(qry, pivot, CNT, false);
 
         qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(gt("id", 2), lt("secId", pivot));
@@ -579,7 +460,7 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
         IndexQuery<Long, Person> qry = new IndexQuery<Long, Person>(Person.class, qryIdx)
             .setCriteria(eq("id", 0), lt("secId", pivot), lt("_KEY", (long) pivot));
 
-        checkPerson(cache.query(qry), 0, pivot);
+        checkPerson(qry, 0, pivot, false);
     }
 
     /** */
@@ -591,17 +472,25 @@ public class MultifieldIndexQueryTest extends GridCommonAbstractTest {
     }
 
     /** */
-    private void checkPerson(QueryCursor<Cache.Entry<Long, Person>> cursor, int left, int right) {
-        List<Cache.Entry<Long, Person>> all = cursor.getAll();
+    private void checkPerson(IndexQuery<Long, Person> qry, int left, int right, boolean desc) {
+        boolean fullSort = qry.getCriteria().size() == 2;
+
+        List<Cache.Entry<Long, Person>> all = cache.query(qry).getAll();
 
         assertEquals(right - left, all.size());
 
-        Set<Long> expKeys = LongStream.range(left, right).boxed().collect(Collectors.toSet());
+        Set<Long> expKeys = fullSort ? null : LongStream.range(left, right).boxed().collect(Collectors.toSet());
 
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Long, Person> entry = all.get(i);
 
-            assertTrue(expKeys.remove(entry.getKey()));
+            if (fullSort) {
+                int exp = desc ? right - 1 - i : left + i;
+
+                assertEquals(exp, entry.getKey().intValue());
+            }
+            else
+                assertTrue(expKeys.remove(entry.getKey()));
 
             assertEquals(new Person(entry.getKey().intValue()), all.get(i).getValue());
         }
diff --git a/modules/indexing/src/test/java/org/apache/ignite/cache/query/RepeatedFieldIndexQueryTest.java b/modules/indexing/src/test/java/org/apache/ignite/cache/query/RepeatedFieldIndexQueryTest.java
index fff71a2..ce39736 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/cache/query/RepeatedFieldIndexQueryTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/cache/query/RepeatedFieldIndexQueryTest.java
@@ -19,8 +19,8 @@ package org.apache.ignite.cache.query;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Random;
-import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import javax.cache.Cache;
@@ -232,15 +232,15 @@ public class RepeatedFieldIndexQueryTest extends GridCommonAbstractTest {
 
         assertEquals(errMsg, right - left, all.size());
 
-        Set<Integer> expKeys = IntStream.range(left, right).boxed().collect(Collectors.toSet());
+        boolean desc = Objects.equals(idxName, DESC_ID_IDX);
 
         for (int i = 0; i < all.size(); i++) {
             Cache.Entry<Integer, Person> entry = all.get(i);
 
-            assertTrue(errMsg, expKeys.remove(entry.getKey()));
-        }
+            int exp = desc ? right - 1 - i : left + i;
 
-        assertTrue(errMsg, expKeys.isEmpty());
+            assertEquals(errMsg, exp, entry.getKey().intValue());
+        }
     }
 
     /** */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheFullTextQueryPagesTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheFullTextQueryPagesTest.java
index ad0354d..1b91f77 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheFullTextQueryPagesTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/GridCacheFullTextQueryPagesTest.java
@@ -90,7 +90,8 @@ public class GridCacheFullTextQueryPagesTest extends GridCacheFullTextQueryAbstr
     public void testTextQueryMultiplePagesNoLimit() {
         checkTextQuery("1*", 0, PAGE_SIZE);
 
-        checkPages(4, 8, 0);
+        // Send 2 additional load requests on each node.
+        checkPages(NODES, NODES * 2, 0);
     }
 
     /** Test that do not send cache page request after limit exceeded. */
@@ -98,7 +99,9 @@ public class GridCacheFullTextQueryPagesTest extends GridCacheFullTextQueryAbstr
     public void testTextQueryLimitedMultiplePages() {
         checkTextQuery("1*", QUERY_LIMIT, 30);
 
-        checkPages(4, 7, 3);
+        // We hold 2 pre-loaded pages per node. Then we send additional load request for every node on beginning,
+        // and 2 more while iterating over data and finish preloaded pages (depends on page size).
+        checkPages(NODES, NODES + 2, NODES);
     }
 
     /** Test that rerequest some pages but then send a cancel query after limit exceeded. */
@@ -106,7 +109,9 @@ public class GridCacheFullTextQueryPagesTest extends GridCacheFullTextQueryAbstr
     public void testTextQueryHighLimitedMultiplePages() {
         checkTextQuery("1*", QUERY_LIMIT, 20);
 
-        checkPages(4, 8, 3);
+        // We hold 2 pre-loaded pages per node. Then we send additional load request for every node on beginning,
+        // and 4 more while iterating over data and finish preloaded pages (depends on page size).
+        checkPages(NODES, NODES + 4, NODES);
     }
 
     /** */