You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2022/11/11 07:13:47 UTC

[ignite] branch master updated: IGNITE-17250 SQL Calcite: Make 'min()/max()' use first/last index value - Fixes #10153.

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

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


The following commit(s) were added to refs/heads/master by this push:
     new ad0c63cd1f2 IGNITE-17250 SQL Calcite: Make 'min()/max()' use first/last index value - Fixes #10153.
ad0c63cd1f2 is described below

commit ad0c63cd1f2bb8219a34d7a3ac2b6607fb019e69
Author: vladsz83 <vl...@gmail.com>
AuthorDate: Fri Nov 11 10:08:36 2022 +0300

    IGNITE-17250 SQL Calcite: Make 'min()/max()' use first/last index value - Fixes #10153.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../query/calcite/exec/IndexFirstLastScan.java     | 135 ++++++++++++++
 .../processors/query/calcite/exec/IndexScan.java   |  37 +++-
 .../query/calcite/exec/LogicalRelImplementor.java  |  49 ++++++
 .../query/calcite/exec/exp/agg/Accumulators.java   |   2 +-
 .../calcite/metadata/IgniteMdFragmentMapping.java  |   9 +
 .../processors/query/calcite/prepare/Cloner.java   |   6 +
 .../query/calcite/prepare/IgniteRelShuttle.java    |   6 +
 .../query/calcite/prepare/PlannerPhase.java        |   2 +
 .../query/calcite/rel/IgniteIndexBound.java        | 194 +++++++++++++++++++++
 .../query/calcite/rel/IgniteRelVisitor.java        |   6 +
 .../query/calcite/rule/IndexMinMaxRule.java        |  95 ++++++++++
 .../query/calcite/schema/CacheIndexImpl.java       |  26 +++
 .../query/calcite/schema/IgniteIndex.java          |  16 ++
 .../query/calcite/schema/SystemViewIndexImpl.java  |  11 ++
 .../calcite/exec/LogicalRelImplementorTest.java    |  50 ++++++
 .../integration/AbstractBasicIntegrationTest.java  |  10 ++
 .../integration/AggregatesIntegrationTest.java     | 114 +++++++++++-
 .../integration/IndexRebuildIntegrationTest.java   |  60 +++++++
 .../calcite/planner/HashAggregatePlannerTest.java  |  89 +++++++++-
 .../calcite/planner/IndexRebuildPlannerTest.java   |  76 +++++++-
 .../java/org/apache/ignite/cache/QueryEntity.java  |   2 +-
 .../query/index/sorted/SortedSegmentedIndex.java   |   8 +
 .../index/sorted/client/ClientInlineIndex.java     |   5 +
 .../query/index/sorted/inline/InlineIndexImpl.java |  76 +++++++-
 .../sorted/inline/InlineTreeFilterClosure.java     |   2 +-
 25 files changed, 1055 insertions(+), 31 deletions(-)

diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java
new file mode 100644
index 00000000000..aacada64133
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexFirstLastScan.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.query.calcite.exec;
+
+import java.util.List;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.ImmutableIntList;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexImpl;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.io.InlineIO;
+import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
+import org.apache.ignite.internal.cache.query.index.sorted.keys.NullIndexKey;
+import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
+import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor;
+import org.apache.ignite.internal.util.lang.GridCursor;
+import org.apache.ignite.internal.util.typedef.F;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Takes only first or last index value excluding nulls.
+ */
+public class IndexFirstLastScan<Row> extends IndexScan<Row> {
+    /**
+     * @param first {@code True} to take first index value. {@code False} to take last value.
+     * @param ectx Execution context.
+     * @param desc Table descriptor.
+     * @param idx Physical index.
+     * @param idxFieldMapping Mapping from index keys to row fields.
+     * @param parts Mapping from index keys to row fields.
+     * @param requiredColumns Required columns.
+     */
+    public IndexFirstLastScan(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        CacheTableDescriptor desc,
+        InlineIndexImpl idx,
+        ImmutableIntList idxFieldMapping,
+        int[] parts,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        super(ectx, desc, new FirstLastIndexWrapper(idx, first), idxFieldMapping, parts, null, null, null,
+            requiredColumns);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IndexQueryContext indexQueryContext() {
+        IndexQueryContext res = super.indexQueryContext();
+
+        BPlusTree.TreeRowClosure<IndexRow, IndexRow> f = res.rowFilter();
+
+        List<InlineIndexKeyType> inlineKeyTypes = idx.segment(0).rowHandler().inlineIndexKeyTypes();
+
+        InlineIndexKeyType keyType = F.isEmpty(inlineKeyTypes) ? null : inlineKeyTypes.get(0);
+
+        return new IndexQueryContext(
+            res.cacheFilter(),
+            new BPlusTree.TreeRowClosure<IndexRow, IndexRow>() {
+                /** {@inheritDoc} */
+                @Override public boolean apply(
+                    BPlusTree<IndexRow, IndexRow> tree,
+                    BPlusIO<IndexRow> io,
+                    long pageAddr,
+                    int idx
+                ) throws IgniteCheckedException {
+                    if (f != null && !f.apply(tree, io, pageAddr, idx))
+                        return false;
+
+                    if (keyType != null && io instanceof InlineIO) {
+                        IndexKey key = keyType.get(pageAddr, io.offset(idx), ((InlineIO)io).inlineSize());
+
+                        if (key != null)
+                            return key != NullIndexKey.INSTANCE;
+                    }
+
+                    return io.getLookupRow(tree, pageAddr, idx).key(0).type() != IndexKeyType.NULL;
+                }
+            },
+            res.mvccSnapshot()
+        );
+    }
+
+    /** */
+    private static class FirstLastIndexWrapper extends IndexScan.TreeIndexWrapper {
+        /** */
+        private final boolean first;
+
+        /**
+         * @param idx   Index
+         * @param first {@code True} to take first index value. {@code False} to take last value.
+         */
+        protected FirstLastIndexWrapper(InlineIndexImpl idx, boolean first) {
+            super(idx);
+            this.first = first;
+        }
+
+        /** {@inheritDoc} */
+        @Override public GridCursor<IndexRow> find(
+            IndexRow lower,
+            IndexRow upper,
+            boolean lowerInclude,
+            boolean upperInclude,
+            IndexQueryContext qctx
+        ) {
+            assert lower == null && upper == null;
+            assert lowerInclude && upperInclude;
+
+            try {
+                return idx.findFirstOrLast(qctx, first);
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException("Failed to take " + (first ? "first" : "last") + " not-null index value.", e);
+            }
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
index a21308e2a44..7a6ca9ea761 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
@@ -86,7 +86,7 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
     private final ImmutableBitSet requiredColumns;
 
     /** */
-    private final InlineIndex idx;
+    protected final InlineIndex idx;
 
     /** Mapping from index keys to row fields. */
     private final ImmutableIntList idxFieldMapping;
@@ -98,7 +98,7 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
      * @param ectx Execution context.
      * @param desc Table descriptor.
      * @param idxFieldMapping Mapping from index keys to row fields.
-     * @param idx Phisycal index.
+     * @param idx Physical index.
      * @param filters Additional filters.
      * @param ranges Index scan bounds.
      */
@@ -112,18 +112,41 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
         RangeIterable<Row> ranges,
         Function<Row, Row> rowTransformer,
         @Nullable ImmutableBitSet requiredColumns
+    ) {
+        this(ectx, desc, new TreeIndexWrapper(idx), idxFieldMapping, parts, filters, ranges, rowTransformer,
+            requiredColumns);
+    }
+
+    /**
+     * @param ectx Execution context.
+     * @param desc Table descriptor.
+     * @param idxFieldMapping Mapping from index keys to row fields.
+     * @param treeIdx Physical index wrapper.
+     * @param filters Additional filters.
+     * @param ranges Index scan bounds.
+     */
+    protected IndexScan(
+        ExecutionContext<Row> ectx,
+        CacheTableDescriptor desc,
+        TreeIndexWrapper treeIdx,
+        ImmutableIntList idxFieldMapping,
+        int[] parts,
+        Predicate<Row> filters,
+        RangeIterable<Row> ranges,
+        Function<Row, Row> rowTransformer,
+        @Nullable ImmutableBitSet requiredColumns
     ) {
         super(
             ectx,
             desc.rowType(ectx.getTypeFactory(), requiredColumns),
-            new TreeIndexWrapper(idx),
+            treeIdx,
             filters,
             ranges,
             rowTransformer
         );
 
         this.desc = desc;
-        this.idx = idx;
+        this.idx = treeIdx.idx;
         cctx = desc.cacheContext();
         kctx = cctx.kernalContext();
 
@@ -283,12 +306,12 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
     }
 
     /** */
-    private static class TreeIndexWrapper implements TreeIndex<IndexRow> {
+    protected static class TreeIndexWrapper implements TreeIndex<IndexRow> {
         /** Underlying index. */
-        private final InlineIndex idx;
+        protected final InlineIndex idx;
 
         /** */
-        private TreeIndexWrapper(InlineIndex idx) {
+        protected TreeIndexWrapper(InlineIndex idx) {
             this.idx = idx;
         }
 
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index fb0a5845e1a..98bc94a8285 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -72,6 +72,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
@@ -419,6 +420,54 @@ public class LogicalRelImplementor<Row> implements IgniteRelVisitor<Node<Row>> {
         }
     }
 
+    /** {@inheritDoc} */
+    @Override public Node<Row> visit(IgniteIndexBound idxBndRel) {
+        IgniteTable tbl = idxBndRel.getTable().unwrap(IgniteTable.class);
+        IgniteIndex idx = tbl.getIndex(idxBndRel.indexName());
+        IgniteTypeFactory typeFactory = ctx.getTypeFactory();
+        ColocationGroup grp = ctx.group(idxBndRel.sourceId());
+        ImmutableBitSet requiredColumns = idxBndRel.requiredColumns();
+        RelDataType rowType = tbl.getRowType(typeFactory, requiredColumns);
+
+        if (idx != null && !tbl.isIndexRebuildInProgress())
+            return new ScanNode<>(ctx, rowType, idx.firstOrLast(idxBndRel.first(), ctx, grp, requiredColumns));
+        else {
+            assert requiredColumns.cardinality() == 1;
+
+            Iterable<Row> rowsIter = tbl.scan(
+                ctx,
+                grp,
+                r -> ctx.rowHandler().get(0, r) != null,
+                null,
+                idxBndRel.requiredColumns()
+            );
+
+            Node<Row> scanNode = new ScanNode<>(ctx, rowType, rowsIter);
+
+            RelCollation collation = idx.collation().apply(LogicalScanConverterRule.createMapping(
+                null,
+                requiredColumns,
+                tbl.getRowType(typeFactory).getFieldCount()
+            ));
+
+            Comparator<Row> cmp = expressionFactory.comparator(collation);
+
+            assert cmp != null;
+
+            SortNode<Row> sortNode = new SortNode<>(
+                ctx,
+                rowType,
+                idxBndRel.first() ? cmp : cmp.reversed(),
+                null,
+                () -> 1
+            );
+
+            sortNode.register(scanNode);
+
+            return sortNode;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override public Node<Row> visit(IgniteTableScan rel) {
         RexNode condition = rel.condition();
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
index 05fd17e1ea8..a98fc2bdb8f 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
@@ -973,7 +973,7 @@ public class Accumulators {
         private final boolean min;
 
         /** */
-        private final Function<IgniteTypeFactory, RelDataType> typeSupplier;
+        private final transient Function<IgniteTypeFactory, RelDataType> typeSupplier;
 
         /** */
         private T val;
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
index 7f38201e501..b4efd07ffcd 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdFragmentMapping.java
@@ -33,6 +33,7 @@ import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetada
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
@@ -199,6 +200,14 @@ public class IgniteMdFragmentMapping implements MetadataHandler<FragmentMappingM
             rel.getTable().unwrap(IgniteTable.class).colocationGroup(ctx));
     }
 
+    /**
+     * See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode, RelMetadataQuery, MappingQueryContext)}
+     */
+    public FragmentMapping fragmentMapping(IgniteIndexBound rel, RelMetadataQuery mq, MappingQueryContext ctx) {
+        return FragmentMapping.create(rel.sourceId(),
+            rel.getTable().unwrap(IgniteTable.class).colocationGroup(ctx));
+    }
+
     /**
      * See {@link IgniteMdFragmentMapping#fragmentMapping(RelNode, RelMetadataQuery, MappingQueryContext)}
      */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
index 606766fa7ee..149645d0c4b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/Cloner.java
@@ -24,6 +24,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
@@ -154,6 +155,11 @@ public class Cloner implements IgniteRelVisitor<IgniteRel> {
         return rel.clone(cluster, F.asList());
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteIndexBound rel) {
+        return rel.clone(cluster, F.asList());
+    }
+
     /** {@inheritDoc} */
     @Override public IgniteRel visit(IgniteIndexCount rel) {
         return rel.clone(cluster, F.asList());
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
index 65abac5aae7..a01198dcd93 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelShuttle.java
@@ -23,6 +23,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedN
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
@@ -153,6 +154,11 @@ public class IgniteRelShuttle implements IgniteRelVisitor<IgniteRel> {
         return processNode(rel);
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteIndexBound rel) {
+        return processNode(rel);
+    }
+
     /** {@inheritDoc} */
     @Override public IgniteRel visit(IgniteTableScan rel) {
         return processNode(rel);
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
index bb4de36b4e5..8de3f3018b6 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java
@@ -50,6 +50,7 @@ import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMerge
 import org.apache.ignite.internal.processors.query.calcite.rule.FilterSpoolMergeToSortedIndexSpoolRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.HashAggregateConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.IndexCountRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.IndexMinMaxRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.LogicalScanConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.MergeJoinConverterRule;
 import org.apache.ignite.internal.processors.query.calcite.rule.NestedLoopJoinConverterRule;
@@ -238,6 +239,7 @@ public enum PlannerPhase {
                     LogicalScanConverterRule.INDEX_SCAN,
                     LogicalScanConverterRule.TABLE_SCAN,
                     IndexCountRule.INSTANCE,
+                    IndexMinMaxRule.INSTANCE,
                     CollectRule.INSTANCE,
                     HashAggregateConverterRule.COLOCATED,
                     HashAggregateConverterRule.MAP_REDUCE,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexBound.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexBound.java
new file mode 100644
index 00000000000..772599d2eaf
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteIndexBound.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.rel;
+
+import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelInput;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+
+/**
+ * Extracts first or last not null index value.
+ */
+public class IgniteIndexBound extends AbstractRelNode implements SourceAwareIgniteRel {
+    /** */
+    private static final double INDEX_NULLS_EXPECTED_RATIO = 0.05f;
+
+    /** */
+    private final String idxName;
+
+    /** */
+    private final long sourceId;
+
+    /** */
+    private final boolean first;
+
+    /** */
+    private final RelOptTable tbl;
+
+    /** */
+    private final ImmutableBitSet requiredCols;
+
+    /**
+     * Ctor.
+     *
+     * @param tbl Table definition.
+     * @param cluster Cluster that this relational expression belongs to.
+     * @param traits Traits of this relational expression.
+     * @param idxName Index name.
+     * @param first {@code True} if first not null index value required. {@code False} if last.
+     * @param requiredCols Required columns.
+     */
+    public IgniteIndexBound(
+        RelOptTable tbl,
+        RelOptCluster cluster,
+        RelTraitSet traits,
+        String idxName,
+        boolean first,
+        ImmutableBitSet requiredCols
+    ) {
+        this(-1, tbl, cluster, traits, idxName, first, requiredCols);
+    }
+
+    /**
+     * Ctor.
+     *
+     * @param sourceId Source id.
+     * @param tbl Table definition.
+     * @param cluster Cluster that this relational expression belongs to.
+     * @param traits Traits of this relational expression.
+     * @param idxName Index name.
+     * @param first {@code True} if first not null index value required. {@code False} if last.
+     * @param requiredCols Required columns.
+     */
+    private IgniteIndexBound(
+        long sourceId,
+        RelOptTable tbl,
+        RelOptCluster cluster,
+        RelTraitSet traits,
+        String idxName,
+        boolean first,
+        ImmutableBitSet requiredCols
+    ) {
+        super(cluster, traits);
+
+        this.sourceId = sourceId;
+        this.tbl = tbl;
+        this.idxName = idxName;
+        this.first = first;
+        this.requiredCols = requiredCols;
+    }
+
+    /**
+     * Constructor for deserialization.
+     *
+     * @param input Serialized representation.
+     */
+    public IgniteIndexBound(RelInput input) {
+        super(input.getCluster(), input.getTraitSet());
+
+        Object srcIdObj = input.get("sourceId");
+        if (srcIdObj != null)
+            sourceId = ((Number)srcIdObj).longValue();
+        else
+            sourceId = -1L;
+
+        tbl = input.getTable("table");
+
+        idxName = input.getString("index");
+
+        first = input.getBoolean("first", true);
+
+        requiredCols = input.getBitSet("requiredCols");
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+        //Taking first or last value supposes scan until not null.
+        return planner.getCostFactory().makeCost(1.0, tbl.getRowCount() * INDEX_NULLS_EXPECTED_RATIO
+            * IgniteCost.ROW_PASS_THROUGH_COST, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelDataType deriveRowType() {
+        return tbl.unwrap(IgniteTable.class).getRowType(Commons.typeFactory(getCluster()), requiredCols);
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelWriter explainTerms(RelWriter pw) {
+        return super.explainTerms(pw)
+            .itemIf("sourceId", sourceId, sourceId != -1L)
+            .item("table", tbl.getQualifiedName())
+            .item("index", idxName)
+            .item("first", first)
+            .item("requiredCols", requiredCols);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T accept(IgniteRelVisitor<T> visitor) {
+        return visitor.visit(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel clone(RelOptCluster cluster, List<IgniteRel> inputs) {
+        return new IgniteIndexBound(sourceId, tbl, cluster, traitSet, idxName, first, requiredCols);
+    }
+
+    /** {@inheritDoc} */
+    @Override public long sourceId() {
+        return sourceId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel clone(long sourceId) {
+        return new IgniteIndexBound(sourceId, tbl, getCluster(), traitSet, idxName, first, requiredCols);
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelOptTable getTable() {
+        return tbl;
+    }
+
+    /** */
+    public String indexName() {
+        return idxName;
+    }
+
+    /**
+     * @return {@code True}, if first index record is required. {@code False}, if last index record is required.
+     */
+    public boolean first() {
+        return first;
+    }
+
+    /** */
+    public ImmutableBitSet requiredColumns() {
+        return requiredCols;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
index fc5084507de..ca9456e0b0b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRelVisitor.java
@@ -74,6 +74,12 @@ public interface IgniteRelVisitor<T> {
      */
     T visit(IgniteIndexCount rel);
 
+
+    /**
+     * See {@link IgniteRelVisitor#visit(IgniteRel)}
+     */
+    T visit(IgniteIndexBound rel);
+
     /**
      * See {@link IgniteRelVisitor#visit(IgniteRel)}
      */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexMinMaxRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexMinMaxRule.java
new file mode 100644
index 00000000000..e74ab9989e7
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IndexMinMaxRule.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.rule;
+
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.sql.SqlAggFunction;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
+import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait;
+import org.apache.ignite.internal.util.typedef.F;
+import org.immutables.value.Value;
+
+/**
+ * Tries to optimize MIN() and MAX() so that it takes only first or last not-null index record.
+ */
+@Value.Enclosing
+public class IndexMinMaxRule extends RelRule<IndexMinMaxRule.Config> {
+    /** */
+    public static final IndexMinMaxRule INSTANCE = Config.DEFAULT.toRule();
+
+    /** Ctor. */
+    private IndexMinMaxRule(IndexMinMaxRule.Config cfg) {
+        super(cfg);
+    }
+
+    /** */
+    @Override public void onMatch(RelOptRuleCall call) {
+        IgniteAggregate aggr = call.rel(0);
+        IgniteIndexScan idxScan = call.rel(1);
+        IgniteTable table = idxScan.getTable().unwrap(IgniteTable.class);
+        IgniteIndex idx = table.getIndex(idxScan.indexName());
+
+        if (
+            table.isIndexRebuildInProgress() ||
+                idxScan.condition() != null ||
+                idxScan.projects() != null ||
+                aggr.getGroupCount() > 0 ||
+                aggr.getAggCallList().stream().filter(a -> a.getAggregation().getKind() == SqlKind.MIN
+                    || a.getAggregation().getKind() == SqlKind.MAX).count() != 1 ||
+                idx.collation().getFieldCollations().isEmpty() ||
+                idx.collation().getFieldCollations().get(0).getFieldIndex() != idxScan.requiredColumns().nextSetBit(0)
+        )
+            return;
+
+        SqlAggFunction aggFun = aggr.getAggCallList().get(0).getAggregation();
+        boolean firstIdxVal = (aggFun.getKind() == SqlKind.MIN) !=
+            idx.collation().getFieldCollations().get(0).getDirection().isDescending();
+
+        IgniteIndexBound idxMinMaxRel = new IgniteIndexBound(
+            idxScan.getTable(),
+            idxScan.getCluster(),
+            idxScan.getTraitSet().replace(RewindabilityTrait.REWINDABLE),
+            idxScan.indexName(),
+            firstIdxVal,
+            idxScan.requiredColumns()
+        );
+
+        call.transformTo(aggr.clone(aggr.getCluster(), F.asList(idxMinMaxRel)));
+    }
+
+    /** The rule config. */
+    @Value.Immutable
+    public interface Config extends RelRule.Config {
+        /** */
+        IndexMinMaxRule.Config DEFAULT = ImmutableIndexMinMaxRule.Config.of()
+            .withDescription("IndexMinMaxRule")
+            .withOperandSupplier(r -> r.operand(IgniteAggregate.class)
+                .oneInput(i -> i.operand(IgniteIndexScan.class).anyInputs()));
+
+        /** {@inheritDoc} */
+        @Override default IndexMinMaxRule toRule() {
+            return new IndexMinMaxRule(this);
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
index 705c5288afb..368e4cdfd6b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
@@ -32,7 +32,9 @@ import org.apache.ignite.IgniteException;
 import org.apache.ignite.internal.cache.query.index.Index;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.IndexQueryContext;
 import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndexImpl;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.exec.IndexFirstLastScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
@@ -117,6 +119,30 @@ public class CacheIndexImpl implements IgniteIndex {
         return Collections.emptyList();
     }
 
+    /** {@inheritDoc} */
+    @Override public <Row> Iterable<Row> firstOrLast(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        UUID localNodeId = ectx.localNodeId();
+
+        if (grp.nodeIds().contains(localNodeId) && idx != null) {
+            return new IndexFirstLastScan<>(
+                first,
+                ectx,
+                tbl.descriptor(),
+                idx.unwrap(InlineIndexImpl.class),
+                collation.getKeys(),
+                grp.partitions(localNodeId),
+                requiredColumns
+            );
+        }
+
+        return Collections.emptyList();
+    }
+
     /** {@inheritDoc} */
     @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
         long cnt = 0;
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
index 5df2b9d60d3..83e76f2d346 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/IgniteIndex.java
@@ -94,4 +94,20 @@ public interface IgniteIndex {
      * @return Index records number for {@code group}.
      */
     public long count(ExecutionContext<?> ectx, ColocationGroup grp);
+
+    /**
+     * Takes only first or last not-null index value.
+     *
+     * @param first {@code True} to take first index not-null value. {@code False} for last.
+     * @param ectx Execution context.
+     * @param grp Colocation group.
+     * @param requiredColumns  Required columns.
+     * @return Index records for {@code grp}.
+     */
+    public <Row> Iterable<Row> firstOrLast(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        @Nullable ImmutableBitSet requiredColumns
+    );
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
index eb203b1c0cb..d511aa47e16 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SystemViewIndexImpl.java
@@ -26,6 +26,7 @@ import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.IgniteException;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.SystemViewScan;
 import org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
@@ -101,6 +102,16 @@ public class SystemViewIndexImpl implements IgniteIndex {
         return tbl.descriptor().systemView().size();
     }
 
+    /** {@inheritDoc} */
+    @Override public <Row> Iterable<Row> firstOrLast(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        throw new IgniteException("Taking first or last value is not implemented for system view index.");
+    }
+
     /** {@inheritDoc} */
     @Override public List<SearchBounds> toSearchBounds(
         RelOptCluster cluster,
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
index b3ec5455981..a3daeb2f2f0 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementorTest.java
@@ -47,6 +47,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.rel.SortNode;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.planner.TestTable;
 import org.apache.ignite.internal.processors.query.calcite.prepare.BaseQueryContext;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
@@ -55,6 +56,7 @@ import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactor
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
@@ -146,6 +148,54 @@ public class LogicalRelImplementorTest extends GridCommonAbstractTest {
         rexBuilder = cluster.getRexBuilder();
     }
 
+    /**
+     * Tests Index take-first execution plan is changed to Sort-Limit/Scan when index is unavailable.
+     */
+    @Test
+    public void testIndexFirstRewriter() {
+        checkIndexFirstOrLastRewriter(true);
+    }
+
+    /**
+     * Tests Index take-last execution plan is changed to Sort-Limit/Scan when index is unavailable.
+     */
+    @Test
+    public void testIndexLastRewriter() {
+        checkIndexFirstOrLastRewriter(false);
+    }
+
+    /**
+     * Tests Index take-last or take-first execution plan is changed to Sort-limit/Scan when index is unavailable.
+     */
+    private void checkIndexFirstOrLastRewriter(boolean first) {
+        int idxColumn = 2;
+
+        tbl.addIndex(QueryUtils.PRIMARY_KEY_INDEX, idxColumn);
+
+        IgniteIndexBound idxScan = new IgniteIndexBound(
+            qctx.catalogReader().getTable(F.asList("PUBLIC", "TBL")),
+            cluster,
+            cluster.traitSet(),
+            QueryUtils.PRIMARY_KEY_INDEX,
+            first,
+            ImmutableBitSet.of(idxColumn)
+        );
+
+        Node<?> node = relImplementor.visit(idxScan);
+
+        assertTrue(node instanceof ScanNode);
+        assertNull(node.sources());
+
+        tbl.markIndexRebuildInProgress(true);
+
+        node = relImplementor.visit(idxScan);
+
+        assertTrue(node instanceof SortNode);
+        assertEquals(1, (int)U.field(node, "limit"));
+        assertTrue(node.sources() != null && node.sources().size() == 1);
+        assertTrue(node.sources().get(0) instanceof ScanNode);
+    }
+
     /**
      * Tests IndexCount execution plan is changed to Collect/Scan when index is unavailable.
      */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
index 3add7c03e23..781f423493d 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
@@ -250,6 +250,16 @@ public class AbstractBasicIntegrationTest extends GridCommonAbstractTest {
         @Override public long count(ExecutionContext<?> ectx, ColocationGroup grp) {
             return delegate.count(ectx, grp);
         }
+
+        /** {@inheritDoc} */
+        @Override public <Row> Iterable<Row> firstOrLast(
+            boolean first,
+            ExecutionContext<Row> ectx,
+            ColocationGroup grp,
+            @Nullable ImmutableBitSet requiredColumns
+        ) {
+            return delegate.firstOrLast(first, ectx, grp, requiredColumns);
+        }
     }
 
     /** */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
index 105ea61324c..ca6c15f7f75 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AggregatesIntegrationTest.java
@@ -21,7 +21,11 @@ import java.util.List;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
 
@@ -29,20 +33,79 @@ import org.junit.Test;
  *
  */
 public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
+    /** */
+    @Test
+    public void testMinMaxWithTable() {
+        String[] indexes = new String[] {
+            "val0",
+            "val0 desc",
+            "val0, val1",
+            "val0 desc, val1 desc",
+            "val0 asc, val1 desc",
+            "val0 desc, val1 asc"
+        };
+
+        for (String idx : indexes) {
+            for (int backups = -1; backups < 3; ++backups) {
+                executeSql("create table tbl(id integer primary key, val0 integer, val1 float, val2 varchar) " +
+                    "with template=" + (backups < 0 ? "replicated" : "partitioned,backups=" + backups));
+
+                executeSql("create index test_idx on tbl(" + idx + ")");
+
+                fillTestTbl();
+
+                assertQuery("select min(val0) from tbl").returns(1).check();
+                assertQuery("select min(val1) from tbl").returns(10.0f).check();
+                assertQuery("select max(val0) from tbl").returns(5).check();
+                assertQuery("select max(val1) from tbl").returns(50.0f).check();
+
+                executeSql("drop table tbl");
+            }
+        }
+    }
+
+    /** */
+    private void fillTestTbl() {
+        executeSql("insert into tbl values(-1, null, null, 'value_-1')");
+        executeSql("insert into tbl values(1, 2, 20.0, 'value_1')");
+        executeSql("insert into tbl values(2, 3, 10.0, 'value_2')");
+        executeSql("insert into tbl values(3, null, 30.0, null)");
+        executeSql("insert into tbl values(4, 4, 30.0, 'value_4')");
+        executeSql("insert into tbl values(5, 5, 50.0, 'value_5')");
+        executeSql("insert into tbl values(6, 1, null, 'value_6')");
+        executeSql("insert into tbl values(7, null, 20.0, 'value_7')");
+        executeSql("insert into tbl values(8, null, null, null)");
+    }
+
+    /** */
+    @Test
+    public void testMinMaxWithEntity() {
+        for (int b = -1; b < 3; ++b) {
+            createAndPopulateIndexedTable(b, b < 0 ? CacheMode.REPLICATED : CacheMode.PARTITIONED);
+
+            assertQuery("select min(salary) from person").returns(1.0).check();
+            assertQuery("select min(descVal) from person").returns(1.0).check();
+            assertQuery("select max(salary) from person").returns(15.0).check();
+            assertQuery("select max(descVal) from person").returns(15.0).check();
+
+            client.destroyCache(TABLE_NAME);
+        }
+    }
+
     /** */
     @Test
     public void testCountWithBackupsAndCacheModes() {
         for (int b = 0; b < 2; ++b) {
-            createAndPopulateTable(b, CacheMode.PARTITIONED);
+            createAndPopulateIndexedTable(b, CacheMode.PARTITIONED);
 
-            assertQuery("select count(*) from person").returns(5L).check();
+            assertQuery("select count(*) from person").returns(7L).check();
 
             client.destroyCache(TABLE_NAME);
         }
 
-        createAndPopulateTable(0, CacheMode.REPLICATED);
+        createAndPopulateIndexedTable(0, CacheMode.REPLICATED);
 
-        assertQuery("select count(*) from person").returns(5L).check();
+        assertQuery("select count(*) from person").returns(7L).check();
     }
 
     /** */
@@ -214,4 +277,47 @@ public class AggregatesIntegrationTest extends AbstractBasicIntegrationTest {
             .returns("val1", 50L)
             .check();
     }
+
+    /** */
+    protected void createAndPopulateIndexedTable(int backups, CacheMode cacheMode) {
+        IgniteCache<Integer, IndexedEmployer> person = client.getOrCreateCache(new CacheConfiguration<Integer, IndexedEmployer>()
+            .setName(TABLE_NAME)
+            .setSqlSchema("PUBLIC")
+            .setQueryEntities(F.asList(new QueryEntity(Integer.class, IndexedEmployer.class).setTableName(TABLE_NAME)))
+            .setCacheMode(cacheMode)
+            .setBackups(backups)
+        );
+
+        int idx = 0;
+
+        person.put(idx++, new IndexedEmployer("Igor", 5d, 7d));
+        person.put(idx++, new IndexedEmployer(null, 3d, null));
+        person.put(idx++, new IndexedEmployer("Ilya", 1d, 1d));
+        person.put(idx++, new IndexedEmployer("Roma", null, 9d));
+        person.put(idx++, new IndexedEmployer(null, null, null));
+        person.put(idx++, new IndexedEmployer("Oleg", 15d, 15d));
+        person.put(idx++, new IndexedEmployer("Maya", null, null));
+    }
+
+    /** */
+    public static class IndexedEmployer {
+        /** */
+        @QuerySqlField
+        public String name;
+
+        /** */
+        @QuerySqlField(index = true)
+        public Double salary;
+
+        /** */
+        @QuerySqlField(index = true, descending = true)
+        public Double descVal;
+
+        /** */
+        public IndexedEmployer(String name, Double salary, Double descVal) {
+            this.name = name;
+            this.salary = salary;
+            this.descVal = salary;
+        }
+    }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
index 152f91e1156..032b7f90f35 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java
@@ -208,6 +208,66 @@ public class IndexRebuildIntegrationTest extends AbstractBasicIntegrationTest {
         }
     }
 
+    /**
+     * Test min-max optimization with index rebuilding.
+     */
+    @Test
+    public void testIndexFirstLastAtUnavailableIndex() throws IgniteCheckedException {
+        int records = 50;
+        int iterations = 500;
+
+        CalciteQueryProcessor srvEngine = Commons.lookupComponent(grid(0).context(), CalciteQueryProcessor.class);
+
+        for (boolean asc : new boolean[] {true, false}) {
+            for (int backups = -1; backups < 3; ++backups) {
+                String ddl = "CREATE TABLE tbl3 (id INT PRIMARY KEY, val BIGINT, val2 VARCHAR) WITH ";
+
+                ddl += backups < 0 ? "TEMPLATE=REPLICATED" : "TEMPLATE=PARTITIONED,backups=" + backups;
+
+                executeSql(ddl);
+
+                executeSql("CREATE INDEX TEST_IDX ON tbl3(val " + (asc ? "asc)" : "desc)"));
+
+                executeSql("INSERT INTO tbl3 VALUES (-1, null, null)");
+
+                for (int i = 0; i < records; i++)
+                    executeSql("INSERT INTO tbl3 VALUES (?, ?, ?)", i, i, "val" + i);
+
+                executeSql("INSERT INTO tbl3 VALUES (" + records + ", null, null)");
+
+                IgniteCacheTable tbl3 = (IgniteCacheTable)srvEngine.schemaHolder().schema("PUBLIC").getTable("TBL3");
+
+                AtomicBoolean stop = new AtomicBoolean();
+
+                tbl3.markIndexRebuildInProgress(false);
+
+                IgniteInternalFuture<?> fut = GridTestUtils.runAsync(() -> {
+                    boolean lever = true;
+
+                    while (!stop.get())
+                        tbl3.markIndexRebuildInProgress(lever = !lever);
+                });
+
+                try {
+                    for (int i = 0; i < iterations; ++i) {
+                        assertQuery("select MIN(val) from tbl3").returns((long)0).check();
+
+                        assertQuery("select MAX(val) from tbl3").returns((long)(records - 1)).check();
+                    }
+                }
+                finally {
+                    stop.set(true);
+
+                    tbl3.markIndexRebuildInProgress(false);
+
+                    executeSql("DROP TABLE tbl3");
+                }
+
+                fut.get();
+            }
+        }
+    }
+
     /**
      * Test IndexCount is disabled at index rebuilding.
      */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
index 403d7465876..01122091fe0 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashAggregatePlannerTest.java
@@ -19,9 +19,10 @@ package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.Arrays;
 import java.util.List;
-
 import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.fun.SqlAvgAggFunction;
 import org.apache.calcite.sql.fun.SqlCountAggFunction;
@@ -29,6 +30,7 @@ import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.rel.agg.IgniteMapHashAggregate;
@@ -43,11 +45,79 @@ import org.hamcrest.core.IsInstanceOf;
 import org.junit.Assert;
 import org.junit.Test;
 
+import static org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils.createFieldCollation;
+
 /**
  *
  */
 @SuppressWarnings({"TooBroadScope", "FieldCanBeLocal", "TypeMayBeWeakened"})
 public class HashAggregatePlannerTest extends AbstractAggregatePlannerTest {
+    /** */
+    @Test
+    public void indexMinMax() throws Exception {
+        IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
+
+        RelCollation[] collations = new RelCollation[] {
+            RelCollations.of(createFieldCollation(1, true)),
+            RelCollations.of(createFieldCollation(1, false)),
+            RelCollations.of(createFieldCollation(1, true), createFieldCollation(2, true)),
+            RelCollations.of(createFieldCollation(1, false), createFieldCollation(2, false)),
+            RelCollations.of(createFieldCollation(1, true), createFieldCollation(2, false)),
+            RelCollations.of(createFieldCollation(1, false), createFieldCollation(2, true))
+        };
+
+        for (RelCollation cll : collations) {
+            for (IgniteDistribution distr : distributions()) {
+                TestTable tbl = createTable(distr);
+                publicSchema.addTable("TEST", tbl);
+
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST", publicSchema);
+
+                tbl.addIndex(cll, "TEST_IDX");
+
+                boolean targetFldIdxAcc = !cll.getFieldCollations().get(0).direction.isDescending();
+
+                assertIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST", targetFldIdxAcc, publicSchema);
+                assertIndexFirstOrLastRecord("SELECT MAX(VAL0) FROM TEST", !targetFldIdxAcc, publicSchema);
+                assertIndexFirstOrLastRecord("SELECT MIN(V) FROM (SELECT MIN(VAL0) AS V FROM TEST)",
+                    targetFldIdxAcc, publicSchema);
+                assertIndexFirstOrLastRecord("SELECT MAX(V) FROM (SELECT MAX(VAL0) AS V FROM TEST)",
+                    !targetFldIdxAcc, publicSchema);
+                assertIndexFirstOrLastRecord("SELECT MAX(VAL0) FROM TEST", !targetFldIdxAcc, publicSchema);
+
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST GROUP BY GRP0", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST GROUP BY GRP0, GRP1 ORDER BY GRP1 DESC",
+                    publicSchema);
+
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL1) FROM TEST", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT MAX(VAL1) FROM TEST", publicSchema);
+
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0) FROM TEST WHERE VAL1 > 1", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT MAX(VAL0) FROM TEST WHERE VAL1 > 1", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT VAL1, MIN(VAL0) FROM TEST GROUP BY VAL1", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT VAL1, MAX(VAL0) FROM TEST GROUP BY VAL1", publicSchema);
+
+                assertNoIndexFirstOrLastRecord("SELECT MIN(VAL0 + 1) FROM TEST", publicSchema);
+                assertNoIndexFirstOrLastRecord("SELECT MAX(VAL0 + 1) FROM TEST", publicSchema);
+
+                publicSchema.removeTable("TEST");
+            }
+        }
+    }
+
+    /** */
+    private void assertIndexFirstOrLastRecord(String sql, boolean first, IgniteSchema schema) throws Exception {
+        assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+            .and(nodeOrAnyChild(isInstanceOf(IgniteIndexBound.class)
+                .and(s -> first && s.first() || !first && !s.first())))));
+    }
+
+    /** */
+    private void assertNoIndexFirstOrLastRecord(String sql, IgniteSchema publicSchema) throws Exception {
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteIndexBound.class)).negate());
+    }
+
     /**
      * Tests COUNT(...) plan with and without IndexCount optimization.
      */
@@ -55,13 +125,7 @@ public class HashAggregatePlannerTest extends AbstractAggregatePlannerTest {
     public void indexCount() throws Exception {
         IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
 
-        List<IgniteDistribution> lst = ImmutableList.of(
-            IgniteDistributions.single(),
-            IgniteDistributions.random(),
-            IgniteDistributions.broadcast(),
-            IgniteDistributions.hash(ImmutableList.of(0, 1, 2, 3)));
-
-        for (IgniteDistribution distr : lst) {
+        for (IgniteDistribution distr : distributions()) {
             TestTable tbl = createTable(distr);
             publicSchema.addTable("TEST", tbl);
 
@@ -108,6 +172,15 @@ public class HashAggregatePlannerTest extends AbstractAggregatePlannerTest {
         }
     }
 
+    /** */
+    private static List<IgniteDistribution> distributions() {
+        return ImmutableList.of(
+            IgniteDistributions.single(),
+            IgniteDistributions.random(),
+            IgniteDistributions.broadcast(),
+            IgniteDistributions.hash(ImmutableList.of(0, 1, 2, 3)));
+    }
+
     /** */
     private void assertIndexCount(String sql, IgniteSchema schema) throws Exception {
         assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
index 114322ac88f..14c100a8871 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexRebuildPlannerTest.java
@@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexBound;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexCount;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
@@ -32,6 +33,8 @@ import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
 
 import static org.apache.calcite.sql.SqlKind.COUNT;
+import static org.apache.calcite.sql.SqlKind.MAX;
+import static org.apache.calcite.sql.SqlKind.MIN;
 import static org.apache.calcite.sql.SqlKind.SUM0;
 
 /**
@@ -49,7 +52,7 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
         super.setup();
 
         tbl = createTable("TBL", 100, IgniteDistributions.single(), "ID", Integer.class, "VAL", String.class)
-            .addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0);
+            .addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0).addIndex("TBL_VAL_IDX", 1);
 
         publicSchema = createSchema(tbl);
     }
@@ -92,6 +95,39 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
             .and(nodeOrAnyChild(isInstanceOf(IgniteIndexCount.class)))));
     }
 
+    /** Test Index min/max (first/last) is disabled when index is unavailable. */
+    @Test
+    public void testIndexMinMaxAtIndexRebuild() throws Exception {
+        checkMinMaxOptimized();
+
+        tbl.markIndexRebuildInProgress(true);
+
+        assertPlan("SELECT MIN(VAL) FROM TBL", publicSchema, isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == MIN).count() == 1)
+            .and(hasChildThat(isTableScan("TBL"))));
+
+        assertPlan("SELECT MAX(VAL) FROM TBL", publicSchema, isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == MAX).count() == 1)
+            .and(hasChildThat(isTableScan("TBL"))));
+
+        tbl.markIndexRebuildInProgress(false);
+
+        checkMinMaxOptimized();
+    }
+
+    /** */
+    private void checkMinMaxOptimized() throws Exception {
+        assertPlan("SELECT MIN(VAL) FROM TBL", publicSchema, isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == MIN).count() == 1)
+            .and(hasChildThat(isInstanceOf(IgniteIndexBound.class)
+                .and(is -> "TBL_VAL_IDX".equals(is.indexName())))));
+
+        assertPlan("SELECT MAX(VAL) FROM TBL", publicSchema, isInstanceOf(IgniteAggregate.class)
+            .and(a -> a.getAggCallList().stream().filter(agg -> agg.getAggregation().getKind() == MAX).count() == 1)
+            .and(hasChildThat(isInstanceOf(IgniteIndexBound.class)
+                .and(is -> "TBL_VAL_IDX".equals(is.indexName())))));
+    }
+
     /** */
     @Test
     public void testConcurrentIndexRebuildStateChange() throws Exception {
@@ -147,4 +183,42 @@ public class IndexRebuildPlannerTest extends AbstractPlannerTest {
 
         fut.get();
     }
+
+    /**
+     * Test Index min/max is disabled when index becomes unavailable.
+     */
+    @Test
+    public void testIndexMinMaxAtConcurrentIndexRebuild() throws Exception {
+        AtomicBoolean stop = new AtomicBoolean();
+
+        IgniteInternalFuture<?> fut = GridTestUtils.runAsync(() -> {
+            boolean lever = true;
+
+            while (!stop.get())
+                tbl.markIndexRebuildInProgress(lever = !lever);
+        });
+
+        tbl.markIndexRebuildInProgress(true);
+
+        try {
+            for (int i = 0; i < 500; i++) {
+                assertPlan("SELECT MIN(VAL) FROM TBL", publicSchema,
+                    nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+                        .and(input(isInstanceOf(IgniteIndexBound.class)
+                            .and(is -> "TBL_VAL_IDX".equals(is.indexName())))
+                            .or(input(isInstanceOf(IgniteTableScan.class))))));
+
+                assertPlan("SELECT MAX(VAL) FROM TBL", publicSchema,
+                    nodeOrAnyChild(isInstanceOf(IgniteAggregate.class)
+                        .and(input(isInstanceOf(IgniteIndexBound.class)
+                            .and(is -> "TBL_VAL_IDX".equals(is.indexName())))
+                            .or(input(isInstanceOf(IgniteTableScan.class))))));
+            }
+        }
+        finally {
+            stop.set(true);
+        }
+
+        fut.get();
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/cache/QueryEntity.java b/modules/core/src/main/java/org/apache/ignite/cache/QueryEntity.java
index cf4f13ea61d..41a4b4c37f3 100644
--- a/modules/core/src/main/java/org/apache/ignite/cache/QueryEntity.java
+++ b/modules/core/src/main/java/org/apache/ignite/cache/QueryEntity.java
@@ -102,7 +102,7 @@ public class QueryEntity implements Serializable {
 
     /** Scale for fields. */
     private Map<String, Integer> fieldsScale = new HashMap<>();
-    
+
     /**
      * Creates an empty query entity.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
index 03a6099f6cc..1db578257d3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java
@@ -85,6 +85,14 @@ public interface SortedSegmentedIndex extends Index {
     public GridCursor<IndexRow> findLast(int segment, IndexQueryContext qryCtx)
         throws IgniteCheckedException;
 
+    /**
+     * Takes only one first or last index record.
+     *
+     * @param qryCtx External index qyery context.
+     * @param first {@code True} to take first index value. {@code False} to take last index value.
+     */
+    public GridCursor<IndexRow> findFirstOrLast(IndexQueryContext qryCtx, boolean first) throws IgniteCheckedException;
+
     /**
      * Counts index rows in specified tree segment.
      *
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
index 529b0e8f2f7..9ae649ec00c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/client/ClientInlineIndex.java
@@ -91,6 +91,11 @@ public class ClientInlineIndex extends AbstractClientIndex implements InlineInde
         throw unsupported();
     }
 
+    /** {@inheritDoc} */
+    @Override public GridCursor<IndexRow> findFirstOrLast(IndexQueryContext qryCtx, boolean first) {
+        throw unsupported();
+    }
+
     /** {@inheritDoc} */
     @Override public long count(int segment) {
         throw unsupported();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
index bbabaf91d6e..2d65ac03bca 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineIndexImpl.java
@@ -17,8 +17,11 @@
 
 package org.apache.ignite.internal.cache.query.index.sorted.inline;
 
+import java.util.ArrayDeque;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.PriorityQueue;
+import java.util.Queue;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.ReadWriteLock;
@@ -121,6 +124,26 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
         }
     }
 
+    /** {@inheritDoc} */
+    @Override public GridCursor<IndexRow> findFirstOrLast(IndexQueryContext qryCtx, boolean first)
+        throws IgniteCheckedException {
+        lock.readLock().lock();
+
+        try {
+            int segmentsCnt = segmentsCount();
+
+            final GridCursor<IndexRow>[] segmentCursors = new GridCursor[segmentsCnt];
+
+            for (int i = 0; i < segmentsCnt; i++)
+                segmentCursors[i] = first ? findFirst(i, qryCtx) : findLast(i, qryCtx);
+
+            return new SingleValueSegmentedIndexCursor(segmentCursors, def);
+        }
+        finally {
+            lock.readLock().unlock();
+        }
+    }
+
     /** {@inheritDoc} */
     @Override public GridCursor<IndexRow> find(
         IndexRow lower,
@@ -452,7 +475,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
 
         assert !cctx.mvccEnabled() || v != null;
 
-        if (cacheFilter == null && v == null)
+        if (cacheFilter == null && v == null && qryCtx.rowFilter() == null)
             return null;
 
         return new InlineTreeFilterClosure(
@@ -574,7 +597,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
     /** Single cursor over multiple segments. The next value is chosen with the index row comparator. */
     private static class SegmentedIndexCursor implements GridCursor<IndexRow> {
         /** Cursors over segments. */
-        private final PriorityQueue<GridCursor<IndexRow>> cursors;
+        private final Queue<GridCursor<IndexRow>> cursors;
 
         /** Comparator to compare index rows. */
         private final Comparator<GridCursor<IndexRow>> cursorComp;
@@ -612,12 +635,7 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
                 }
             };
 
-            this.cursors = new PriorityQueue<>(cursors.length, cursorComp);
-
-            for (GridCursor<IndexRow> c: cursors) {
-                if (c.next())
-                    this.cursors.add(c);
-            }
+            this.cursors = cursorsQueue(cursors);
         }
 
         /** {@inheritDoc} */
@@ -639,5 +657,47 @@ public class InlineIndexImpl extends AbstractIndex implements InlineIndex {
         @Override public IndexRow get() throws IgniteCheckedException {
             return head;
         }
+
+        /** */
+        protected Queue<GridCursor<IndexRow>> cursorsQueue(GridCursor<IndexRow>[] cursors)
+            throws IgniteCheckedException {
+            PriorityQueue<GridCursor<IndexRow>> q = new PriorityQueue<>(cursors.length, cursorComp);
+
+            for (GridCursor<IndexRow> c: cursors) {
+                if (c.next())
+                    q.add(c);
+            }
+
+            return q;
+        }
+    }
+
+    /** First-only, single-value-only segmented cursor. */
+    private static class SingleValueSegmentedIndexCursor extends SegmentedIndexCursor {
+        /** Ctor. */
+        SingleValueSegmentedIndexCursor(
+            GridCursor<IndexRow>[] cursors,
+            SortedIndexDefinition idxDef
+        ) throws IgniteCheckedException {
+            super(cursors, idxDef);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected Queue<GridCursor<IndexRow>> cursorsQueue(GridCursor<IndexRow>[] cursors)
+            throws IgniteCheckedException {
+            Queue<GridCursor<IndexRow>> srcQueue = super.cursorsQueue(cursors);
+
+            if (!srcQueue.isEmpty()) {
+                GridCursor<IndexRow> cur = srcQueue.poll();
+
+                IndexRow row = cur.get();
+
+                assert !cur.next();
+
+                return new ArrayDeque<>(Collections.singleton(new SingleCursor<>(row)));
+            }
+
+            return srcQueue;
+        }
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
index 3ff58127dd0..70ceb250d60 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/inline/InlineTreeFilterClosure.java
@@ -64,7 +64,7 @@ public class InlineTreeFilterClosure implements BPlusTree.TreeRowClosure<IndexRo
         GridCacheContext<?, ?> cctx,
         IgniteLogger log
     ) {
-        assert (cacheFilter != null || mvccSnapshot != null) && cctx != null;
+        assert (cacheFilter != null || mvccSnapshot != null || rowFilter != null) && cctx != null;
 
         this.cacheFilter = cacheFilter;
         this.rowFilter = rowFilter;