You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by zs...@apache.org on 2022/04/07 09:01:58 UTC

[ignite-3] branch ignite-14925-sorted-indexes updated: IGNITE-16266 Add unique id for indexes

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

zstan pushed a commit to branch ignite-14925-sorted-indexes
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/ignite-14925-sorted-indexes by this push:
     new 81e27785b IGNITE-16266 Add unique id for indexes
81e27785b is described below

commit 81e27785b6c7a1ad32b3cb3ddc988d7f716e030d
Author: zstan <st...@gmail.com>
AuthorDate: Thu Apr 7 12:01:46 2022 +0300

    IGNITE-16266 Add unique id for indexes
---
 .../table/TableIndexConfigurationSchema.java       |   6 +
 .../apache/ignite/lang/IndexNotFoundException.java |  12 ++
 .../ignite/internal/manager/AbstractProducer.java  |   2 +-
 .../apache/ignite/internal/idx/IndexManager.java   |   9 ++
 .../ignite/internal/idx/InternalSortedIndex.java   |   7 +
 .../internal/idx/event/IndexEventParameters.java   |  23 +++-
 .../ignite/internal/idx/IndexManagerImpl.java      | 141 ++++++++++++++-------
 .../internal/idx/InternalSortedIndexImpl.java      |  10 +-
 .../internal/runner/app/IgniteCliRunnerTest.java   |  39 ++++--
 .../internal/sql/engine/util/QueryChecker.java     |   7 +-
 .../ignite/internal/schema/ByteBufferRow.java      |  11 +-
 .../internal/sql/engine/SqlQueryProcessor.java     |   2 +-
 .../sql/engine/exec/LogicalRelImplementor.java     |   2 +-
 .../sql/engine/externalize/RelInputEx.java         |  13 +-
 .../sql/engine/externalize/RelJsonReader.java      |  21 ++-
 .../internal/sql/engine/prepare/PlannerHelper.java |   2 +-
 .../internal/sql/engine/rel/AbstractIndexScan.java |  22 ++--
 .../internal/sql/engine/rel/IgniteIndexScan.java   |  23 ++--
 .../engine/rel/ProjectableFilterableTableScan.java |   2 +-
 .../engine/rel/logical/IgniteLogicalIndexScan.java |  13 +-
 .../sql/engine/rule/LogicalScanConverterRule.java  |   4 +-
 .../sql/engine/rule/logical/ExposeIndexRule.java   |   4 +-
 .../engine/rule/logical/FilterScanMergeRule.java   |   2 +-
 .../engine/rule/logical/ProjectScanMergeRule.java  |   2 +-
 .../internal/sql/engine/schema/IgniteIndex.java    |  33 ++---
 .../sql/engine/schema/IgniteTableImpl.java         |  10 +-
 .../sql/engine/schema/InternalIgniteTable.java     |  10 +-
 .../sql/engine/schema/SqlSchemaManager.java        |  10 ++
 .../sql/engine/schema/SqlSchemaManagerImpl.java    |  65 ++++++++--
 .../internal/sql/engine/util/BaseQueryContext.java |   1 +
 .../sql/engine/exec/MockedStructuresTest.java      |   3 +-
 .../sql/engine/externalize/RelJsonReaderTest.java  |  49 ++++++-
 .../sql/engine/planner/AbstractPlannerTest.java    |  51 +++++---
 .../engine/planner/JoinColocationPlannerTest.java  |  13 +-
 .../internal/sql/engine/planner/PlannerTest.java   |   6 +-
 .../sql/engine/planner/TableDmlPlannerTest.java    |   7 +-
 36 files changed, 448 insertions(+), 189 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/configuration/schemas/table/TableIndexConfigurationSchema.java b/modules/api/src/main/java/org/apache/ignite/configuration/schemas/table/TableIndexConfigurationSchema.java
index 819795c8a..86fd7950c 100644
--- a/modules/api/src/main/java/org/apache/ignite/configuration/schemas/table/TableIndexConfigurationSchema.java
+++ b/modules/api/src/main/java/org/apache/ignite/configuration/schemas/table/TableIndexConfigurationSchema.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.configuration.schemas.table;
 
+import java.util.UUID;
+import org.apache.ignite.configuration.annotation.InternalId;
 import org.apache.ignite.configuration.annotation.PolymorphicConfig;
 import org.apache.ignite.configuration.annotation.PolymorphicId;
 import org.apache.ignite.configuration.annotation.Value;
@@ -40,6 +42,10 @@ public class TableIndexConfigurationSchema {
     @PolymorphicId
     public String type;
 
+    /** Index id. **/
+    @InternalId
+    public UUID id;
+
     /** Index name. */
     @Value
     @Immutable
diff --git a/modules/api/src/main/java/org/apache/ignite/lang/IndexNotFoundException.java b/modules/api/src/main/java/org/apache/ignite/lang/IndexNotFoundException.java
index 1e89aee02..471132e35 100644
--- a/modules/api/src/main/java/org/apache/ignite/lang/IndexNotFoundException.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/IndexNotFoundException.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.lang;
 
+import java.util.UUID;
+
 /**
  * Exception is thrown when appropriate index is not found.
  */
@@ -29,4 +31,14 @@ public class IndexNotFoundException extends IgniteException {
     public IndexNotFoundException(String indexName) {
         super(IgniteStringFormatter.format("Index '{}' does not exist.", indexName));
     }
+
+    /**
+     * Create a new exception with given index name.
+     *
+     * @param indexName Index canonical name.
+     * @param id Index id.
+     */
+    public IndexNotFoundException(String indexName, UUID id) {
+        super(IgniteStringFormatter.format("Index '{}' was concurrently modified or deleted, id='{}'.", indexName, id));
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/manager/AbstractProducer.java b/modules/core/src/main/java/org/apache/ignite/internal/manager/AbstractProducer.java
index 8a86a302e..704081b40 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/manager/AbstractProducer.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/manager/AbstractProducer.java
@@ -57,7 +57,7 @@ public abstract class AbstractProducer<T extends Event, P extends EventParameter
      * @param params Event parameters.
      * @param err    Exception when it was happened, or {@code null} otherwise.
      */
-    protected void fireEvent(T evt, P params, Throwable err) {
+    protected void fireEvent(T evt, P params, @Nullable Throwable err) {
         ConcurrentLinkedQueue<EventListener<P>> queue = listeners.get(evt);
 
         if (queue == null) {
diff --git a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/IndexManager.java b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/IndexManager.java
index f1a195019..fcaed349e 100644
--- a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/IndexManager.java
+++ b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/IndexManager.java
@@ -28,6 +28,7 @@ import org.apache.ignite.internal.manager.Producer;
 import org.apache.ignite.lang.IndexAlreadyExistsException;
 import org.apache.ignite.lang.IndexNotFoundException;
 import org.apache.ignite.lang.NodeStoppingException;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Internal index manager facade provides low-level methods for indexes operations.
@@ -102,4 +103,12 @@ public interface IndexManager extends Producer<IndexEvent, IndexEventParameters>
      * @throws NodeStoppingException If an implementation stopped before the method was invoked.
      */
     CompletableFuture<InternalSortedIndex> indexAsync(String idxCanonicalName);
+
+    /**
+     * Returns index by specified id.
+     *
+     * @param id Index id.
+     * @return Index or {@code null} if can`t be found.
+     */
+    @Nullable InternalSortedIndex getIndexById(UUID id);
 }
diff --git a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndex.java b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndex.java
index ba9b54cfd..422d2d2d7 100644
--- a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndex.java
+++ b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndex.java
@@ -45,6 +45,13 @@ public interface InternalSortedIndex {
      */
     String name();
 
+    /**
+     * Return index id.
+     *
+     * @return Index id.
+     */
+    UUID id();
+
     /**
      * Return indexed table.
      *
diff --git a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/event/IndexEventParameters.java b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/event/IndexEventParameters.java
index 3b286a5df..f3b6dd947 100644
--- a/modules/index-api/src/main/java/org/apache/ignite/internal/idx/event/IndexEventParameters.java
+++ b/modules/index-api/src/main/java/org/apache/ignite/internal/idx/event/IndexEventParameters.java
@@ -17,14 +17,18 @@
 
 package org.apache.ignite.internal.idx.event;
 
+import java.util.UUID;
 import org.apache.ignite.internal.idx.InternalSortedIndex;
 import org.apache.ignite.internal.manager.EventParameters;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Table event parameters. There are properties which associate with a concrete table.
+ * Index event parameters. There are properties which are associates with a concrete index.
  */
 public class IndexEventParameters implements EventParameters {
+    /** Index id. */
+    private final UUID idxId;
+
     /** Index name. */
     private final String idxName;
 
@@ -41,32 +45,39 @@ public class IndexEventParameters implements EventParameters {
      * @param idx Index instance.
      */
     public IndexEventParameters(String tblName, InternalSortedIndex idx) {
-        this(idx.name(), tblName, idx);
+        this(idx.id(), idx.name(), tblName, idx);
     }
 
     /**
      * Constructor.
      *
+     * @param idxId Index id.
      * @param idxName Index name.
      * @param tblName Table name.
      */
-    public IndexEventParameters(String idxName, String tblName) {
-        this(idxName, tblName, null);
+    public IndexEventParameters(UUID idxId, String idxName, String tblName) {
+        this(idxId, idxName, tblName, null);
     }
 
     /**
      * Constructor.
      *
+     * @param idxId Index id.
      * @param idxName Index name.
      * @param tblName Table name.
-     * @param idx     Index.
+     * @param idx Index.
      */
-    private IndexEventParameters(String idxName, String tblName, @Nullable InternalSortedIndex idx) {
+    private IndexEventParameters(UUID idxId, String idxName, String tblName, @Nullable InternalSortedIndex idx) {
+        this.idxId = idxId;
         this.idxName = idxName;
         this.tblName = tblName;
         this.idx = idx;
     }
 
+    public UUID indexId() {
+        return idxId;
+    }
+
     public String indexName() {
         return idxName;
     }
diff --git a/modules/index/src/main/java/org/apache/ignite/internal/idx/IndexManagerImpl.java b/modules/index/src/main/java/org/apache/ignite/internal/idx/IndexManagerImpl.java
index ebada673f..f6ee3fa6a 100644
--- a/modules/index/src/main/java/org/apache/ignite/internal/idx/IndexManagerImpl.java
+++ b/modules/index/src/main/java/org/apache/ignite/internal/idx/IndexManagerImpl.java
@@ -18,10 +18,12 @@
 package org.apache.ignite.internal.idx;
 
 import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.directProxy;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.getByInternalId;
 
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
@@ -51,6 +53,7 @@ import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.table.TableImpl;
 import org.apache.ignite.internal.table.distributed.TableManager;
+import org.apache.ignite.internal.util.IgniteObjectName;
 import org.apache.ignite.internal.util.IgniteSpinBusyLock;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.IgniteLogger;
@@ -58,7 +61,6 @@ import org.apache.ignite.lang.IndexAlreadyExistsException;
 import org.apache.ignite.lang.IndexNotFoundException;
 import org.apache.ignite.lang.NodeStoppingException;
 import org.apache.ignite.lang.TableNotFoundException;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -74,6 +76,9 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
     /** Indexes by canonical name. */
     private final Map<String, InternalSortedIndex> idxsByName = new ConcurrentHashMap<>();
 
+    /** Indexes by id. */
+    private final Map<UUID, InternalSortedIndex> idxsById = new ConcurrentHashMap<>();
+
     /** Busy lock to stop synchronously. */
     private final IgniteSpinBusyLock busyLock = new IgniteSpinBusyLock();
 
@@ -83,7 +88,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
     /**
      * Constructor.
      *
-     * @param tablesCfg      Tables configuration.
+     * @param tablesCfg Tables configuration.
      */
     public IndexManagerImpl(
             TableManager tblMgr,
@@ -98,16 +103,15 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
         tablesCfg.tables().any().indices().listenElements(
                 new ConfigurationNamedListListener<>() {
                     @Override
-                    public @NotNull CompletableFuture<?> onCreate(@NotNull ConfigurationNotificationEvent<TableIndexView> ctx) {
+                    public CompletableFuture<?> onCreate(ConfigurationNotificationEvent<TableIndexView> ctx) {
                         TableConfiguration tbl = ctx.config(TableConfiguration.class);
                         String tblName = tbl.name().value();
+                        UUID idxId = ctx.newValue().id();
                         String idxName = ctx.newValue().name();
 
                         if (!busyLock.enterBusy()) {
                             fireEvent(IndexEvent.CREATE,
-                                    new IndexEventParameters(
-                                            idxName,
-                                            tblName),
+                                    new IndexEventParameters(idxId, idxName, tblName),
                                     new NodeStoppingException());
 
                             return CompletableFuture.completedFuture(new NodeStoppingException());
@@ -115,7 +119,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
                         try {
                             onIndexCreate(ctx);
                         } catch (Exception e) {
-                            fireEvent(IndexEvent.CREATE, new IndexEventParameters(idxName, tblName), e);
+                            fireEvent(IndexEvent.CREATE, new IndexEventParameters(idxId, idxName, tblName), e);
 
                             LOG.error("Internal error, index creation failed [name={}, table={}]", e, idxName, tblName);
 
@@ -128,24 +132,22 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
                     }
 
                     @Override
-                    public @NotNull CompletableFuture<?> onRename(@NotNull String oldName, @NotNull String newName,
-                            @NotNull ConfigurationNotificationEvent<TableIndexView> ctx) {
+                    public CompletableFuture<?> onRename(String oldName, String newName,
+                            ConfigurationNotificationEvent<TableIndexView> ctx) {
                         // TODO: IGNITE-16196 Supports index rename
                         return ConfigurationNamedListListener.super.onRename(oldName, newName, ctx);
                     }
 
                     @Override
-                    public @NotNull CompletableFuture<?> onDelete(@NotNull ConfigurationNotificationEvent<TableIndexView> ctx) {
+                    public CompletableFuture<?> onDelete(ConfigurationNotificationEvent<TableIndexView> ctx) {
                         TableConfiguration tbl = ctx.config(TableConfiguration.class);
                         String tblName = tbl.name().value();
                         String idxName = ctx.oldValue().name();
+                        UUID idxId = ctx.oldValue().id();
 
                         if (!busyLock.enterBusy()) {
                             fireEvent(IndexEvent.DROP,
-                                    new IndexEventParameters(
-                                            idxName,
-                                            tblName
-                                    ),
+                                    new IndexEventParameters(idxId, idxName, tblName),
                                     new NodeStoppingException());
 
                             return CompletableFuture.completedFuture(new NodeStoppingException());
@@ -160,7 +162,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
                     }
 
                     @Override
-                    public @NotNull CompletableFuture<?> onUpdate(@NotNull ConfigurationNotificationEvent<TableIndexView> ctx) {
+                    public CompletableFuture<?> onUpdate(ConfigurationNotificationEvent<TableIndexView> ctx) {
                         assert false : "Index cannot be updated [ctx=" + ctx + ']';
 
                         return CompletableFuture.completedFuture(null);
@@ -172,11 +174,19 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
         TableConfiguration tbl = ctx.config(TableConfiguration.class);
         String tblName = tbl.name().value();
 
-        InternalSortedIndex idx = idxsByName.remove(ctx.oldValue().name());
+        String idxName = ctx.oldValue().name();
+        UUID idxId = ctx.oldValue().id();
+
+        InternalSortedIndex idx = idxsByName.remove(idxName);
+
+        assert idx != null : "Index wasn't found: indexName=" + idxName;
+
+        idxsById.remove(idxId);
 
         idx.drop();
 
         fireEvent(IndexEvent.DROP, new IndexEventParameters(
+                idxId,
                 idx.name(),
                 tblName
         ), null);
@@ -234,7 +244,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
         try {
             CompletableFuture<Void> idxFut = new CompletableFuture<>();
 
-            indexAsyncInternal(idxCanonicalName).thenAccept((idx) -> {
+            indexAsync(idxCanonicalName).thenAccept((idx) -> {
                 if (idx == null) {
                     idxFut.completeExceptionally(new IndexNotFoundException(idxCanonicalName));
                     return;
@@ -283,6 +293,9 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
         if (!busyLock.enterBusy()) {
             throw new IgniteException(new NodeStoppingException());
         }
+
+        String idxName = IgniteObjectName.parseCanonicalName(idxCanonicalName);
+
         try {
             CompletableFuture<InternalSortedIndex> idxFut = new CompletableFuture<>();
 
@@ -291,17 +304,17 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
                     idxFut.completeExceptionally(new TableNotFoundException(tblCanonicalName));
                 }
 
-                indexAsyncInternal(idxCanonicalName).thenAccept((idx) -> {
+                indexAsync(idxName).thenAccept((idx) -> {
                     if (idx != null) {
                         idxFut.completeExceptionally(new IndexAlreadyExistsException(idxCanonicalName));
                     }
 
                     tblMgr.alterTableAsync(tblCanonicalName, chng -> chng.changeIndices(idxes -> {
-                                if (idxes.get(idxCanonicalName) != null) {
+                                if (idxes.get(idxName) != null) {
                                     idxFut.completeExceptionally(new IndexAlreadyExistsException(idxCanonicalName));
                                 }
 
-                                idxes.create(idxCanonicalName, idxChange::accept);
+                                idxes.create(idxName, idxChange::accept);
                             }
                     )).whenComplete((res, t) -> {
                         if (t != null) {
@@ -313,7 +326,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
 
                             idxFut.completeExceptionally(ex);
                         } else {
-                            idxFut.complete(idxsByName.get(idxCanonicalName));
+                            idxFut.complete(idxsByName.get(idxName));
                         }
                     });
                 });
@@ -338,14 +351,22 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
             throw new IgniteException(new NodeStoppingException());
         }
         try {
-            return indexAsyncInternal(idxCanonicalName);
+            String idxName = IgniteObjectName.parseCanonicalName(idxCanonicalName);
+
+            UUID idxId = directIndexId(idxName);
+
+            if (idxId == null) { // If not configured.
+                return CompletableFuture.completedFuture(null);
+            }
+
+            return indexAsyncInternal(idxId);
         } finally {
             busyLock.leaveBusy();
         }
     }
 
-    private CompletableFuture<InternalSortedIndex> indexAsyncInternal(String idxCanonicalName) {
-        var idx = idxsByName.get(idxCanonicalName);
+    private CompletableFuture<InternalSortedIndex> indexAsyncInternal(UUID idxId) {
+        InternalSortedIndex idx = idxsById.get(idxId);
 
         if (idx != null) {
             return CompletableFuture.completedFuture(idx);
@@ -355,8 +376,8 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
 
         EventListener<IndexEventParameters> clo = new EventListener<>() {
             @Override
-            public boolean notify(@NotNull IndexEventParameters parameters, @Nullable Throwable e) {
-                if (!idxCanonicalName.equals(parameters.indexName())) {
+            public boolean notify(IndexEventParameters parameters, @Nullable Throwable e) {
+                if (!idxId.equals(parameters.indexId())) {
                     return false;
                 }
 
@@ -370,16 +391,16 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
             }
 
             @Override
-            public void remove(@NotNull Throwable e) {
+            public void remove(Throwable e) {
                 getIdxFut.completeExceptionally(e);
             }
         };
 
         listen(IndexEvent.CREATE, clo);
 
-        idx = idxsByName.get(idxCanonicalName);
+        idx = idxsById.get(idxId);
 
-        if (idx != null && getIdxFut.complete(idx) || !isIndexConfigured(idxCanonicalName) && getIdxFut.complete(null)) {
+        if (idx != null && getIdxFut.complete(idx) || !isIndexConfigured(idxId) && getIdxFut.complete(null)) {
             removeListener(IndexEvent.CREATE, clo, null);
         } else {
             getIdxFut.thenRun(() -> removeListener(IndexEvent.CREATE, clo, null));
@@ -391,10 +412,34 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
     /**
      * Checks that the index is configured with specific name.
      *
-     * @param idxName Index name.
+     * @param idxId Index id.
      * @return True when the table is configured into cluster, false otherwise.
      */
-    private boolean isIndexConfigured(String idxName) {
+    private boolean isIndexConfigured(UUID idxId) {
+        try {
+            NamedListView<TableView> directTablesCfg = directProxy(tablesCfg.tables()).value();
+
+            // TODO: IGNITE-15721 Need to review this approach after the ticket would be fixed.
+            // Probably, it won't be required getting configuration of all tables from Metastore.
+            for (String name : directTablesCfg.namedListKeys()) {
+                TableView tableView = directTablesCfg.get(name);
+
+                getByInternalId(tableView.indices(), idxId);
+            }
+
+            return true;
+        } catch (NoSuchElementException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Gets direct id of index with {@code idxName}.
+     *
+     * @param idxName Index name.
+     * @return TableIndexView.
+     */
+    private @Nullable UUID directIndexId(String idxName) {
         NamedListView<TableView> directTablesCfg = directProxy(tablesCfg.tables()).value();
 
         // TODO: IGNITE-15721 Need to review this approach after the ticket would be fixed.
@@ -403,16 +448,16 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
             TableView tableView = directTablesCfg.get(name);
 
             if (tableView != null) {
-                if (tableView.indices().get(idxName) != null) {
-                    return true;
+                TableIndexView tableIndexView = tableView.indices().get(idxName);
+
+                if (tableIndexView != null) {
+                    return tableIndexView.id();
                 }
             }
         }
-
-        return false;
+        return null;
     }
 
-
     /**
      * Waits for future result and return, or unwraps {@link CompletionException} to {@link IgniteException} if failed.
      *
@@ -454,7 +499,7 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
      * @return A root exception which will be acceptable to throw for public API.
      */
     //TODO: IGNITE-16051 Implement exception converter for public API.
-    private @NotNull IgniteException getRootCause(Throwable t) {
+    private IgniteException getRootCause(Throwable t) {
         Throwable ex;
 
         if (t instanceof CompletionException) {
@@ -479,10 +524,10 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
         //        ).collect(Collectors.toMap(Column::name, Function.identity()));
 
         List<SortedIndexColumnDescriptor> idxCols = Stream.concat(
-                        idxView.columns().namedListKeys().stream(),
-                        Arrays.stream(tblSchema.keyColumns().columns())
-                                .map(Column::name)
-                )
+                idxView.columns().namedListKeys().stream(),
+                Arrays.stream(tblSchema.keyColumns().columns())
+                        .map(Column::name)
+        )
                 .distinct()
                 .map(colName -> {
                     IndexColumnView idxCol = idxView.columns().get(colName);
@@ -493,12 +538,13 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
                 .collect(Collectors.toList());
 
         SortedIndexDescriptor idxDesc = new SortedIndexDescriptor(
-                idxView.name(),
+                idxView.id().toString(),
                 idxCols,
                 tblSchema.keyColumns().columns()
         );
 
         InternalSortedIndexImpl idx = new InternalSortedIndexImpl(
+                idxView.id(),
                 idxView.name(),
                 tbl.internalTable().storage().createSortedIndex(idxDesc),
                 tbl
@@ -506,8 +552,17 @@ public class IndexManagerImpl extends AbstractProducer<IndexEvent, IndexEventPar
 
         tbl.addRowListener(idx);
 
-        idxsByName.put(idx.name(), idx);
+        idxsById.put(idxView.id(), idx);
+        idxsByName.put(idxView.name(), idx);
 
         fireEvent(IndexEvent.CREATE, new IndexEventParameters(tbl.name(), idx), null);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public InternalSortedIndex getIndexById(UUID id) {
+        InternalSortedIndex res = join(indexAsyncInternal(id));
+
+        return res;
+    }
 }
diff --git a/modules/index/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndexImpl.java b/modules/index/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndexImpl.java
index b7c598c05..320e0a0fd 100644
--- a/modules/index/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndexImpl.java
+++ b/modules/index/src/main/java/org/apache/ignite/internal/idx/InternalSortedIndexImpl.java
@@ -46,6 +46,8 @@ import org.jetbrains.annotations.Nullable;
  * Internal index manager facade provides low-level methods for indexes operations.
  */
 public class InternalSortedIndexImpl implements InternalSortedIndex, StorageRowListener {
+    private final UUID id;
+
     private final String name;
 
     private final TableImpl tbl;
@@ -57,7 +59,8 @@ public class InternalSortedIndexImpl implements InternalSortedIndex, StorageRowL
     /**
      * Create sorted index.
      */
-    public InternalSortedIndexImpl(String name, SortedIndexStorage store, TableImpl tbl) {
+    public InternalSortedIndexImpl(UUID id, String name, SortedIndexStorage store, TableImpl tbl) {
+        this.id = id;
         this.name = name;
         this.store = store;
         this.tbl = tbl;
@@ -71,6 +74,11 @@ public class InternalSortedIndexImpl implements InternalSortedIndex, StorageRowL
         return name;
     }
 
+    /** {@inheritDoc} */
+    @Override public UUID id() {
+        return id;
+    }
+
     /** {@inheritDoc} */
     @Override
     public UUID tableId() {
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
index a600ab0fd..d28040201 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/IgniteCliRunnerTest.java
@@ -20,9 +20,13 @@ package org.apache.ignite.internal.runner.app;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.nio.file.Path;
+import java.util.Objects;
+import java.util.stream.Stream;
+import org.apache.ignite.Ignite;
 import org.apache.ignite.app.IgniteCliRunner;
 import org.apache.ignite.internal.testframework.WorkDirectory;
 import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.internal.util.IgniteUtils;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -36,19 +40,28 @@ public class IgniteCliRunnerTest {
     public void runnerArgsSmokeTest(@WorkDirectory Path workDir) throws Exception {
         Path configPath = Path.of(IgniteCliRunnerTest.class.getResource("/ignite-config.json").toURI());
 
-        assertNotNull(IgniteCliRunner.start(
-                new String[]{
-                        "--config", configPath.toAbsolutePath().toString(),
-                        "--work-dir", workDir.resolve("node1").toAbsolutePath().toString(),
-                        "node1"
-                }
-        ));
+        Ignite node1 = null;
+        Ignite node2 = null;
+        try {
+            node1 = IgniteCliRunner.start(
+                    new String[]{
+                            "--config", configPath.toAbsolutePath().toString(),
+                            "--work-dir", workDir.resolve("node1").toAbsolutePath().toString(),
+                            "node1"
+                    }
+            );
+            assertNotNull(node1);
 
-        assertNotNull(IgniteCliRunner.start(
-                new String[]{
-                        "--work-dir", workDir.resolve("node2").toAbsolutePath().toString(),
-                        "node2"
-                }
-        ));
+            node2 = IgniteCliRunner.start(
+                    new String[]{
+                            "--work-dir", workDir.resolve("node2").toAbsolutePath().toString(),
+                            "node2"
+                    }
+            );
+
+            assertNotNull(node2);
+        } finally {
+            IgniteUtils.closeAll(Stream.of(node1, node2).filter(Objects::nonNull));
+        }
     }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
index 31b55d2f0..b37f21c9b 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
@@ -71,7 +71,8 @@ public abstract class QueryChecker {
      * @return Matcher.
      */
     public static Matcher<String> containsIndexScan(String schema, String tblName) {
-        return containsSubPlan("IgniteIndexScan(table=[[" + schema + ", " + tblName + "]]");
+        return matchesOnce(".*IgniteIndexScan\\(table=\\[\\[" + schema + ", " + tblName + "\\]\\],"
+                + " tableId=\\[.*\\].*\\)");
     }
 
     /**
@@ -83,8 +84,8 @@ public abstract class QueryChecker {
      * @return Matcher.
      */
     public static Matcher<String> containsIndexScan(String schema, String tblName, String idxName) {
-        return matchesOnce(".*IgniteIndexScan\\(tableId=\\[.*\\], index=\\[" + idxName
-                + "\\].*requiredColumns=\\[\\{(\\d|\\W|,)+\\}\\].*");
+        return matchesOnce(".*IgniteIndexScan\\(table=\\[\\[" + schema + ", " + tblName + "\\]\\],"
+                + " tableId=\\[.*\\], index=\\[" + idxName + "\\].*\\)");
     }
 
     /**
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/ByteBufferRow.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/ByteBufferRow.java
index 410195c4e..f5d925113 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/ByteBufferRow.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/ByteBufferRow.java
@@ -24,6 +24,7 @@ import java.nio.ByteOrder;
 import java.nio.channels.Channels;
 import java.nio.channels.WritableByteChannel;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 
 /**
  * Heap byte buffer-based row.
@@ -196,13 +197,9 @@ public class ByteBufferRow implements BinaryRow {
     }
 
     /** {@inheritDoc} */
-    @Override public byte[] bytes() {
+    @Override
+    public byte[] bytes() {
         // TODO IGNITE-15934 avoid copy.
-        byte[] tmp = new byte[buf.limit()];
-
-        buf.get(tmp);
-        buf.rewind();
-
-        return tmp;
+        return Arrays.copyOf(buf.array(), buf.limit());
     }
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
index 06e2039d9..cf63cade1 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
@@ -168,7 +168,7 @@ public class SqlQueryProcessor implements QueryProcessor {
 
         extensions = extensionList.stream().collect(Collectors.toMap(SqlExtension::name, Function.identity()));
 
-        SqlSchemaManagerImpl schemaManager = new SqlSchemaManagerImpl(tblManager, planCache::clear);
+        SqlSchemaManagerImpl schemaManager = new SqlSchemaManagerImpl(tblManager, idxManager, planCache::clear);
 
         executionSrvc = registerService(new ExecutionServiceImpl<>(
                 clusterSrvc.topologyService(),
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
index 88076e0c4..64ca49c83 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
@@ -302,7 +302,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
         Supplier<RowT> upper = upperCond == null ? null : expressionFactory.rowSource(upperCond);
         Function<RowT, RowT> prj = projects == null ? null : expressionFactory.project(projects, rowType);
 
-        IgniteIndex idx = tbl.getIndex(rel.indexName());
+        IgniteIndex idx = rel.getIndex();
 
         ColocationGroup group = ctx.group(rel.sourceId());
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelInputEx.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelInputEx.java
index b1c3ab016..bb56d52f7 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelInputEx.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelInputEx.java
@@ -20,6 +20,8 @@ package org.apache.ignite.internal.sql.engine.externalize;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelInput;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
+import org.apache.ignite.lang.IndexNotFoundException;
 
 /**
  * Extension to the {@link RelInput} interface.
@@ -38,8 +40,17 @@ public interface RelInputEx extends RelInput {
     /**
      * Returns table by its id.
      *
-     * @param tag Tag under which id is serialised.
+     * @param tag Tag under which id is serialized.
      * @return A table with given id.
      */
     RelOptTable getTableById(String tag);
+
+    /**
+     * Returns an index by its id.
+     *
+     * @param tag Tag represents id serialization.
+     * @param idxName Index name.
+     * @throws IndexNotFoundException If not found.
+     */
+    IgniteIndex getIndexById(String tag, String idxName);
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReader.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReader.java
index f91f9c3d1..cec776b81 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReader.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReader.java
@@ -28,6 +28,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.function.Function;
 import org.apache.calcite.plan.RelOptCluster;
@@ -47,6 +48,7 @@ import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
 import org.apache.ignite.internal.sql.engine.schema.SqlSchemaManager;
 import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -155,12 +157,29 @@ public class RelJsonReader {
         @Override
         public RelOptTable getTableById(String tag) {
             String tableId = getString(tag);
+
+            Objects.requireNonNull(tableId);
+
             IgniteTable table = schemaManager.tableById(UUID.fromString(tableId));
 
-            return RelOptTableImpl.create(null, table.getRowType(Commons.typeFactory()), List.of(tableId),
+            List<String> tableName = getStringList("table");
+
+            return RelOptTableImpl.create(null, table.getRowType(Commons.typeFactory()), tableName,
                     table, null);
         }
 
+        /** {@inheritDoc} */
+        @Override
+        public IgniteIndex getIndexById(String tag, String idxName) {
+            String idxId = getString(tag);
+
+            Objects.requireNonNull(idxId);
+
+            UUID id = UUID.fromString(idxId);
+
+            return schemaManager.indexById(id);
+        }
+
         /** {@inheritDoc} */
         @Override
         public RelNode getInput() {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
index c3b98fad0..4a9012c35 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlannerHelper.java
@@ -235,7 +235,7 @@ public class PlannerHelper {
             }
 
             IntSet indexedCols = CollectionUtils.setOf(
-                    tbl.getIndex(((AbstractIndexScan) scan).indexName()).collation().getKeys());
+                    ((AbstractIndexScan) scan).getIndex().collation().getKeys());
 
             spoolNeeded = modifyNode.getUpdateColumnList().stream()
                     .map(tbl.descriptor()::columnDescriptor)
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
index 4a67b625b..80e61df4e 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.sql.engine.rel;
 
+import static org.apache.calcite.sql.SqlExplainLevel.EXPPLAN_ATTRIBUTES;
+
 import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
@@ -31,7 +33,9 @@ import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.sql.engine.externalize.RelInputEx;
 import org.apache.ignite.internal.sql.engine.metadata.cost.IgniteCost;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.util.IndexConditions;
 import org.jetbrains.annotations.Nullable;
 
@@ -39,7 +43,7 @@ import org.jetbrains.annotations.Nullable;
  * Class with index conditions info.
  */
 public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
-    protected final String idxName;
+    protected final IgniteIndex index;
 
     protected final IndexConditions idxCond;
 
@@ -50,7 +54,8 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
      */
     protected AbstractIndexScan(RelInput input) {
         super(input);
-        idxName = input.getString("index");
+        String idxName = input.getString("indexName");
+        index = ((RelInputEx) input).getIndexById("indexId", idxName);
         idxCond = new IndexConditions(input);
     }
 
@@ -63,7 +68,7 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
             RelTraitSet traitSet,
             List<RelHint> hints,
             RelOptTable table,
-            String idxName,
+            IgniteIndex idx,
             @Nullable List<RexNode> proj,
             @Nullable RexNode cond,
             @Nullable IndexConditions idxCond,
@@ -71,25 +76,26 @@ public abstract class AbstractIndexScan extends ProjectableFilterableTableScan {
     ) {
         super(cluster, traitSet, hints, table, proj, cond, reqColumns);
 
-        this.idxName = idxName;
+        this.index = idx;
         this.idxCond = idxCond;
     }
 
     /** {@inheritDoc} */
     @Override
     protected RelWriter explainTerms0(RelWriter pw) {
-        pw = pw.item("index", idxName);
+        pw = pw.item("indexName", index.name());
+        pw = pw.itemIf("indexId", index.id().toString(), pw.getDetailLevel() != EXPPLAN_ATTRIBUTES);
         pw = super.explainTerms0(pw);
 
         return idxCond.explainTerms(pw);
     }
 
     /**
-     * Get index name.
+     * Get index instance.
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
      */
-    public String indexName() {
-        return idxName;
+    public IgniteIndex getIndex() {
+        return index;
     }
 
     /**
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/IgniteIndexScan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/IgniteIndexScan.java
index 26c41cf48..26cbd3874 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/IgniteIndexScan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/IgniteIndexScan.java
@@ -27,6 +27,7 @@ import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.util.IndexConditions;
 import org.jetbrains.annotations.Nullable;
 
@@ -58,14 +59,14 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
      * @param cluster Cluster that this relational expression belongs to
      * @param traits  Traits of this relational expression
      * @param tbl     Table definition.
-     * @param idxName Index name.
+     * @param idx     Index impl.
      */
     public IgniteIndexScan(
             RelOptCluster cluster,
             RelTraitSet traits,
             RelOptTable tbl,
-            String idxName) {
-        this(cluster, traits, tbl, idxName, null, null, null, null);
+            IgniteIndex idx) {
+        this(cluster, traits, tbl, idx, null, null, null, null);
     }
 
     /**
@@ -74,7 +75,7 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
      * @param cluster      Cluster that this relational expression belongs to
      * @param traits       Traits of this relational expression
      * @param tbl          Table definition.
-     * @param idxName      Index name.
+     * @param idx          Index impl.
      * @param proj         Projects.
      * @param cond         Filters.
      * @param requiredCols Participating columns.
@@ -83,13 +84,13 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
             RelOptCluster cluster,
             RelTraitSet traits,
             RelOptTable tbl,
-            String idxName,
+            IgniteIndex idx,
             @Nullable List<RexNode> proj,
             @Nullable RexNode cond,
             @Nullable IndexConditions idxCond,
             @Nullable ImmutableBitSet requiredCols
     ) {
-        this(-1L, cluster, traits, tbl, idxName, proj, cond, idxCond, requiredCols);
+        this(-1L, cluster, traits, tbl, idx, proj, cond, idxCond, requiredCols);
     }
 
     /**
@@ -98,7 +99,7 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
      * @param cluster      Cluster that this relational expression belongs to
      * @param traits       Traits of this relational expression
      * @param tbl          Table definition.
-     * @param idxName      Index name.
+     * @param idx          Index impl.
      * @param proj         Projects.
      * @param cond         Filters.
      * @param requiredCols Participating columns.
@@ -108,13 +109,13 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
             RelOptCluster cluster,
             RelTraitSet traits,
             RelOptTable tbl,
-            String idxName,
+            IgniteIndex idx,
             @Nullable List<RexNode> proj,
             @Nullable RexNode cond,
             @Nullable IndexConditions idxCond,
             @Nullable ImmutableBitSet requiredCols
     ) {
-        super(cluster, traits, List.of(), tbl, idxName, proj, cond, idxCond, requiredCols);
+        super(cluster, traits, List.of(), tbl, idx, proj, cond, idxCond, requiredCols);
 
         this.sourceId = sourceId;
     }
@@ -142,13 +143,13 @@ public class IgniteIndexScan extends AbstractIndexScan implements SourceAwareIgn
     @Override
     public IgniteRel clone(long sourceId) {
         return new IgniteIndexScan(sourceId, getCluster(), getTraitSet(), getTable(),
-                idxName, projects, condition, idxCond, requiredColumns);
+                index, projects, condition, idxCond, requiredColumns);
     }
 
     /** {@inheritDoc} */
     @Override
     public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
         return new IgniteIndexScan(sourceId, cluster, getTraitSet(), getTable(),
-                idxName, projects, condition, idxCond, requiredColumns);
+                index, projects, condition, idxCond, requiredColumns);
     }
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
index bbd60c720..b7b304617 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
@@ -133,7 +133,7 @@ public abstract class ProjectableFilterableTableScan extends TableScan {
     @Override
     public RelWriter explainTerms(RelWriter pw) {
         return explainTerms0(pw
-                .itemIf("table", table.getQualifiedName(), pw.getDetailLevel() == EXPPLAN_ATTRIBUTES)
+                .item("table", table.getQualifiedName())
                 .itemIf("tableId", table.unwrap(IgniteTable.class).id().toString(),
                         pw.getDetailLevel() != EXPPLAN_ATTRIBUTES)
         );
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
index f64cce5e9..7b1a2c951 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
@@ -27,6 +27,7 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.mapping.Mappings;
 import org.apache.ignite.internal.sql.engine.rel.AbstractIndexScan;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.InternalIgniteTable;
 import org.apache.ignite.internal.sql.engine.trait.TraitUtils;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
@@ -45,7 +46,7 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
             RelOptCluster cluster,
             RelTraitSet traits,
             RelOptTable table,
-            String idxName,
+            IgniteIndex idx,
             @Nullable List<RexNode> proj,
             @Nullable RexNode cond,
             @Nullable ImmutableBitSet requiredColumns
@@ -53,7 +54,7 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
         InternalIgniteTable tbl = table.unwrap(InternalIgniteTable.class);
         IgniteTypeFactory typeFactory = Commons.typeFactory(cluster);
         RelDataType rowType = tbl.getRowType(typeFactory, requiredColumns);
-        RelCollation collation = tbl.getIndex(idxName).collation();
+        RelCollation collation = idx.collation();
 
         if (requiredColumns != null) {
             Mappings.TargetMapping targetMapping = Commons.mapping(requiredColumns,
@@ -79,7 +80,7 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
                 cluster,
                 traits,
                 table,
-                idxName,
+                idx,
                 proj,
                 cond,
                 idxCond,
@@ -92,7 +93,7 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
      * @param cluster      Cluster that this relational expression belongs to
      * @param traits       Traits of this relational expression
      * @param tbl          Table definition.
-     * @param idxName      Index name.
+     * @param idx          Index impl.
      * @param proj         Projects.
      * @param cond         Filters.
      * @param idxCond      Index conditions.
@@ -102,12 +103,12 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
             RelOptCluster cluster,
             RelTraitSet traits,
             RelOptTable tbl,
-            String idxName,
+            IgniteIndex idx,
             @Nullable List<RexNode> proj,
             @Nullable RexNode cond,
             @Nullable IndexConditions idxCond,
             @Nullable ImmutableBitSet requiredCols
     ) {
-        super(cluster, traits, List.of(), tbl, idxName, proj, cond, idxCond, requiredCols);
+        super(cluster, traits, List.of(), tbl, idx, proj, cond, idxCond, requiredCols);
     }
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/LogicalScanConverterRule.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/LogicalScanConverterRule.java
index e8f9b963b..204733ba4 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/LogicalScanConverterRule.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/LogicalScanConverterRule.java
@@ -64,7 +64,7 @@ public abstract class LogicalScanConverterRule<T extends ProjectableFilterableTa
                 ) {
                     RelOptCluster cluster = rel.getCluster();
                     InternalIgniteTable table = rel.getTable().unwrap(InternalIgniteTable.class);
-                    IgniteIndex index = table.getIndex(rel.indexName());
+                    IgniteIndex index = rel.getIndex();
 
                     RelDistribution distribution = table.distribution();
                     RelCollation collation = index.collation();
@@ -95,7 +95,7 @@ public abstract class LogicalScanConverterRule<T extends ProjectableFilterableTa
                         cluster,
                         traits,
                         rel.getTable(),
-                        rel.indexName(),
+                        rel.getIndex(),
                         rel.projects(),
                         rel.condition(),
                         rel.indexConditions(),
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ExposeIndexRule.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ExposeIndexRule.java
index b25cefeae..a56be0a4f 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ExposeIndexRule.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ExposeIndexRule.java
@@ -63,8 +63,8 @@ public class ExposeIndexRule extends RelRule<ExposeIndexRule.Config> {
         RexNode condition = scan.condition();
         ImmutableBitSet requiredCols = scan.requiredColumns();
 
-        List<IgniteLogicalIndexScan> indexes = igniteTable.indexes().keySet().stream()
-                .map(idxName -> igniteTable.toRel(cluster, optTable, idxName, proj, condition, requiredCols))
+        List<IgniteLogicalIndexScan> indexes = igniteTable.indexes().values().stream()
+                .map(idx -> igniteTable.toRel(cluster, optTable, idx, proj, condition, requiredCols))
                 .collect(Collectors.toList());
 
         if (indexes.isEmpty()) {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
index b100aa49e..531e80ff8 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/FilterScanMergeRule.java
@@ -158,7 +158,7 @@ public abstract class FilterScanMergeRule<T extends ProjectableFilterableTableSc
                 RelTraitSet traits,
                 RexNode cond
         ) {
-            return IgniteLogicalIndexScan.create(cluster, traits, scan.getTable(), scan.indexName(),
+            return IgniteLogicalIndexScan.create(cluster, traits, scan.getTable(), scan.getIndex(),
                     scan.projects(), cond, scan.requiredColumns());
         }
     }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ProjectScanMergeRule.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ProjectScanMergeRule.java
index a32d7a52a..325e4240e 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ProjectScanMergeRule.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/logical/ProjectScanMergeRule.java
@@ -200,7 +200,7 @@ public abstract class ProjectScanMergeRule<T extends ProjectableFilterableTableS
                 cluster,
                 traits,
                 scan.getTable(),
-                scan.indexName(),
+                scan.getIndex(),
                 projections,
                 cond, requiredColumns
             );
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
index e30041372..00eaaa182 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.sql.engine.schema;
 
+import java.util.Objects;
+import java.util.UUID;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -33,37 +35,18 @@ import org.apache.ignite.internal.sql.engine.metadata.ColocationGroup;
 public class IgniteIndex {
     private final RelCollation collation;
 
-    private final String idxName;
-
     private final InternalSortedIndex idx;
 
     private final InternalIgniteTable tbl;
 
-    /**
-     * Constructor.
-     * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
-     */
-    public IgniteIndex(RelCollation collation, String name, InternalIgniteTable tbl) {
-        this(collation, name, null, tbl);
-    }
-
     /**
      * Constructor.
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
      */
     public IgniteIndex(RelCollation collation, InternalSortedIndex idx, InternalIgniteTable tbl) {
-        this(collation, idx.name(), idx, tbl);
-    }
-
-    /**
-     * Constructor.
-     * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
-     */
-    private IgniteIndex(RelCollation collation, String name, InternalSortedIndex idx, InternalIgniteTable tbl) {
-        this.collation = collation;
-        idxName = name;
-        this.idx = idx;
-        this.tbl = tbl;
+        this.collation = Objects.requireNonNull(collation);
+        this.idx = Objects.requireNonNull(idx);
+        this.tbl = Objects.requireNonNull(tbl);
     }
 
     public RelCollation collation() {
@@ -71,7 +54,7 @@ public class IgniteIndex {
     }
 
     public String name() {
-        return idxName;
+        return idx.name();
     }
 
     public InternalIgniteTable table() {
@@ -82,6 +65,10 @@ public class IgniteIndex {
         return idx;
     }
 
+    public UUID id() {
+        return idx.id();
+    }
+
     /**
      * Scan index.
      */
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteTableImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteTableImpl.java
index a893918a8..2725fb4dd 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteTableImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteTableImpl.java
@@ -158,7 +158,7 @@ public class IgniteTableImpl extends AbstractTable implements InternalIgniteTabl
     public IgniteLogicalIndexScan toRel(
             RelOptCluster cluster,
             RelOptTable relOptTable,
-            String idxName,
+            IgniteIndex idx,
             List<RexNode> proj,
             RexNode condition,
             ImmutableBitSet requiredCols
@@ -166,9 +166,9 @@ public class IgniteTableImpl extends AbstractTable implements InternalIgniteTabl
         RelTraitSet traitSet = cluster.traitSetOf(Convention.Impl.NONE)
                 .replace(distribution())
                 .replace(RewindabilityTrait.REWINDABLE)
-                .replace(getIndex(idxName).collation());
+                .replace(idx.collation());
 
-        return IgniteLogicalIndexScan.create(cluster, traitSet, relOptTable, idxName, proj, condition, requiredCols);
+        return IgniteLogicalIndexScan.create(cluster, traitSet, relOptTable, idx, proj, condition, requiredCols);
     }
 
     /** {@inheritDoc} */
@@ -212,8 +212,8 @@ public class IgniteTableImpl extends AbstractTable implements InternalIgniteTabl
 
     /** {@inheritDoc} */
     @Override
-    public void removeIndex(String idxName) {
-        indexes.remove(idxName);
+    public IgniteIndex removeIndex(String idxName) {
+        return indexes.remove(idxName);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/InternalIgniteTable.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/InternalIgniteTable.java
index 99b95a118..4ea6b97cc 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/InternalIgniteTable.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/InternalIgniteTable.java
@@ -49,11 +49,11 @@ public interface InternalIgniteTable extends IgniteTable {
      *
      * @param cluster   Custer.
      * @param relOptTbl Table.
-     * @param idxName   Index name.
+     * @param idx       Index impl.
      * @return Table relational expression.
      */
-    default IgniteLogicalIndexScan toRel(RelOptCluster cluster, RelOptTable relOptTbl, String idxName) {
-        return toRel(cluster, relOptTbl, idxName, null, null, null);
+    default IgniteLogicalIndexScan toRel(RelOptCluster cluster, RelOptTable relOptTbl, IgniteIndex idx) {
+        return toRel(cluster, relOptTbl, idx, null, null, null);
     }
 
     /**
@@ -73,7 +73,7 @@ public interface InternalIgniteTable extends IgniteTable {
     IgniteLogicalIndexScan toRel(
             RelOptCluster cluster,
             RelOptTable relOptTbl,
-            String idxName,
+            IgniteIndex idx,
             List<RexNode> proj,
             RexNode condition,
             ImmutableBitSet requiredCols
@@ -148,5 +148,5 @@ public interface InternalIgniteTable extends IgniteTable {
      *
      * @param idxName Index name.
      */
-    void removeIndex(String idxName);
+    IgniteIndex removeIndex(String idxName);
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManager.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManager.java
index 7c1aeb690..d83dc5efa 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManager.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManager.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.schema;
 
 import java.util.UUID;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.ignite.lang.IgniteInternalException;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -38,4 +39,13 @@ public interface SqlSchemaManager {
      * @return The table.
      */
     IgniteTable tableById(UUID id);
+
+    /**
+     * Returns the index by id.
+     *
+     * @param id An id of required index.
+     * @return Appropriate index instance.
+     * @throws IgniteInternalException if not found.
+     */
+    IgniteIndex indexById(UUID id);
 }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManagerImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManagerImpl.java
index e5bd3a9ea..62b98155b 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManagerImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/SqlSchemaManagerImpl.java
@@ -33,6 +33,7 @@ import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.tools.Frameworks;
+import org.apache.ignite.internal.idx.IndexManager;
 import org.apache.ignite.internal.idx.InternalSortedIndex;
 import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
@@ -41,6 +42,7 @@ import org.apache.ignite.internal.sql.engine.extension.SqlExtension.ExternalSche
 import org.apache.ignite.internal.table.TableImpl;
 import org.apache.ignite.internal.table.distributed.TableManager;
 import org.apache.ignite.lang.IgniteInternalException;
+import org.apache.ignite.lang.IgniteLogger;
 import org.apache.ignite.lang.IgniteStringFormatter;
 import org.apache.ignite.lang.NodeStoppingException;
 import org.jetbrains.annotations.NotNull;
@@ -50,25 +52,33 @@ import org.jetbrains.annotations.Nullable;
  * Holds actual schema and mutates it on schema change, requested by Ignite.
  */
 public class SqlSchemaManagerImpl implements SqlSchemaManager {
+    /** Logger. */
+    private static final IgniteLogger LOG = IgniteLogger.forClass(SqlSchemaManagerImpl.class);
+
     private final Map<String, IgniteSchema> igniteSchemas = new HashMap<>();
 
     private final Map<UUID, IgniteTable> tablesById = new ConcurrentHashMap<>();
 
+    private final Map<UUID, IgniteIndex> indexById = new ConcurrentHashMap<>();
+
     private final Map<String, Schema> externalCatalogs = new HashMap<>();
 
     private final Runnable onSchemaUpdatedCallback;
 
     private final TableManager tableManager;
 
+    private final IndexManager indexManager;
+
     private volatile SchemaPlus calciteSchema;
 
     /**
      * Constructor.
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
      */
-    public SqlSchemaManagerImpl(TableManager tableManager, Runnable onSchemaUpdatedCallback) {
+    public SqlSchemaManagerImpl(TableManager tblManager, IndexManager idxManager, Runnable onSchemaUpdatedCallback) {
+        tableManager = tblManager;
+        indexManager = idxManager;
         this.onSchemaUpdatedCallback = onSchemaUpdatedCallback;
-        this.tableManager = tableManager;
 
         SchemaPlus newCalciteSchema = Frameworks.createRootSchema(false);
         newCalciteSchema.add("PUBLIC", new IgniteSchema("PUBLIC"));
@@ -106,6 +116,25 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
         return table;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public IgniteIndex indexById(UUID id) throws IgniteInternalException {
+        IgniteIndex idx = indexById.get(id);
+
+        if (idx == null) {
+            indexManager.getIndexById(id);
+
+            idx = indexById.get(id);
+        }
+
+        if (idx == null) {
+            throw new IgniteInternalException(
+                    IgniteStringFormatter.format("Index not found [idxId={}]", id));
+        }
+
+        return idx;
+    }
+
     private void ensureTableStructuresCreated(UUID id) {
         try {
             tableManager.table(id);
@@ -163,7 +192,7 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
 
         IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);
 
-        schema.addTable(removeSchema(schemaName, table.name()), igniteTable);
+        schema.addTable(normalizeSchema(schemaName, table.name()), igniteTable);
         tablesById.put(igniteTable.id(), igniteTable);
 
         rebuild();
@@ -204,7 +233,7 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
 
         IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);
 
-        String tblName = removeSchema(schemaName, table.name());
+        String tblName = normalizeSchema(schemaName, table.name());
 
         // Rebuild indexes collation.
         igniteTable.addIndexes(schema.internalTable(tblName).indexes().values().stream()
@@ -230,6 +259,8 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
 
         InternalIgniteTable table = (InternalIgniteTable) schema.getTable(tableName);
         if (table != null) {
+            table.indexes().values().forEach(indexById::remove);
+
             tablesById.remove(table.id());
             schema.removeTable(tableName);
         }
@@ -241,7 +272,7 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
      * Build new SQL schema when new index is created.
      */
     public void onIndexCreated(String schema, String tblName, InternalSortedIndex idx) {
-        InternalIgniteTable tbl = igniteSchemas.get(schema).internalTable(removeSchema(schema, tblName));
+        InternalIgniteTable tbl = igniteSchemas.get(schema).internalTable(normalizeSchema(schema, tblName));
 
         tbl.addIndex(createIndex(idx, tbl));
 
@@ -262,20 +293,28 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
                         )
                 ).collect(Collectors.toList());
 
-        return new IgniteIndex(
-                RelCollations.of(idxFieldsCollation),
-                idx,
-                tbl
-        );
+        IgniteIndex idx0 = new IgniteIndex(RelCollations.of(idxFieldsCollation), idx, tbl);
+
+        indexById.put(idx0.id(), idx0);
+
+        return idx0;
     }
 
     /**
      * Build new SQL schema when existed index is dropped.
      */
     public void onIndexDropped(String schema, String tblName, String idxName) {
-        InternalIgniteTable tbl = igniteSchemas.get(schema).internalTable(removeSchema(schema, tblName));
+        InternalIgniteTable tbl = igniteSchemas.get(schema).internalTable(normalizeSchema(schema, tblName));
+
+        IgniteIndex idx0 = tbl.removeIndex(idxName);
 
-        tbl.removeIndex(idxName);
+        if (idx0 != null) {
+            IgniteIndex old = indexById.remove(idx0.id());
+
+            if (old == null) {
+                LOG.trace(IgniteStringFormatter.format("Index [name={}] not found in inner store.", idxName));
+            }
+        }
 
         rebuild();
     }
@@ -293,7 +332,7 @@ public class SqlSchemaManagerImpl implements SqlSchemaManager {
         onSchemaUpdatedCallback.run();
     }
 
-    private static String removeSchema(String schemaName, String canonicalName) {
+    private static String normalizeSchema(String schemaName, String canonicalName) {
         return canonicalName.substring(schemaName.length() + 1);
     }
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java
index 0085362b6..006d78cba 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java
@@ -98,6 +98,7 @@ public final class BaseQueryContext extends AbstractQueryContext {
         RelOptCluster cluster = RelOptCluster.create(DUMMY_PLANNER, DFLT_REX_BUILDER);
 
         cluster.setMetadataProvider(IgniteMetadata.METADATA_PROVIDER);
+        cluster.setMetadataQuerySupplier(RelMetadataQueryEx::create);
 
         CLUSTER = cluster;
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/MockedStructuresTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/MockedStructuresTest.java
index b6c44b42d..8bb9cd480 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/MockedStructuresTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/MockedStructuresTest.java
@@ -172,8 +172,9 @@ public class MockedStructuresTest extends IgniteAbstractTest {
     @Test
     void checkAppropriateTableFound() throws Exception {
         TableManager tableManager = mock(TableManager.class);
+        IndexManager indexManager = mock(IndexManager.class);
 
-        SqlSchemaManagerImpl schemaManager = new SqlSchemaManagerImpl(tableManager, () -> {});
+        SqlSchemaManagerImpl schemaManager = new SqlSchemaManagerImpl(tableManager, indexManager, () -> {});
         UUID tblId = UUID.randomUUID();
 
         assertTrue(assertThrows(IgniteInternalException.class, () -> schemaManager.tableById(tblId))
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReaderTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReaderTest.java
index 63e84a604..0a98de3e8 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReaderTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/externalize/RelJsonReaderTest.java
@@ -29,7 +29,10 @@ import java.util.UUID;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.schema.Statistic;
+import org.apache.ignite.internal.sql.engine.rel.AbstractIndexScan;
+import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
 import org.apache.ignite.internal.sql.engine.schema.SqlSchemaManager;
 import org.junit.jupiter.api.Test;
@@ -39,12 +42,11 @@ import org.mockito.Mockito;
  * Tests to verify {@link RelJsonReader} behaviour.
  */
 public class RelJsonReaderTest {
-
     /**
      * Test verifies that during deserialization table being resolved by its ID.
      */
     @Test
-    void fromJson() {
+    void fromJsonTblScan() {
         UUID tableId = UUID.randomUUID();
 
         IgniteTable igniteTableMock = mock(IgniteTable.class);
@@ -59,6 +61,7 @@ public class RelJsonReaderTest {
                 + "  \"rels\" : [ {\n"
                 + "    \"id\" : \"0\",\n"
                 + "    \"relOp\" : \"IgniteTableScan\",\n"
+                + "    \"table\" : [\"PUBLIC\", \"TEST\"],\n"
                 + "    \"tableId\" : \"" + tableId + "\",\n"
                 + "    \"inputs\" : [ ]\n"
                 + "  } ]\n"
@@ -71,4 +74,46 @@ public class RelJsonReaderTest {
         assertThat(node.getTable().unwrap(IgniteTable.class), is(igniteTableMock));
         Mockito.verify(schemaMock).tableById(tableId);
     }
+
+    @Test
+    void fromJsonIdxScan() {
+        UUID indexId = UUID.randomUUID();
+        UUID tableId = UUID.randomUUID();
+
+        IgniteTable igniteTableMock = mock(IgniteTable.class);
+        IgniteIndex igniteIdxMock = mock(IgniteIndex.class);
+        when(igniteTableMock.getStatistic()).thenReturn(new Statistic() {});
+        when(igniteTableMock.getRowType(any())).thenReturn(mock(RelDataType.class));
+
+        SqlSchemaManager schemaMock = mock(SqlSchemaManager.class);
+
+        when(schemaMock.tableById(tableId)).thenReturn(igniteTableMock);
+        when(schemaMock.indexById(indexId)).thenReturn(igniteIdxMock);
+
+        String json = ""
+                + "{\n"
+                + "  \"rels\" : [ {\n"
+                + "    \"id\" : \"0\",\n"
+                + "    \"relOp\" : \"IgniteIndexScan\",\n"
+                + "    \"index\" : \"idx0\",\n"
+                + "    \"table\" : [\"PUBLIC\", \"TEST\"],\n"
+                + "    \"indexId\" : \"" + indexId + "\",\n"
+                + "    \"tableId\" : \"" + tableId + "\",\n"
+                + "    \"inputs\" : [ ]\n"
+                + "  } ]\n"
+                + "}";
+
+        RelNode node = RelJsonReader.fromJson(schemaMock, json);
+
+        assertThat(node, isA(IgniteIndexScan.class));
+        assertThat(node.getTable(), notNullValue());
+        assertThat(node.getTable().unwrap(IgniteTable.class), is(igniteTableMock));
+
+        AbstractIndexScan scan = (AbstractIndexScan) node;
+        assertThat(scan.getIndex(), notNullValue());
+        assertThat(scan.getIndex(), is(igniteIdxMock));
+
+        Mockito.verify(schemaMock).tableById(tableId);
+        Mockito.verify(schemaMock).indexById(indexId);
+    }
 }
\ No newline at end of file
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
index 7a22652fb..c80be021f 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
@@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -71,6 +72,7 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql2rel.InitializerContext;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Util;
+import org.apache.ignite.internal.idx.InternalSortedIndexImpl;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.NativeType;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
@@ -100,7 +102,9 @@ import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
 import org.apache.ignite.internal.sql.engine.util.BaseQueryContext;
+import org.apache.ignite.internal.storage.index.SortedIndexStorage;
 import org.apache.ignite.internal.table.InternalTable;
+import org.apache.ignite.internal.table.TableImpl;
 import org.apache.ignite.internal.testframework.IgniteAbstractTest;
 import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.util.ArrayUtils;
@@ -312,16 +316,11 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
     }
 
     protected static void createTable(IgniteSchema schema, String name, RelDataType type, IgniteDistribution distr) {
-        TestTable table = new TestTable(type) {
+        TestTable table = new TestTable(type, name) {
             @Override
             public IgniteDistribution distribution() {
                 return distr;
             }
-
-            @Override
-            public String name() {
-                return name;
-            }
         };
 
         schema.addTable(name, table);
@@ -520,13 +519,16 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
 
         List<RelNode> deserializedNodes = new ArrayList<>();
 
-        Map<UUID, IgniteTable> tableMap = publicSchema.getTableNames().stream()
+        Map<UUID, InternalIgniteTable> tableMap = publicSchema.getTableNames().stream()
                 .map(publicSchema::getTable)
-                .map(IgniteTable.class::cast)
+                .map(InternalIgniteTable.class::cast)
                 .collect(Collectors.toMap(IgniteTable::id, Function.identity()));
 
+        Map<UUID, IgniteIndex> idxMap = tableMap.values().stream().map(InternalIgniteTable::indexes).flatMap(i -> i.values().stream())
+                .collect(Collectors.toMap(IgniteIndex::id, Function.identity()));
+
         for (String s : serialized) {
-            RelJsonReader reader = new RelJsonReader(new SqlSchemaManagerImpl(tableMap));
+            RelJsonReader reader = new RelJsonReader(new SqlSchemaManagerImpl(tableMap, idxMap));
             deserializedNodes.add(reader.read(s));
         }
 
@@ -576,6 +578,10 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
             this(type, 100.0);
         }
 
+        TestTable(RelDataType type, String name) {
+            this(name, type, 100.0);
+        }
+
         TestTable(RelDataType type, double rowCnt) {
             this(UUID.randomUUID().toString(), type, rowCnt);
         }
@@ -611,12 +617,12 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
         public IgniteLogicalIndexScan toRel(
                 RelOptCluster cluster,
                 RelOptTable relOptTbl,
-                String idxName,
+                IgniteIndex idx,
                 @Nullable List<RexNode> proj,
                 @Nullable RexNode cond,
                 @Nullable ImmutableBitSet requiredColumns
         ) {
-            return IgniteLogicalIndexScan.create(cluster, cluster.traitSet(), relOptTbl, idxName, proj, cond, requiredColumns);
+            return IgniteLogicalIndexScan.create(cluster, cluster.traitSet(), relOptTbl, idx, proj, cond, requiredColumns);
         }
 
         /** {@inheritDoc} */
@@ -735,7 +741,7 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
          * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
          */
         public TestTable addIndex(RelCollation collation, String name) {
-            indexes.put(name, new IgniteIndex(collation, name, this));
+            indexes.put(name, new TestIgniteIndex(collation, name, this));
 
             return this;
         }
@@ -748,7 +754,7 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
 
         /** {@inheritDoc} */
         @Override
-        public void removeIndex(String idxName) {
+        public IgniteIndex removeIndex(String idxName) {
             throw new AssertionError();
         }
 
@@ -928,10 +934,13 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
     }
 
     static class SqlSchemaManagerImpl implements SqlSchemaManager {
-        private final Map<UUID, IgniteTable> tablesById;
+        private final Map<UUID, InternalIgniteTable> tablesById;
+
+        private final Map<UUID, IgniteIndex> idxMap;
 
-        public SqlSchemaManagerImpl(Map<UUID, IgniteTable> tablesById) {
+        public SqlSchemaManagerImpl(Map<UUID, InternalIgniteTable> tablesById, Map<UUID, IgniteIndex> idxMap) {
             this.tablesById = tablesById;
+            this.idxMap = idxMap;
         }
 
         @Override
@@ -943,5 +952,17 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
         public IgniteTable tableById(UUID id) {
             return tablesById.get(id);
         }
+
+        @Override
+        public IgniteIndex indexById(UUID id) {
+            return idxMap.get(id);
+        }
+    }
+
+    static class TestIgniteIndex extends IgniteIndex {
+        public TestIgniteIndex(RelCollation collation, String idxName, InternalIgniteTable tbl) {
+            super(collation, new InternalSortedIndexImpl(UUID.randomUUID(), idxName, mock(SortedIndexStorage.class),
+                    mock(TableImpl.class)), tbl);
+        }
     }
 }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/JoinColocationPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/JoinColocationPlannerTest.java
index 2dcf2c7b1..6ea122581 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/JoinColocationPlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/JoinColocationPlannerTest.java
@@ -35,7 +35,6 @@ import org.apache.ignite.internal.sql.engine.rel.IgniteExchange;
 import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
 import org.apache.ignite.internal.sql.engine.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
-import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.junit.jupiter.api.Test;
@@ -56,7 +55,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        tbl.addIndex(new IgniteIndex(RelCollations.of(0), "PK", tbl));
+        tbl.addIndex(new TestIgniteIndex(RelCollations.of(0), "PK", tbl));
 
         IgniteSchema schema = createSchema(tbl);
 
@@ -89,7 +88,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        tbl.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", tbl));
+        tbl.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", tbl));
 
         IgniteSchema schema = createSchema(tbl);
 
@@ -123,7 +122,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        complexTbl.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTbl));
+        complexTbl.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTbl));
 
         TestTable simpleTbl = createTable(
                 "SIMPLE_TBL",
@@ -133,7 +132,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        simpleTbl.addIndex(new IgniteIndex(RelCollations.of(0), "PK", simpleTbl));
+        simpleTbl.addIndex(new TestIgniteIndex(RelCollations.of(0), "PK", simpleTbl));
 
         IgniteSchema schema = createSchema(complexTbl, simpleTbl);
 
@@ -172,7 +171,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        complexTblDirect.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTblDirect));
+        complexTblDirect.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTblDirect));
 
         TestTable complexTblIndirect = createTable(
                 "COMPLEX_TBL_INDIRECT",
@@ -182,7 +181,7 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
                 "VAL", String.class
         );
 
-        complexTblIndirect.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTblIndirect));
+        complexTblIndirect.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(0, 1)), "PK", complexTblIndirect));
 
         IgniteSchema schema = createSchema(complexTblDirect, complexTblIndirect);
 
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
index ebd9a981c..d518aee7f 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
@@ -98,7 +98,7 @@ public class PlannerTest extends AbstractPlannerTest {
                         .build()) {
             @Override
             public IgniteIndex getIndex(String idxName) {
-                return new IgniteIndex(null, (String) null, null);
+                return new TestIgniteIndex(null, (String) null, null);
             }
 
             @Override
@@ -756,7 +756,7 @@ public class PlannerTest extends AbstractPlannerTest {
             }
         };
 
-        emp.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(1, 2)), "emp_idx", emp));
+        emp.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(1, 2)), "emp_idx", emp));
 
         TestTable dept = new TestTable(
                 new RelDataTypeFactory.Builder(f)
@@ -770,7 +770,7 @@ public class PlannerTest extends AbstractPlannerTest {
             }
         };
 
-        dept.addIndex(new IgniteIndex(RelCollations.of(ImmutableIntList.of(1, 0)), "dep_idx", dept));
+        dept.addIndex(new TestIgniteIndex(RelCollations.of(ImmutableIntList.of(1, 0)), "dep_idx", dept));
 
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
 
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableDmlPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableDmlPlannerTest.java
index 2a85167e3..c1214d2b1 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableDmlPlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableDmlPlannerTest.java
@@ -31,7 +31,6 @@ import org.apache.calcite.rel.core.Spool;
 import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableModify;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
-import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.junit.jupiter.api.Test;
@@ -81,7 +80,7 @@ public class TableDmlPlannerTest extends AbstractPlannerTest {
     public void insertCachesIndexScan() throws Exception {
         TestTable tbl = createTable("TEST", IgniteDistributions.random(), "VAL", Integer.class);
 
-        tbl.addIndex(new IgniteIndex(RelCollations.of(0), "IDX", tbl));
+        tbl.addIndex(new TestIgniteIndex(RelCollations.of(0), "IDX", tbl));
 
         IgniteSchema schema = createSchema(tbl);
 
@@ -138,7 +137,7 @@ public class TableDmlPlannerTest extends AbstractPlannerTest {
     public void updateNotCachesNonDependentIndexScan() throws Exception {
         TestTable tbl = createTable("TEST", IgniteDistributions.random(), "VAL", Integer.class, "IDX_VAL", Integer.class);
 
-        tbl.addIndex(new IgniteIndex(RelCollations.of(1), "IDX", tbl));
+        tbl.addIndex(new TestIgniteIndex(RelCollations.of(1), "IDX", tbl));
 
         IgniteSchema schema = createSchema(tbl);
 
@@ -164,7 +163,7 @@ public class TableDmlPlannerTest extends AbstractPlannerTest {
     public void updateCachesDependentIndexScan() throws Exception {
         TestTable tbl = createTable("TEST", IgniteDistributions.random(), "VAL", Integer.class);
 
-        tbl.addIndex(new IgniteIndex(RelCollations.of(0), "IDX", tbl));
+        tbl.addIndex(new TestIgniteIndex(RelCollations.of(0), "IDX", tbl));
 
         IgniteSchema schema = createSchema(tbl);