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 2021/12/21 15:28:03 UTC

[ignite] branch sql-calcite updated: IGNITE-16107 Fix projects and filters merge into scan - Fixes #9647.

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

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


The following commit(s) were added to refs/heads/sql-calcite by this push:
     new 788a11c  IGNITE-16107 Fix projects and filters merge into scan - Fixes #9647.
788a11c is described below

commit 788a11c35bdaed2f1175af966316b0892e83c39b
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Tue Dec 21 17:32:31 2021 +0300

    IGNITE-16107 Fix projects and filters merge into scan - Fixes #9647.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../query/calcite/prepare/PlannerPhase.java        |  48 ++--
 .../rel/logical/IgniteLogicalIndexScan.java        |   6 -
 .../query/calcite/rule/FilterConverterRule.java    |  20 +-
 .../calcite/rule/logical/FilterScanMergeRule.java  | 161 ++++++-------
 .../calcite/rule/logical/LogicalOrToUnionRule.java | 125 ++++++++---
 .../calcite/rule/logical/ProjectScanMergeRule.java | 106 +++++----
 .../processors/query/calcite/util/RexUtils.java    |  19 --
 .../ServerStatisticsIntegrationTest.java           |   2 +-
 .../planner/ProjectFilterScanMergePlannerTest.java | 249 +++++++++++++++++++++
 .../apache/ignite/testsuites/PlannerTestSuite.java |   2 +
 10 files changed, 508 insertions(+), 230 deletions(-)

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 c386b4c7..893821a 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
@@ -73,10 +73,12 @@ public enum PlannerPhase {
     HEP_DECORRELATE("Heuristic phase to decorrelate subqueries") {
         /** {@inheritDoc} */
         @Override public RuleSet getRules(PlanningContext ctx) {
-            return RuleSets.ofList(
-                CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
-                CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
-                CoreRules.JOIN_SUB_QUERY_TO_CORRELATE
+            return ctx.rules(
+                RuleSets.ofList(
+                    CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
+                    CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
+                    CoreRules.JOIN_SUB_QUERY_TO_CORRELATE
+                )
             );
         }
 
@@ -90,13 +92,17 @@ public enum PlannerPhase {
     HEP_FILTER_PUSH_DOWN("Heuristic phase to push down filters") {
         /** {@inheritDoc} */
         @Override public RuleSet getRules(PlanningContext ctx) {
-            return RuleSets.ofList(
-                CoreRules.FILTER_MERGE,
-                CoreRules.FILTER_AGGREGATE_TRANSPOSE,
-                CoreRules.FILTER_SET_OP_TRANSPOSE,
-                CoreRules.JOIN_CONDITION_PUSH,
-                CoreRules.FILTER_INTO_JOIN,
-                CoreRules.FILTER_PROJECT_TRANSPOSE
+            return ctx.rules(
+                RuleSets.ofList(
+                    FilterScanMergeRule.TABLE_SCAN_SKIP_CORRELATED,
+
+                    CoreRules.FILTER_MERGE,
+                    CoreRules.FILTER_AGGREGATE_TRANSPOSE,
+                    CoreRules.FILTER_SET_OP_TRANSPOSE,
+                    CoreRules.JOIN_CONDITION_PUSH,
+                    CoreRules.FILTER_INTO_JOIN,
+                    CoreRules.FILTER_PROJECT_TRANSPOSE
+                )
             );
         }
 
@@ -110,14 +116,15 @@ public enum PlannerPhase {
     HEP_PROJECT_PUSH_DOWN("Heuristic phase to push down and merge projects") {
         /** {@inheritDoc} */
         @Override public RuleSet getRules(PlanningContext ctx) {
-            return RuleSets.ofList(
-                ProjectScanMergeRule.TABLE_SCAN_SKIP_CORRELATED,
-                ProjectScanMergeRule.INDEX_SCAN_SKIP_CORRELATED,
-
-                CoreRules.JOIN_PUSH_EXPRESSIONS,
-                CoreRules.PROJECT_MERGE,
-                CoreRules.PROJECT_REMOVE,
-                CoreRules.PROJECT_FILTER_TRANSPOSE
+            return ctx.rules(
+                RuleSets.ofList(
+                    ProjectScanMergeRule.TABLE_SCAN_SKIP_CORRELATED,
+
+                    CoreRules.JOIN_PUSH_EXPRESSIONS,
+                    CoreRules.PROJECT_MERGE,
+                    CoreRules.PROJECT_REMOVE,
+                    CoreRules.PROJECT_FILTER_TRANSPOSE
+                )
             );
         }
 
@@ -210,7 +217,8 @@ public enum PlannerPhase {
                     FilterSpoolMergeToSortedIndexSpoolRule.INSTANCE,
                     FilterSpoolMergeToHashIndexSpoolRule.INSTANCE,
 
-                    LogicalOrToUnionRule.INSTANCE,
+                    LogicalOrToUnionRule.FILTER_INSTANCE,
+                    LogicalOrToUnionRule.SCAN_INSTANCE,
 
                     CorrelatedNestedLoopJoinRule.INSTANCE,
                     CorrelateToNestedLoopRule.INSTANCE,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
index 9c6c5ec..8e7aa30 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalIndexScan.java
@@ -18,19 +18,16 @@
 package org.apache.ignite.internal.processors.query.calcite.rel.logical;
 
 import java.util.List;
-
 import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.mapping.Mappings;
 import org.apache.ignite.internal.processors.query.calcite.rel.AbstractIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
-import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
@@ -51,15 +48,12 @@ public class IgniteLogicalIndexScan extends AbstractIndexScan {
     ) {
         IgniteTable tbl = table.unwrap(IgniteTable.class);
         IgniteTypeFactory typeFactory = Commons.typeFactory(cluster);
-        RelDataType rowType = tbl.getRowType(typeFactory, requiredColumns);
         RelCollation collation = tbl.getIndex(idxName).collation();
 
         if (requiredColumns != null) {
             Mappings.TargetMapping targetMapping = Commons.mapping(requiredColumns,
                 tbl.getRowType(typeFactory).getFieldCount());
             collation = collation.apply(targetMapping);
-            if (proj != null)
-                collation = TraitUtils.projectCollation(collation, proj, rowType);
         }
 
         IndexConditions idxCond = new IndexConditions();
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
index 7320c5c..009316f 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
@@ -29,6 +29,8 @@ import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTrait;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 
 /**
@@ -46,13 +48,23 @@ public class FilterConverterRule extends AbstractIgniteConverterRule<LogicalFilt
     /** {@inheritDoc} */
     @Override protected PhysicalNode convert(RelOptPlanner planner, RelMetadataQuery mq, LogicalFilter rel) {
         RelOptCluster cluster = rel.getCluster();
-        RelTraitSet traits = rel.getTraitSet().replace(IgniteConvention.INSTANCE);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE)
+            .replace(IgniteDistributions.single());
 
         Set<CorrelationId> corrIds = RexUtils.extractCorrelationIds(rel.getCondition());
 
-        if (!corrIds.isEmpty())
-            traits = traits.replace(CorrelationTrait.correlations(corrIds));
+        if (!corrIds.isEmpty()) {
+            traits = traits
+                .replace(CorrelationTrait.correlations(corrIds))
+                .replace(RewindabilityTrait.REWINDABLE);
+        }
 
-        return new IgniteFilter(cluster, traits, rel.getInput(), rel.getCondition());
+        return new IgniteFilter(
+            cluster,
+            traits,
+            convert(rel.getInput(), traits.replace(CorrelationTrait.UNCORRELATED)),
+            rel.getCondition()
+        );
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java
index b4ff743..00ce133 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java
@@ -16,34 +16,21 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.rule.logical;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalFilter;
-import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.util.ControlFlowException;
-import org.apache.calcite.util.mapping.Mappings;
 import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilterableTableScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalTableScan;
-import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
-import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTrait;
-import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
-import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
-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;
 
@@ -54,41 +41,15 @@ public abstract class FilterScanMergeRule<T extends ProjectableFilterableTableSc
     extends RelRule<FilterScanMergeRule.Config> {
     /** Instance. */
     public static final FilterScanMergeRule<IgniteLogicalIndexScan> INDEX_SCAN =
-        new FilterScanMergeRule<IgniteLogicalIndexScan>(Config.INDEX_SCAN) {
-            /** {@inheritDoc} */
-            @Override protected IgniteLogicalIndexScan createNode(
-                RelOptCluster cluster,
-                IgniteLogicalIndexScan scan,
-                RelTraitSet traits,
-                RexNode cond) {
-                Set<CorrelationId> corrIds = RexUtils.extractCorrelationIds(cond);
-
-                if (!F.isEmpty(corrIds))
-                    traits = traits.replace(CorrelationTrait.correlations(corrIds));
-
-                return IgniteLogicalIndexScan.create(cluster, traits, scan.getTable(), scan.indexName(),
-                    scan.projects(), cond, scan.requiredColumns());
-            }
-        };
+        new FilterIndexScanMergeRule(Config.INDEX_SCAN);
 
     /** Instance. */
     public static final FilterScanMergeRule<IgniteLogicalTableScan> TABLE_SCAN =
-        new FilterScanMergeRule<IgniteLogicalTableScan>(Config.TABLE_SCAN) {
-            /** {@inheritDoc} */
-            @Override protected IgniteLogicalTableScan createNode(
-                RelOptCluster cluster,
-                IgniteLogicalTableScan scan,
-                RelTraitSet traits,
-                RexNode cond) {
-                Set<CorrelationId> corrIds = RexUtils.extractCorrelationIds(cond);
-
-                if (!F.isEmpty(corrIds))
-                    traits = traits.replace(CorrelationTrait.correlations(corrIds));
-
-                return IgniteLogicalTableScan.create(cluster, traits, scan.getTable(), scan.projects(),
-                    cond, scan.requiredColumns());
-            }
-        };
+        new FilterTableScanMergeRule(Config.TABLE_SCAN);
+
+    /** Instance. */
+    public static final FilterScanMergeRule<IgniteLogicalTableScan> TABLE_SCAN_SKIP_CORRELATED =
+        new FilterTableScanMergeRule(Config.TABLE_SCAN_SKIP_CORRELATED);
 
     /**
      * Constructor.
@@ -108,66 +69,29 @@ public abstract class FilterScanMergeRule<T extends ProjectableFilterableTableSc
         RexBuilder builder = RexUtils.builder(cluster);
 
         RexNode condition = filter.getCondition();
-        RexNode remaining = null;
-
-        if (scan.condition() != null)
-            condition = RexUtil.composeConjunction(builder, F.asList(scan.condition(), condition));
 
         if (scan.projects() != null) {
-            IgniteTypeFactory typeFactory = Commons.typeFactory(scan);
-
-            IgniteTable tbl = scan.getTable().unwrap(IgniteTable.class);
-
-            RelDataType cols = tbl.getRowType(typeFactory, scan.requiredColumns());
-
-            Mappings.TargetMapping permutation = RexUtils.permutation(scan.projects(), cols, true);
-
-            List<RexNode> conjunctions = RelOptUtil.conjunctions(condition);
-
-            List<RexNode> condition0 = new ArrayList<>(conjunctions.size());
-            List<RexNode> remaining0 = new ArrayList<>(conjunctions.size());
-
             RexShuttle shuttle = new RexShuttle() {
                 @Override public RexNode visitInputRef(RexInputRef ref) {
-                    int targetRef = permutation.getTargetOpt(ref.getIndex());
-                    if (targetRef == -1)
-                        throw new ControlFlowException();
-                    return new RexInputRef(targetRef, ref.getType());
+                    return scan.projects().get(ref.getIndex());
                 }
             };
 
-            for (RexNode cond0 : conjunctions) {
-                try {
-                    condition0.add(shuttle.apply(cond0));
-                }
-                catch (ControlFlowException e) {
-                    remaining0.add(cond0);
-                }
-            }
-
-            condition = RexUtil.composeConjunction(builder, condition0, false);
-            remaining = RexUtil.composeConjunction(builder, remaining0, true);
+            condition = shuttle.apply(condition);
         }
 
+        if (scan.condition() != null)
+            condition = RexUtil.composeConjunction(builder, F.asList(scan.condition(), condition));
+
         // We need to replace RexInputRef with RexLocalRef because TableScan doesn't have inputs.
         // TODO SEARCH support
         condition = RexUtils.replaceInputRefs(RexUtil.expandSearch(builder, null, condition));
 
-        RelTraitSet trait = scan.getTraitSet();
-        CorrelationTrait filterCorr = TraitUtils.correlation(filter);
-
-        if (filterCorr.correlated())
-            trait = trait.replace(filterCorr);
+        // Set default traits, real traits will be calculated for physical node.
+        RelTraitSet trait = cluster.traitSet();
 
         RelNode res = createNode(cluster, scan, trait, condition);
 
-        if (remaining != null) {
-            res = relBuilderFactory.create(cluster, null)
-                .push(res)
-                .filter(remaining)
-                .build();
-        }
-
         call.transformTo(res);
     }
 
@@ -175,22 +99,71 @@ public abstract class FilterScanMergeRule<T extends ProjectableFilterableTableSc
     protected abstract T createNode(RelOptCluster cluster, T scan, RelTraitSet traits, RexNode cond);
 
     /** */
+    private static class FilterIndexScanMergeRule extends FilterScanMergeRule<IgniteLogicalIndexScan> {
+        /** */
+        private FilterIndexScanMergeRule(FilterScanMergeRule.Config cfg) {
+            super(cfg);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected IgniteLogicalIndexScan createNode(
+            RelOptCluster cluster,
+            IgniteLogicalIndexScan scan,
+            RelTraitSet traits,
+            RexNode cond
+        ) {
+            return IgniteLogicalIndexScan.create(cluster, traits, scan.getTable(), scan.indexName(),
+                scan.projects(), cond, scan.requiredColumns());
+        }
+    }
+
+    /** */
+    private static class FilterTableScanMergeRule extends FilterScanMergeRule<IgniteLogicalTableScan> {
+        /** */
+        private FilterTableScanMergeRule(FilterScanMergeRule.Config cfg) {
+            super(cfg);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected IgniteLogicalTableScan createNode(
+            RelOptCluster cluster,
+            IgniteLogicalTableScan scan,
+            RelTraitSet traits,
+            RexNode cond
+        ) {
+            return IgniteLogicalTableScan.create(cluster, traits, scan.getTable(), scan.projects(),
+                cond, scan.requiredColumns());
+        }
+    }
+
+    /** */
     @SuppressWarnings("ClassNameSameAsAncestorName")
     public interface Config extends RelRule.Config {
         /** */
         Config DEFAULT = EMPTY.withRelBuilderFactory(RelFactories.LOGICAL_BUILDER).as(Config.class);
 
         /** */
-        Config TABLE_SCAN = DEFAULT.withScanRuleConfig(IgniteLogicalTableScan.class, "FilterTableScanMergeRule");
+        Config TABLE_SCAN = DEFAULT
+            .withScanRuleConfig(IgniteLogicalTableScan.class, "FilterTableScanMergeRule", false);
+
+        /** */
+        Config TABLE_SCAN_SKIP_CORRELATED = DEFAULT
+            .withScanRuleConfig(IgniteLogicalTableScan.class, "FilterTableScanMergeSkipCorrelatedRule", true);
 
         /** */
-        Config INDEX_SCAN = DEFAULT.withScanRuleConfig(IgniteLogicalIndexScan.class, "FilterIndexScanMergeRule");
+        Config INDEX_SCAN = DEFAULT
+            .withScanRuleConfig(IgniteLogicalIndexScan.class, "FilterIndexScanMergeRule", false);
 
         /** */
-        default Config withScanRuleConfig(Class<? extends ProjectableFilterableTableScan> scanCls, String desc) {
+        default Config withScanRuleConfig(
+            Class<? extends ProjectableFilterableTableScan> scanCls,
+            String desc,
+            boolean skipCorrelated
+        ) {
             return withDescription(desc)
-                .withOperandSupplier(b ->
-                    b.operand(LogicalFilter.class).oneInput(b1 -> b1.operand(scanCls).noInputs()))
+                .withOperandSupplier(b -> b.operand(LogicalFilter.class)
+                    .predicate(p -> !skipCorrelated || !RexUtils.hasCorrelation(p.getCondition()))
+                    .oneInput(b1 -> b1.operand(scanCls).noInputs()))
                 .as(Config.class);
         }
     }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/LogicalOrToUnionRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/LogicalOrToUnionRule.java
index e91d148..ea0f6a6 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/LogicalOrToUnionRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/LogicalOrToUnionRule.java
@@ -25,20 +25,64 @@ import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.tools.RelBuilder;
+import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalTableScan;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Converts OR to UNION ALL.
  */
-public class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
-    /** Instance. */
-    public static final RelOptRule INSTANCE = Config.DEFAULT.toRule();
+public abstract class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
+    /** Rule instance to replace filters. */
+    public static final RelOptRule FILTER_INSTANCE = new LogicalOrToUnionRule(Config.FILTER) {
+        @Override protected RexNode getCondition(RelOptRuleCall call) {
+            final LogicalFilter rel = call.rel(0);
+
+            return rel.getCondition();
+        }
+
+        @Override protected RelNode getInput(RelOptRuleCall call) {
+            final LogicalFilter rel = call.rel(0);
+
+            return rel.getInput();
+        }
+
+        @Override protected void buildInput(RelBuilder relBldr, RelNode input, RexNode condition) {
+            relBldr.push(input).filter(condition);
+        }
+    };
+
+    /** Rule instance to replace table scans with condition. */
+    public static final RelOptRule SCAN_INSTANCE = new LogicalOrToUnionRule(Config.SCAN) {
+        @Override protected RexNode getCondition(RelOptRuleCall call) {
+            final IgniteLogicalTableScan rel = call.rel(0);
+
+            return rel.condition();
+        }
+
+        @Override protected RelNode getInput(RelOptRuleCall call) {
+            return call.rel(0);
+        }
+
+        @Override protected void buildInput(RelBuilder relBldr, RelNode input, RexNode condition) {
+            IgniteLogicalTableScan scan = (IgniteLogicalTableScan)input;
+
+            relBldr.push(IgniteLogicalTableScan.create(
+                scan.getCluster(),
+                scan.getTraitSet(),
+                scan.getTable(),
+                scan.projects(),
+                condition,
+                scan.requiredColumns()
+            ));
+        }
+    };
 
     /**
      * Constructor.
@@ -51,20 +95,14 @@ public class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
 
     /** {@inheritDoc} */
     @Override public void onMatch(RelOptRuleCall call) {
-        final LogicalFilter rel = call.rel(0);
-        final RelOptCluster cluster = rel.getCluster();
-
-        RexNode dnf = RexUtil.toDnf(cluster.getRexBuilder(), rel.getCondition());
-
-        if (!dnf.isA(SqlKind.OR))
-            return;
+        final RelOptCluster cluster = call.rel(0).getCluster();
 
-        List<RexNode> operands = RelOptUtil.disjunctions(dnf);
+        List<RexNode> operands = getOrOperands(cluster.getRexBuilder(), getCondition(call));
 
-        if (operands.size() != 2 || RexUtil.find(SqlKind.IS_NULL).anyContain(operands))
+        if (operands == null)
             return;
 
-        RelNode input = rel.getInput(0);
+        RelNode input = getInput(call);
 
         RelNode rel0 = createUnionAll(cluster, input, operands.get(0), operands.get(1));
 
@@ -73,6 +111,30 @@ public class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
         ));
     }
 
+    /** */
+    protected abstract RexNode getCondition(RelOptRuleCall call);
+
+    /** */
+    protected abstract RelNode getInput(RelOptRuleCall call);
+
+    /** */
+    private static @Nullable List<RexNode> getOrOperands(RexBuilder rexBuilder, RexNode condition) {
+        RexNode dnf = RexUtil.toDnf(rexBuilder, condition);
+
+        if (!dnf.isA(SqlKind.OR))
+            return null;
+
+        List<RexNode> operands = RelOptUtil.disjunctions(dnf);
+
+        if (operands.size() != 2 || RexUtil.find(SqlKind.IS_NULL).anyContain(operands))
+            return null;
+
+        return operands;
+    }
+
+    /** */
+    protected abstract void buildInput(RelBuilder relBldr, RelNode input, RexNode condition);
+
     /**
      * Creates 'UnionAll' for conditions.
      *
@@ -85,11 +147,10 @@ public class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
     private RelNode createUnionAll(RelOptCluster cluster, RelNode input, RexNode op1, RexNode op2) {
         RelBuilder relBldr = relBuilderFactory.create(cluster, null);
 
+        buildInput(relBldr, input, op1);
+        buildInput(relBldr, input, relBldr.and(op2, relBldr.or(relBldr.isNull(op1), relBldr.not(op1))));
+
         return relBldr
-            .push(input).filter(op1)
-            .push(input).filter(
-                relBldr.and(op2,
-                    relBldr.or(relBldr.isNull(op1), relBldr.not(op1))))
             .union(true)
             .build();
     }
@@ -100,19 +161,21 @@ public class LogicalOrToUnionRule extends RelRule<LogicalOrToUnionRule.Config> {
         /** */
         Config DEFAULT = RelRule.Config.EMPTY
             .withRelBuilderFactory(RelFactories.LOGICAL_BUILDER)
-            .withDescription("LogicalOrToUnionRule")
-            .as(Config.class)
-            .withOperandFor(LogicalFilter.class);
-
-        /** Defines an operand tree for the given classes. */
-        default Config withOperandFor(Class<? extends Filter> filterClass) {
-            return withOperandSupplier(o -> o.operand(filterClass).anyInputs())
-                .as(Config.class);
-        }
+            .as(Config.class);
 
-        /** {@inheritDoc} */
-        @Override default LogicalOrToUnionRule toRule() {
-            return new LogicalOrToUnionRule(this);
-        }
+        /** */
+        Config FILTER = DEFAULT
+            .withDescription("FilterLogicalOrToUnionRule")
+            .withOperandSupplier(o -> o.operand(LogicalFilter.class).anyInputs())
+            .as(Config.class);
+
+        /** */
+        Config SCAN = DEFAULT
+            .withDescription("ScanLogicalOrToUnionRule")
+            .withOperandSupplier(o -> o.operand(IgniteLogicalTableScan.class)
+                .predicate(scan -> scan.condition() != null)
+                .anyInputs()
+            )
+            .as(Config.class);
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/ProjectScanMergeRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/ProjectScanMergeRule.java
index 4a5d81e..e15e843 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/ProjectScanMergeRule.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/ProjectScanMergeRule.java
@@ -18,7 +18,6 @@
 package org.apache.ignite.internal.processors.query.calcite.rule.logical;
 
 import java.util.List;
-
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelRule;
@@ -35,7 +34,6 @@ import org.apache.ignite.internal.processors.query.calcite.rel.ProjectableFilter
 import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalTableScan;
 import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
-import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
@@ -48,10 +46,6 @@ public abstract class ProjectScanMergeRule<T extends ProjectableFilterableTableS
         new ProjectIndexScanMergeRule(Config.INDEX_SCAN);
 
     /** Instance. */
-    public static final ProjectScanMergeRule<IgniteLogicalIndexScan> INDEX_SCAN_SKIP_CORRELATED =
-            new ProjectIndexScanMergeRule(Config.INDEX_SCAN_SKIP_CORRELATED);
-
-    /** Instance. */
     public static final ProjectScanMergeRule<IgniteLogicalTableScan> TABLE_SCAN =
         new ProjectTableScanMergeRule(Config.TABLE_SCAN);
 
@@ -79,12 +73,6 @@ public abstract class ProjectScanMergeRule<T extends ProjectableFilterableTableS
     }
 
     /** {@inheritDoc} */
-    @Override public boolean matches(RelOptRuleCall call) {
-        T rel = call.rel(1);
-        return rel.requiredColumns() == null;
-    }
-
-    /** {@inheritDoc} */
     @Override public void onMatch(RelOptRuleCall call) {
         LogicalProject relProject = call.rel(0);
         T scan = call.rel(1);
@@ -92,54 +80,66 @@ public abstract class ProjectScanMergeRule<T extends ProjectableFilterableTableS
         RelOptCluster cluster = scan.getCluster();
         List<RexNode> projects = relProject.getProjects();
         RexNode cond = scan.condition();
+        ImmutableBitSet requiredColumns = scan.requiredColumns();
+        List<RexNode> scanProjects = scan.projects();
 
-        // projection changes input collation and distribution.
-        RelTraitSet traits = scan.getTraitSet();
-
-        traits = traits.replace(TraitUtils.projectCollation(
-            TraitUtils.collation(traits), projects, scan.getRowType()));
-
-        traits = traits.replace(TraitUtils.projectDistribution(
-            TraitUtils.distribution(traits), projects, scan.getRowType()));
+        // Set default traits, real traits will be calculated for physical node.
+        RelTraitSet traits = cluster.traitSet();
 
         IgniteTable tbl = scan.getTable().unwrap(IgniteTable.class);
         IgniteTypeFactory typeFactory = Commons.typeFactory(cluster);
-        ImmutableBitSet.Builder builder = ImmutableBitSet.builder();
-
-        new RexShuttle() {
-            @Override public RexNode visitInputRef(RexInputRef ref) {
-                builder.set(ref.getIndex());
-                return ref;
-            }
-        }.apply(projects);
 
-        new RexShuttle() {
-            @Override public RexNode visitLocalRef(RexLocalRef inputRef) {
-                builder.set(inputRef.getIndex());
-                return inputRef;
-            }
-        }.apply(cond);
-
-        ImmutableBitSet requiredColumns = builder.build();
-
-        Mappings.TargetMapping targetMapping = Commons.mapping(requiredColumns,
-            tbl.getRowType(typeFactory).getFieldCount());
-
-        projects = new RexShuttle() {
-            @Override public RexNode visitInputRef(RexInputRef ref) {
-                return new RexLocalRef(targetMapping.getTarget(ref.getIndex()), ref.getType());
-            }
-        }.apply(projects);
+        if (requiredColumns == null) {
+            assert scanProjects == null;
+
+            ImmutableBitSet.Builder builder = ImmutableBitSet.builder();
+
+            new RexShuttle() {
+                @Override public RexNode visitInputRef(RexInputRef ref) {
+                    builder.set(ref.getIndex());
+                    return ref;
+                }
+            }.apply(projects);
+
+            new RexShuttle() {
+                @Override public RexNode visitLocalRef(RexLocalRef inputRef) {
+                    builder.set(inputRef.getIndex());
+                    return inputRef;
+                }
+            }.apply(cond);
+
+            requiredColumns = builder.build();
+
+            Mappings.TargetMapping targetMapping = Commons.mapping(requiredColumns,
+                tbl.getRowType(typeFactory).getFieldCount());
+
+            projects = new RexShuttle() {
+                @Override public RexNode visitInputRef(RexInputRef ref) {
+                    return new RexLocalRef(targetMapping.getTarget(ref.getIndex()), ref.getType());
+                }
+            }.apply(projects);
+
+            cond = new RexShuttle() {
+                @Override public RexNode visitLocalRef(RexLocalRef ref) {
+                    return new RexLocalRef(targetMapping.getTarget(ref.getIndex()), ref.getType());
+                }
+            }.apply(cond);
+        }
+        else
+            projects = RexUtils.replaceInputRefs(projects);
+
+        if (scanProjects != null) {
+            // Merge projects.
+            projects = new RexShuttle() {
+                @Override public RexNode visitLocalRef(RexLocalRef ref) {
+                    return scanProjects.get(ref.getIndex());
+                }
+            }.apply(projects);
+        }
 
         if (RexUtils.isIdentity(projects, tbl.getRowType(typeFactory, requiredColumns), true))
             projects = null;
 
-        cond = new RexShuttle() {
-            @Override public RexNode visitLocalRef(RexLocalRef ref) {
-                return new RexLocalRef(targetMapping.getTarget(ref.getIndex()), ref.getType());
-            }
-        }.apply(cond);
-
         call.transformTo(createNode(cluster, scan, traits, projects, cond, requiredColumns));
 
         if (!RexUtils.hasCorrelation(relProject.getProjects()))
@@ -227,10 +227,6 @@ public abstract class ProjectScanMergeRule<T extends ProjectableFilterableTableS
             IgniteLogicalIndexScan.class, "ProjectIndexScanMergeRule", false);
 
         /** */
-        Config INDEX_SCAN_SKIP_CORRELATED = DEFAULT.withScanRuleConfig(
-            IgniteLogicalIndexScan.class, "ProjectIndexScanMergeSkipCorrelatedRule", true);
-
-        /** */
         default Config withScanRuleConfig(
             Class<? extends ProjectableFilterableTableScan> scanCls,
             String desc,
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
index f8e0ced..e1b361e 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
@@ -438,20 +438,6 @@ public class RexUtils {
     }
 
     /** */
-    public static Mappings.TargetMapping permutation(List<RexNode> nodes, RelDataType inputRowType, boolean local) {
-        final Mappings.TargetMapping mapping =
-            Mappings.create(MappingType.PARTIAL_FUNCTION, nodes.size(), inputRowType.getFieldCount());
-
-        Class<? extends RexSlot> clazz = local ? RexLocalRef.class : RexInputRef.class;
-
-        for (Ord<RexNode> node : Ord.zip(nodes)) {
-            if (clazz.isInstance(node.e))
-                mapping.set(node.i, ((RexSlot)node.e).getIndex());
-        }
-        return mapping;
-    }
-
-    /** */
     public static Mappings.TargetMapping inversePermutation(List<RexNode> nodes, RelDataType inputRowType, boolean local) {
         final Mappings.TargetMapping mapping =
             Mappings.create(MappingType.INVERSE_FUNCTION, nodes.size(), inputRowType.getFieldCount());
@@ -466,11 +452,6 @@ public class RexUtils {
     }
 
     /** */
-    public static Mappings.TargetMapping permutation(List<RexNode> nodes, RelDataType inputRowType) {
-        return permutation(nodes, inputRowType, false);
-    }
-
-    /** */
     public static List<RexNode> replaceLocalRefs(List<RexNode> nodes) {
         return LocalRefReplacer.INSTANCE.apply(nodes);
     }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java
index e99b3ab..77b9d11 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/ServerStatisticsIntegrationTest.java
@@ -324,7 +324,7 @@ public class ServerStatisticsIntegrationTest extends AbstractBasicIntegrationTes
                 "%s > %d and %s < %d", numericField, -1, numericField, 101))
                 .matches(QueryChecker.containsResultRowCount(allRowCnt)).check();
 
-            assertQuerySrv(String.format("select /*+ DISABLE_RULE('LogicalOrToUnionRule') */ * from all_types where " +
+            assertQuerySrv(String.format("select /*+ DISABLE_RULE('ScanLogicalOrToUnionRule') */ * from all_types where " +
                 "(%s > %d and %s < %d) or " +
                 "(int_field > -1 and int_field < 101)",
                 numericField, -1, numericField, 101))
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java
new file mode 100644
index 0000000..9336c3e
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.planner;
+
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests ProjectScanMergeRule and FilterScanMergeRule.
+ */
+public class ProjectFilterScanMergePlannerTest extends AbstractPlannerTest {
+    /** Public schema. */
+    private IgniteSchema publicSchema;
+
+    /** {@inheritDoc} */
+    @Before
+    @Override public void setup() {
+        super.setup();
+
+        publicSchema = new IgniteSchema("PUBLIC");
+
+        IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
+
+        RelDataType type = new RelDataTypeFactory.Builder(f)
+            .add("A", f.createSqlType(SqlTypeName.INTEGER))
+            .add("B", f.createSqlType(SqlTypeName.INTEGER))
+            .add("C", f.createSqlType(SqlTypeName.INTEGER))
+            .build();
+
+        createTable(publicSchema, "TBL", type, IgniteDistributions.single(), null);
+    }
+
+    /** */
+    @Test
+    public void testProjectFilterMerge() throws Exception {
+        // Order of merge: ((scan + filter) + project).
+        assertPlan("SELECT a, b FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteTableScan.class)
+            .and(scan -> scan.projects() != null)
+            .and(scan -> "[$t0, $t1]".equals(scan.projects().toString()))
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(0, 1, 2).equals(scan.requiredColumns()))
+        );
+    }
+
+    /** */
+    @Test
+    public void testIdentityFilterMerge() throws Exception {
+        // Order of merge: ((scan + filter) + identity).
+        assertPlan("SELECT a, b, c FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteTableScan.class)
+            .and(scan -> scan.projects() == null)
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(0, 1, 2).equals(scan.requiredColumns()))
+        );
+    }
+
+    /** */
+    @Test
+    public void testProjectFilterMergeIndex() throws Exception {
+        // Test project and filter merge into index scan.
+        TestTable tbl = ((TestTable)publicSchema.getTable("TBL"));
+        tbl.addIndex(new IgniteIndex(RelCollations.of(2), "IDX_C", null, tbl));
+
+        // Without index condition shift.
+        assertPlan("SELECT a, b FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteIndexScan.class)
+            .and(scan -> scan.projects() != null)
+            .and(scan -> "[$t0, $t1]".equals(scan.projects().toString()))
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(0, 1, 2).equals(scan.requiredColumns()))
+            .and(scan -> "[=($t2, 0)]".equals(scan.indexConditions().lowerCondition().toString()))
+            .and(scan -> "[=($t2, 0)]".equals(scan.indexConditions().upperCondition().toString()))
+        );
+
+        // Index condition shifted according to requiredColumns.
+        assertPlan("SELECT b FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteIndexScan.class)
+            .and(scan -> scan.projects() != null)
+            .and(scan -> "[$t0]".equals(scan.projects().toString()))
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t1, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(1, 2).equals(scan.requiredColumns()))
+            .and(scan -> "[=($t1, 0)]".equals(scan.indexConditions().lowerCondition().toString()))
+            .and(scan -> "[=($t1, 0)]".equals(scan.indexConditions().upperCondition().toString()))
+        );
+    }
+
+    /** */
+    @Test
+    public void testIdentityFilterMergeIndex() throws Exception {
+        // Test project and filter merge into index scan.
+        TestTable tbl = ((TestTable)publicSchema.getTable("TBL"));
+        tbl.addIndex(new IgniteIndex(RelCollations.of(2), "IDX_C", null, tbl));
+
+        // Without index condition shift.
+        assertPlan("SELECT a, b, c FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteIndexScan.class)
+            .and(scan -> scan.projects() == null)
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(0, 1, 2).equals(scan.requiredColumns()))
+            .and(scan -> "[=($t2, 0)]".equals(scan.indexConditions().lowerCondition().toString()))
+            .and(scan -> "[=($t2, 0)]".equals(scan.indexConditions().upperCondition().toString()))
+        );
+
+        // Index condition shift and identity.
+        assertPlan("SELECT b, c FROM tbl WHERE c = 0", publicSchema, isInstanceOf(IgniteIndexScan.class)
+            .and(scan -> scan.projects() == null)
+            .and(scan -> scan.condition() != null)
+            .and(scan -> "=($t1, 0)".equals(scan.condition().toString()))
+            .and(scan -> ImmutableBitSet.of(1, 2).equals(scan.requiredColumns()))
+            .and(scan -> "[=($t1, 0)]".equals(scan.indexConditions().lowerCondition().toString()))
+            .and(scan -> "[=($t1, 0)]".equals(scan.indexConditions().upperCondition().toString()))
+        );
+    }
+
+    /** */
+    @Test
+    public void testProjectFilterProjectMerge() throws Exception {
+        // Inner query contains correlate, it prevents filter to be moved below project, and after HEP_FILTER_PUSH_DOWN
+        // phase we should have chain Project - Filter - Project - Scan. Whole this chain should be merged into a single
+        // table scan on the next phases. Order of merge: (((scan + inner project) + filter) + outer project).
+        String sql = "SELECT (SELECT a+2 FROM (SELECT c, a+1 AS a FROM tbl) AS t2 WHERE t2.c = t1.c) FROM tbl AS t1";
+
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteAggregate.class)
+            .and(input(isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[+(+($t0, 1), 2)]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t1, $cor0.C)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 2).equals(scan.requiredColumns()))
+            ))));
+    }
+
+    /** */
+    @Test
+    public void testIdentityFilterProjectMerge() throws Exception {
+        // The same as two projects merge, but outer project is identity and should be eliminated together with inner
+        // project by project to scan merge rule.
+        String sql = "SELECT (SELECT a FROM (SELECT a, a+1 FROM tbl) AS t2 WHERE t2.a = t1.a) FROM tbl AS t1";
+
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteAggregate.class)
+            .and(input(isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() == null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t0, $cor0.A)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0).equals(scan.requiredColumns()))
+            ))), "ProjectFilterTransposeRule");
+
+        // Filter on project that is not permutation should be merged too.
+        sql = "SELECT (SELECT a FROM (SELECT a+1 AS a FROM tbl) AS t2 WHERE t2.a = t1.a) FROM tbl AS t1";
+
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteAggregate.class)
+            .and(input(isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[+($t0, 1)]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=(+($t0, 1), $cor0.A)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0).equals(scan.requiredColumns()))
+            ))), "ProjectFilterTransposeRule");
+    }
+
+    /** */
+    @Test
+    public void testProjectFilterIdentityMerge() throws Exception {
+        // The same as two projects merge, but inner project is identity and should be eliminated by project to scan
+        // merge rule.
+        String sql = "SELECT (SELECT a+2 FROM (SELECT a, c FROM tbl) AS t2 WHERE t2.c = t1.c) FROM tbl AS t1";
+
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteAggregate.class)
+            .and(input(isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[+($t0, 2)]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t1, $cor0.C)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 2).equals(scan.requiredColumns()))
+            ))));
+    }
+
+    /** */
+    @Test
+    public void testIdentityFilterIdentityMerge() throws Exception {
+        // The same as two projects merge, but projects are identity and should be eliminated by project to scan
+        // merge rule.
+        String sql = "SELECT (SELECT c FROM (SELECT a AS c FROM tbl) AS t2 WHERE t2.c = t1.c) FROM tbl AS t1";
+
+        assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteAggregate.class)
+            .and(input(isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() == null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t0, $cor0.C)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0).equals(scan.requiredColumns()))
+            ))));
+    }
+
+    /** */
+    @Test
+    public void testFilterProjectFilterMerge() throws Exception {
+        String sql = "SELECT * FROM (SELECT c, a FROM tbl WHERE a = 1) WHERE c = 1";
+
+        assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[$t1, $t0]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "AND(=($t0, 1), =($t1, 1))".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 2).equals(scan.requiredColumns())),
+            "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
+    }
+
+    /** */
+    @Test
+    public void testFilterIdentityFilterMerge() throws Exception {
+        String sql = "SELECT * FROM (SELECT a, c FROM tbl WHERE a = 1) WHERE c = 1";
+
+        assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class)
+                .and(scan -> scan.projects() == null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "AND(=($t0, 1), =($t1, 1))".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 2).equals(scan.requiredColumns())),
+            "ProjectFilterTransposeRule", "FilterProjectTransposeRule");
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
index baa407e..5de95d3 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
@@ -29,6 +29,7 @@ import org.apache.ignite.internal.processors.query.calcite.planner.JoinWithUsing
 import org.apache.ignite.internal.processors.query.calcite.planner.LimitOffsetPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.MergeJoinPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.PlannerTest;
+import org.apache.ignite.internal.processors.query.calcite.planner.ProjectFilterScanMergePlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.SetOpPlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.SortAggregatePlannerTest;
 import org.apache.ignite.internal.processors.query.calcite.planner.SortedIndexSpoolPlannerTest;
@@ -65,6 +66,7 @@ import org.junit.runners.Suite;
     StatisticsPlannerTest.class,
     CorrelatedSubqueryPlannerTest.class,
     JoinWithUsingPlannerTest.class,
+    ProjectFilterScanMergePlannerTest.class,
 })
 public class PlannerTestSuite {
 }