You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by tl...@apache.org on 2020/08/25 09:04:40 UTC

[ignite] branch master updated: IGNITE-13280 fix improper index usage (#8067)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 31a45bf  IGNITE-13280 fix improper index usage (#8067)
31a45bf is described below

commit 31a45bf23fcf5384364f3c931a24d282954af19a
Author: tledkov <tl...@gridgain.com>
AuthorDate: Tue Aug 25 12:04:18 2020 +0300

    IGNITE-13280 fix improper index usage (#8067)
---
 .../query/h2/database/H2TreeIndexBase.java         |   2 +-
 .../processors/query/h2/opt/GridH2IndexBase.java   |   5 +-
 .../processors/query/h2/opt/GridH2ProxyIndex.java  |   8 +-
 .../processors/query/h2/opt/H2IndexCostedBase.java | 234 +++++++++++++++++++++
 .../processors/cache/index/BasicIndexTest.java     | 146 +++++++++++--
 .../query/h2/IgniteSqlQueryMinMaxTest.java         |   5 +-
 6 files changed, 369 insertions(+), 31 deletions(-)

diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndexBase.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndexBase.java
index db07b96..53e93eb 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndexBase.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/database/H2TreeIndexBase.java
@@ -63,7 +63,7 @@ public abstract class H2TreeIndexBase extends GridH2IndexBase {
         HashSet<Column> allColumnsSet) {
         long rowCnt = getRowCountApproximation();
 
-        double baseCost = getCostRangeIndex(masks, rowCnt, filters, filter, sortOrder, false, allColumnsSet);
+        double baseCost = getCostRangeIndexEx(masks, rowCnt, filters, filter, sortOrder, false, allColumnsSet);
 
         int mul = getDistributedMultiplier(ses, filters, filter);
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2IndexBase.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2IndexBase.java
index 8110c47..ad7e391 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2IndexBase.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2IndexBase.java
@@ -26,7 +26,6 @@ import org.apache.ignite.internal.processors.query.h2.opt.join.CollocationModel;
 import org.apache.ignite.internal.processors.query.h2.opt.join.CollocationModelMultiplier;
 import org.apache.ignite.spi.indexing.IndexingQueryCacheFilter;
 import org.h2.engine.Session;
-import org.h2.index.BaseIndex;
 import org.h2.index.IndexType;
 import org.h2.message.DbException;
 import org.h2.result.Row;
@@ -39,7 +38,7 @@ import org.jetbrains.annotations.NotNull;
 /**
  * Index base.
  */
-public abstract class GridH2IndexBase extends BaseIndex {
+public abstract class GridH2IndexBase extends H2IndexCostedBase {
     /**
      * Constructor.
      *
@@ -49,7 +48,7 @@ public abstract class GridH2IndexBase extends BaseIndex {
      * @param type Index type.
      */
     protected GridH2IndexBase(GridH2Table tbl, String name, IndexColumn[] cols, IndexType type) {
-        initBaseIndex(tbl, 0, name, cols, type);
+        super(tbl, name, cols, type);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ProxyIndex.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ProxyIndex.java
index cbe58dd..5174277 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ProxyIndex.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ProxyIndex.java
@@ -21,7 +21,6 @@ import java.util.HashSet;
 import java.util.List;
 import org.apache.ignite.internal.processors.query.h2.opt.join.ProxyDistributedLookupBatch;
 import org.h2.engine.Session;
-import org.h2.index.BaseIndex;
 import org.h2.index.Cursor;
 import org.h2.index.Index;
 import org.h2.index.IndexLookupBatch;
@@ -39,8 +38,7 @@ import org.h2.table.TableFilter;
  * Allows to have 'free' index for alias columns
  * Delegates the calls to underlying normal index
  */
-public class GridH2ProxyIndex extends BaseIndex {
-
+public class GridH2ProxyIndex extends H2IndexCostedBase {
     /** Underlying normal index */
     protected Index idx;
 
@@ -55,6 +53,8 @@ public class GridH2ProxyIndex extends BaseIndex {
                             String name,
                             List<IndexColumn> colsList,
                             Index idx) {
+        super(tbl, name, GridH2IndexBase.columnsArray(tbl, colsList),
+            IndexType.createNonUnique(false, false, idx instanceof SpatialIndex));
 
         IndexColumn[] cols = colsList.toArray(new IndexColumn[colsList.size()]);
 
@@ -102,7 +102,7 @@ public class GridH2ProxyIndex extends BaseIndex {
     @Override public double getCost(Session session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, HashSet<Column> allColumnsSet) {
         long rowCnt = getRowCountApproximation();
 
-        double baseCost = getCostRangeIndex(masks, rowCnt, filters, filter, sortOrder, false, allColumnsSet);
+        double baseCost = getCostRangeIndexEx(masks, rowCnt, filters, filter, sortOrder, false, allColumnsSet);
 
         int mul = ((GridH2IndexBase)idx).getDistributedMultiplier(session, filters, filter);
 
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/H2IndexCostedBase.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/H2IndexCostedBase.java
new file mode 100644
index 0000000..b6e7ba6
--- /dev/null
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/H2IndexCostedBase.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.h2.opt;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import org.apache.ignite.internal.util.typedef.F;
+import org.h2.engine.Constants;
+import org.h2.index.BaseIndex;
+import org.h2.index.IndexCondition;
+import org.h2.index.IndexType;
+import org.h2.result.SortOrder;
+import org.h2.table.Column;
+import org.h2.table.IndexColumn;
+import org.h2.table.TableFilter;
+
+/**
+ * Index base.
+ */
+public abstract class H2IndexCostedBase extends BaseIndex {
+    /**
+     * Constructor.
+     *
+     * @param tbl Table.
+     * @param name Index name.
+     * @param cols Indexed columns.
+     * @param type Index type.
+     */
+    protected H2IndexCostedBase(GridH2Table tbl, String name, IndexColumn[] cols, IndexType type) {
+        initBaseIndex(tbl, 0, name, cols, type);
+    }
+
+    /**
+     * Re-implement {@link BaseIndex#getCostRangeIndex} to support  compatibility with old version.
+     */
+    protected long getCostRangeIndexEx(int[] masks, long rowCount,
+                                       TableFilter[] filters, int filter, SortOrder sortOrder,
+                                       boolean isScanIndex, HashSet<Column> allColumnsSet) {
+        rowCount += Constants.COST_ROW_OFFSET;
+
+        int totalSelectivity = 0;
+
+        long rowsCost = rowCount;
+
+        if (masks != null) {
+            int i = 0, len = columns.length;
+
+            while (i < len) {
+                Column column = columns[i++];
+
+                int index = column.getColumnId();
+                int mask = masks[index];
+
+                if ((mask & IndexCondition.EQUALITY) == IndexCondition.EQUALITY) {
+                    if (i == len && getIndexType().isUnique()) {
+                        rowsCost = 3;
+
+                        break;
+                    }
+
+                    totalSelectivity = 100 - ((100 - totalSelectivity) *
+                        (100 - column.getSelectivity()) / 100);
+
+                    long distinctRows = rowCount * totalSelectivity / 100;
+
+                    if (distinctRows <= 0)
+                        distinctRows = 1;
+
+                    rowsCost = Math.min(5 + Math.max(rowsCost / distinctRows, 1), rowsCost - (i > 0 ? 1 : 0));
+                }
+                else if ((mask & IndexCondition.RANGE) == IndexCondition.RANGE) {
+                    rowsCost = Math.min(5 + rowsCost / 4, rowsCost - (i > 0 ? 1 : 0));
+
+                    break;
+                }
+                else if ((mask & IndexCondition.START) == IndexCondition.START) {
+                    rowsCost = Math.min(5 + rowsCost / 3, rowsCost - (i > 0 ? 1 : 0));
+
+                    break;
+                }
+                else if ((mask & IndexCondition.END) == IndexCondition.END) {
+                    rowsCost = Math.min(rowsCost / 3, rowsCost - (i > 0 ? 1 : 0));
+
+                    break;
+                }
+                else
+                    break;
+            }
+        }
+
+        // If the ORDER BY clause matches the ordering of this index,
+        // it will be cheaper than another index, so adjust the cost
+        // accordingly.
+        long sortingCost = 0;
+
+        if (sortOrder != null)
+            sortingCost = 100 + rowCount / 10;
+
+        if (sortOrder != null && !isScanIndex) {
+            boolean sortOrderMatches = true;
+            int coveringCount = 0;
+            int[] sortTypes = sortOrder.getSortTypes();
+
+            TableFilter tableFilter = filters == null ? null : filters[filter];
+
+            for (int i = 0, len = sortTypes.length; i < len; i++) {
+                if (i >= indexColumns.length) {
+                    // We can still use this index if we are sorting by more
+                    // than it's columns, it's just that the coveringCount
+                    // is lower than with an index that contains
+                    // more of the order by columns.
+                    break;
+                }
+
+                Column col = sortOrder.getColumn(i, tableFilter);
+
+                if (col == null) {
+                    sortOrderMatches = false;
+
+                    break;
+                }
+
+                IndexColumn indexCol = indexColumns[i];
+
+                if (!col.equals(indexCol.column)) {
+                    sortOrderMatches = false;
+
+                    break;
+                }
+
+                int sortType = sortTypes[i];
+
+                if (sortType != indexCol.sortType) {
+                    sortOrderMatches = false;
+
+                    break;
+                }
+
+                coveringCount++;
+            }
+
+            if (sortOrderMatches) {
+                // "coveringCount" makes sure that when we have two
+                // or more covering indexes, we choose the one
+                // that covers more.
+                sortingCost = 100 - coveringCount;
+            }
+        }
+
+        TableFilter tableFilter;
+
+        boolean skipColumnsIntersection = false;
+
+        if (filters != null && (tableFilter = filters[filter]) != null && columns != null) {
+            skipColumnsIntersection = true;
+
+            ArrayList<IndexCondition> idxConds = tableFilter.getIndexConditions();
+
+            // Only pk with _key used.
+            if (F.isEmpty(idxConds))
+                skipColumnsIntersection = false;
+
+            for (IndexCondition cond : idxConds) {
+                if (cond.getColumn() == columns[0]) {
+                    skipColumnsIntersection = false;
+
+                    break;
+                }
+            }
+        }
+
+        // If we have two indexes with the same cost, and one of the indexes can
+        // satisfy the query without needing to read from the primary table
+        // (scan index), make that one slightly lower cost.
+        boolean needsToReadFromScanIndex = true;
+
+        if (!isScanIndex && allColumnsSet != null && !skipColumnsIntersection && !allColumnsSet.isEmpty()) {
+            boolean foundAllColumnsWeNeed = true;
+
+            for (Column c : allColumnsSet) {
+                boolean found = false;
+
+                for (Column c2 : columns) {
+                    if (c == c2) {
+                        found = true;
+
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    foundAllColumnsWeNeed = false;
+
+                    break;
+                }
+            }
+
+            if (foundAllColumnsWeNeed)
+                needsToReadFromScanIndex = false;
+        }
+
+        long rc;
+
+        if (isScanIndex)
+            rc = rowsCost + sortingCost + 20;
+        else if (needsToReadFromScanIndex)
+            rc = rowsCost + rowsCost + sortingCost + 20;
+        else {
+            // The (20-x) calculation makes sure that when we pick a covering
+            // index, we pick the covering index that has the smallest number of
+            // columns (the more columns we have in index - the higher cost).
+            // This is faster because a smaller index will fit into fewer data
+            // blocks.
+            rc = rowsCost + sortingCost + columns.length;
+        }
+
+        return rc;
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
index 7944e74..ee28b71 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/BasicIndexTest.java
@@ -34,6 +34,7 @@ import org.apache.ignite.cache.QueryIndex;
 import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cluster.ClusterState;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
@@ -51,6 +52,7 @@ import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 
+import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.PK_IDX_NAME;
 import static org.apache.ignite.internal.processors.query.h2.database.H2Tree.IGNITE_THROTTLE_INLINE_SIZE_CALCULATION;
 import static org.apache.ignite.internal.processors.query.h2.opt.H2TableScanIndex.SCAN_INDEX_NAME_SUFFIX;
 
@@ -61,10 +63,10 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
     /** Default client name. */
     private static final String CLIENT_NAME = "client";
 
-    /** {@code True} If index need to be created throught static config. */
+    /** {@code True} If index need to be created by static config. */
     private static boolean createIdx = true;
 
-    /** {@code True} If cache nedd to be created throught static config. */
+    /** {@code True} If cache need to be created by static config. */
     private static boolean createStaticCache = true;
 
     /** Default table name. */
@@ -303,7 +305,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         startGrid(1);
 
         if (persistEnabled)
-            ig0.cluster().active(true);
+            ig0.cluster().state(ClusterState.ACTIVE);
 
         IgniteCache<Key, Val> cache = grid(0).cache(DEFAULT_CACHE_NAME);
 
@@ -324,7 +326,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         ig0 = startGrid(0);
 
         if (persistEnabled)
-            ig0.cluster().active(true);
+            ig0.cluster().state(ClusterState.ACTIVE);
 
         ig0.getOrCreateCache(DEFAULT_CACHE_NAME);
 
@@ -370,9 +372,9 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         inlineSize = 10;
 
-        srvLog = new ListeningTestLogger(false, log);
+        srvLog = new ListeningTestLogger(log);
 
-        clientLog = new ListeningTestLogger(false, log);
+        clientLog = new ListeningTestLogger(log);
 
         String msg1 = "Index with the given set or subset of columns already exists";
 
@@ -409,6 +411,11 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         cache.query(new SqlFieldsQuery("create index \"idx4\" on Val(valLong)"));
 
+        String plan = cache.query(new SqlFieldsQuery("explain select min(_key), max(_key) from Val")).getAll()
+            .get(0).get(0).toString().toUpperCase();
+
+        assertTrue(plan, plan.contains(PK_IDX_NAME.toUpperCase()));
+
         assertTrue(lsnr.check());
 
         srvLog.unregisterListener(lsnr);
@@ -446,6 +453,8 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         String plan = qryProc.querySqlFields(new SqlFieldsQuery(sql), true)
             .getAll().get(0).get(0).toString().toUpperCase();
 
+        System.err.println(plan);
+
         return idxName != null ? (!plan.contains(SCAN_INDEX_NAME_SUFFIX) && plan.contains(idxName.toUpperCase())) : !plan.contains(SCAN_INDEX_NAME_SUFFIX);
     }
 
@@ -467,7 +476,13 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         return lsnrIdx.check();
     }
 
-    /** */
+    /**
+     * Create and fills the table.
+     *
+     * @param qryProc Query processor.
+     * @param tblName Table name.
+     * @param consPkFldsNum Number of fields to use in pk, 0 - default pk, positive\negative value shows fields iteration direction.
+     */
     private void populateTable(GridQueryProcessor qryProc, String tblName, int consPkFldsNum, String... reqFlds) {
         assert consPkFldsNum <= reqFlds.length;
 
@@ -476,16 +491,22 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         String sqlIns = "INSERT INTO " + tblName + " (";
 
         for (int i = 0; i < reqFlds.length; ++i) {
-            sql += reqFlds[i] + " VARCHAR, ";
+            sql += reqFlds[i] + " VARCHAR" + (consPkFldsNum == 0 && i == 0 ? " PRIMARY KEY, " : ", ");
 
             sqlIns += reqFlds[i] + ((i < reqFlds.length - 1) ? ", " : ") values (");
         }
 
-        if (consPkFldsNum > 0) {
+        if (consPkFldsNum != 0) {
             sql += " CONSTRAINT PK_PERSON PRIMARY KEY (";
 
-            for (int i = 0; i < consPkFldsNum; ++i)
-                sql += reqFlds[i] + ((i < consPkFldsNum - 1) ? ", " : "))");
+            if (consPkFldsNum > 0) {
+                for (int i = 0; i < consPkFldsNum; ++i)
+                    sql += reqFlds[i] + ((i < consPkFldsNum - 1) ? ", " : "))");
+            }
+            else {
+                for (int i = -consPkFldsNum - 1; i >= 0; --i)
+                    sql += reqFlds[i] + ((i > 0) ? ", " : "))");
+            }
         }
         else
             sql += ")";
@@ -520,7 +541,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
     public void testAllTableFieldsCoveredByIdx() throws Exception {
         inlineSize = 10;
 
-        srvLog = new ListeningTestLogger(false, log);
+        srvLog = new ListeningTestLogger(log);
 
         IgniteEx ig0 = startGrid(0);
 
@@ -535,7 +556,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "FIRST_NAME"));
 
-        assertTrue(checkIdxUsed(qryProc, "_key_PK", TEST_TBL_NAME, "FIRST_NAME",
+        assertTrue(checkIdxUsed(qryProc, PK_IDX_NAME, TEST_TBL_NAME, "FIRST_NAME",
             "LAST_NAME", "LANG", "ADDRESS"));
 
         assertTrue(checkIdxAlreadyExistLog(
@@ -550,9 +571,65 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "FIRST_NAME",
             "LAST_NAME", "ADDRESS", "LANG"));
+    }
+
+    /**
+     *  Checks index usage for full coverage.
+     */
+    @Test
+    public void testConditionsWithoutIndexes() throws Exception {
+        inlineSize = 10;
+
+        srvLog = new ListeningTestLogger(log);
+
+        IgniteEx ig0 = startGrid(0);
+
+        GridQueryProcessor qryProc = ig0.context().query();
+
+        populateTable(qryProc, TEST_TBL_NAME, 2, "FIRST_NAME", "LAST_NAME", "ADDRESS", "LANG");
+
+        String sqlIdx = String.format("create index \"idx1\" on %s(LANG, ADDRESS)", TEST_TBL_NAME);
+
+        qryProc.querySqlFields(new SqlFieldsQuery(sqlIdx), true).getAll();
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "LAST_NAME"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "ADDRESS"));
+
+        assertTrue(checkIdxUsed(qryProc, "idx1", TEST_TBL_NAME, "LANG"));
+
         // first idx fields not belongs to request fields.
-        assertTrue(checkIdxUsed(qryProc, "idx2", TEST_TBL_NAME, "ADDRESS",
-            "LAST_NAME"));
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "ADDRESS", "LAST_NAME"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "ADDRESS", "ADDRESS"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "LAST_NAME", "ADDRESS"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "ADDRESS"));
+    }
+
+    /**
+     *  Checks proxy index usage.
+     */
+    @Test
+    public void testConditionsWithoutIndexesUseProxy() throws Exception {
+        inlineSize = 10;
+
+        srvLog = new ListeningTestLogger(log);
+
+        IgniteEx ig0 = startGrid(0);
+
+        GridQueryProcessor qryProc = ig0.context().query();
+
+        populateTable(qryProc, TEST_TBL_NAME, 1, "ID", "VAL0", "VAL1");
+
+        String sqlIdx = String.format("create index \"idx1\" on %s(VAL0, VAL1)", TEST_TBL_NAME);
+
+        qryProc.querySqlFields(new SqlFieldsQuery(sqlIdx), true).getAll();
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "VAL1"));
+
+        assertTrue(checkIdxUsed(qryProc, "idx1", TEST_TBL_NAME, "VAL0"));
     }
 
     /**
@@ -584,7 +661,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "FIRST_NAME"));
 
-        assertTrue(checkIdxUsed(qryProc, "_key_PK", TEST_TBL_NAME, "FIRST_NAME",
+        assertTrue(checkIdxUsed(qryProc, PK_IDX_NAME, TEST_TBL_NAME, "FIRST_NAME",
             "LAST_NAME", "LANG", "ADDRESS"));
 
         assertTrue(checkIdxAlreadyExistLog(
@@ -593,10 +670,11 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         String sqlIdx2 = String.format("create index \"idx2\" on %s(LANG, ADDRESS)", TEST_TBL_NAME);
 
         qryProc.querySqlFields(new SqlFieldsQuery(sqlIdx2), true).getAll();
-        // _key_PK used.
+
+        // PK_IDX_NAME used.
         assertFalse(checkIdxUsed(qryProc, "idx2", TEST_TBL_NAME, "FIRST_NAME",
             "LAST_NAME", "LANG", "ADDRESS"));
-        // _key_PK used.
+
         assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "FIRST_NAME",
             "LAST_NAME", "LANG", "ADDRESS"));
 
@@ -612,7 +690,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
         assertTrue(checkIdxAlreadyExistLog(
             qryProc, "idx4", TEST_TBL_NAME, "FIRST_NAME", "LAST_NAME", "ADDRESS", "LANG"));
 
-        LogListener lsnrIdx4 = LogListener.matches(msg0).andMatches("_key_PK").build();
+        LogListener lsnrIdx4 = LogListener.matches(msg0).andMatches(PK_IDX_NAME).build();
 
         srvLog.registerListener(lsnrIdx4);
 
@@ -624,7 +702,33 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
     }
 
     /**
-     * Tests different fields sequence in indexes.
+     * Check three fields in pk index.
+     */
+    @Test
+    public void testCheckThreeFieldsInPk() throws Exception {
+        inlineSize = 10;
+
+        srvLog = new ListeningTestLogger(log);
+
+        IgniteEx ig0 = startGrid(0);
+
+        GridQueryProcessor qryProc = ig0.context().query();
+
+        populateTable(qryProc, TEST_TBL_NAME, 3, "c1", "c2", "c3", "c4", "c5", "c6");
+
+        assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "c1"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "c2"));
+
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "c3"));
+    }
+
+    /**
+     * Test composite indices with PK field in first place.
+     *
+     * There is no sense to create such indices:
+     * 1. PK index will be enough for equality condition on PK field.
+     * 2. None of these indices will be used for non-equality condition on PK field.
      */
     @Test
     public void testCreateIdxWithDifferentIdxFldsSeq() throws Exception {
@@ -784,7 +888,7 @@ public class BasicIndexTest extends AbstractIndexingCommonTest {
 
         assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "FIRST_NAME", "LAST_NAME", "ADDRESS"));
 
-        assertTrue(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "LAST_NAME", "ADDRESS"));
+        assertFalse(checkIdxUsed(qryProc, null, TEST_TBL_NAME, "LAST_NAME", "ADDRESS"));
     }
 
     /** */
diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/IgniteSqlQueryMinMaxTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/IgniteSqlQueryMinMaxTest.java
index 9c299d3..e5319e2 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/IgniteSqlQueryMinMaxTest.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/h2/IgniteSqlQueryMinMaxTest.java
@@ -169,8 +169,9 @@ public class IgniteSqlQueryMinMaxTest extends AbstractIndexingCommonTest {
             QueryCursor<List<?>> cursor = cache.query(new SqlFieldsQuery("explain select min(_key), max(_key) from Integer"));
             List<List<?>> result = cursor.getAll();
             assertEquals(2, result.size());
-            assertTrue(((String)result.get(0).get(0)).toLowerCase().contains("_key_pk"));
-            assertTrue(((String)result.get(0).get(0)).toLowerCase().contains("direct lookup"));
+            String res = ((String)result.get(0).get(0)).toLowerCase();
+            assertTrue(res, res.contains("_key_pk"));
+            assertTrue(res, res.contains("direct lookup"));
         }
     }