You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by gv...@apache.org on 2020/03/20 19:53:00 UTC

[ignite] branch ignite-12248 updated: IGNITE-12820 Calcite integration. Do not use AbstarctConverter while planning. This closes #7553

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

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


The following commit(s) were added to refs/heads/ignite-12248 by this push:
     new 4654a3e  IGNITE-12820 Calcite integration. Do not use AbstarctConverter while planning. This closes #7553
4654a3e is described below

commit 4654a3e0873972afc27d12b40124b43e532be5dd
Author: Igor Seliverstov <gv...@gmail.com>
AuthorDate: Fri Mar 20 22:52:48 2020 +0300

    IGNITE-12820 Calcite integration. Do not use AbstarctConverter while planning. This closes #7553
---
 .../apache/calcite/plan/volcano/VolcanoUtils.java  |  46 -----
 .../query/calcite/exec/LogicalRelImplementor.java  |   2 +-
 .../metadata/IgniteMdDerivedDistribution.java      |  70 +++----
 .../calcite/metadata/IgniteMdDistribution.java     |  11 +-
 .../query/calcite/prepare/IgnitePlanner.java       |  23 ++-
 .../query/calcite/prepare/PlannerPhase.java        |  28 +--
 .../query/calcite/rel/IgniteConvention.java        |   7 -
 .../processors/query/calcite/rel/IgniteJoin.java   |  12 ++
 .../query/calcite/rel/IgniteReceiver.java          |  18 --
 .../processors/query/calcite/rel/IgniteRel.java    |  19 ++
 .../processors/query/calcite/rel/IgniteSender.java |  16 --
 .../query/calcite/rule/FilterConverter.java        |  70 -------
 ...luesConverter.java => FilterConverterRule.java} |  40 ++--
 ...erter.java => FilterTraitsPropagationRule.java} |  40 ++--
 .../query/calcite/rule/IgniteConverter.java        |  88 ---------
 .../query/calcite/rule/JoinConverter.java          |  75 --------
 ...ValuesConverter.java => JoinConverterRule.java} |  41 ++--
 .../calcite/rule/JoinTraitsPropagationRule.java    |  75 ++++++++
 .../query/calcite/rule/ProjectConverter.java       |  71 -------
 ...ifyConverter.java => ProjectConverterRule.java} |  43 ++---
 ...rter.java => ProjectTraitsPropagationRule.java} |  42 ++---
 .../processors/query/calcite/rule/RuleUtils.java   | 196 +++++++++++++++++++
 ...onverter.java => TableModifyConverterRule.java} |  37 ++--
 ...luesConverter.java => ValuesConverterRule.java} |  32 ++--
 .../calcite/serialize/RelToPhysicalConverter.java  |   4 +-
 .../query/calcite/trait/DistributionTraitDef.java  |  38 ++--
 .../query/calcite/trait/IgniteDistributions.java   | 209 +++++++++++----------
 .../processors/query/calcite/PlannerTest.java      |   2 +
 28 files changed, 623 insertions(+), 732 deletions(-)

diff --git a/modules/calcite/src/main/java/org/apache/calcite/plan/volcano/VolcanoUtils.java b/modules/calcite/src/main/java/org/apache/calcite/plan/volcano/VolcanoUtils.java
deleted file mode 100644
index d66861c..0000000
--- a/modules/calcite/src/main/java/org/apache/calcite/plan/volcano/VolcanoUtils.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.calcite.plan.volcano;
-
-import org.apache.calcite.plan.RelTraitSet;
-
-/**
- * Utility methods to exploit package private API.
- */
-public class VolcanoUtils {
-    /**
-     * Requests an alternative subset with relational nodes, satisfying required traits.
-     * @param subset Original subset.
-     * @param traits Required traits.
-     * @return Result subset, what contains relational nodes, satisfying required traits.
-     */
-    public static RelSubset subset(RelSubset subset, RelTraitSet traits) {
-        return subset.set.getOrCreateSubset(subset.getCluster(), traits.simplify());
-    }
-
-    /**
-     * Utility method, sets {@link VolcanoPlanner#impatient} parameter to {@code true}.
-     * @param planner Planer.
-     * @return Planer for chaining.
-     */
-    public static VolcanoPlanner impatient(VolcanoPlanner planner) {
-        planner.impatient = true;
-
-        return planner;
-    }
-}
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 baea3f2..b6e9b3b 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
@@ -97,7 +97,7 @@ public class LogicalRelImplementor implements IgniteRelVisitor<Node<Object[]>> {
     /** {@inheritDoc} */
     @Override public Node<Object[]> visit(IgniteSender rel) {
         RelTarget target = rel.target();
-        IgniteDistribution distribution = rel.targetDistribution();
+        IgniteDistribution distribution = rel.distribution();
         Destination destination = distribution.function().destination(partitionService, target.mapping(), distribution.getKeys());
 
         // Outbox fragment ID is used as exchange ID as well.
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDerivedDistribution.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDerivedDistribution.java
index 23f213c..69e7c90 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDerivedDistribution.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDerivedDistribution.java
@@ -25,7 +25,7 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.hep.HepRelVertex;
 import org.apache.calcite.plan.volcano.AbstractConverter;
 import org.apache.calcite.plan.volcano.RelSubset;
-import org.apache.calcite.plan.volcano.VolcanoUtils;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.core.Project;
@@ -41,6 +41,7 @@ import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.util.mapping.Mappings;
 import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetadata.DerivedDistribution;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
@@ -53,15 +54,6 @@ import org.apache.ignite.internal.util.typedef.F;
  */
 public class IgniteMdDerivedDistribution implements MetadataHandler<DerivedDistribution> {
     /**
-     * Holds initially requested convention. In case there is no physical nodes in interested RelSubset we need to discover
-     * another RelSubset, which holds logical ones (to calculate possible distribution types instead of actual).
-     * On a deeper layer we need to return to initially requested RelSubset because we primarily interested
-     * in physical nodes, but logical ones cannot have them as children, so, we use a value from this holder
-     * to request RelSubset of possible physical nodes of a logical parent.
-     */
-    private static final ThreadLocal<Convention> REQUESTED_CONVENTION = ThreadLocal.withInitial(() -> Convention.NONE);
-
-    /**
      * Metadata provider, responsible for distribution types derivation. It uses this implementation class under the hood.
      */
     public static final RelMetadataProvider SOURCE =
@@ -138,24 +130,25 @@ public class IgniteMdDerivedDistribution implements MetadataHandler<DerivedDistr
      * For general information see {@link IgniteMdDerivedDistribution#deriveDistributions(RelNode, RelMetadataQuery)}
      */
     public List<IgniteDistribution> deriveDistributions(RelSubset rel, RelMetadataQuery mq) {
-        rel = VolcanoUtils.subset(rel, rel.getTraitSet().replace(REQUESTED_CONVENTION.get()));
-
         HashSet<IgniteDistribution> res = new HashSet<>();
 
-        for (RelNode rel0 : rel.getRels())
+        RelSubset newSubset = subset(rel, IgniteConvention.INSTANCE);
+
+        for (RelNode rel0 : newSubset.getRels())
             res.addAll(_deriveDistributions(rel0, mq));
 
-        if (F.isEmpty(res)) {
-            // default traits + NONE convention return a set of all logical rels.
-            RelSubset newRel = VolcanoUtils.subset(rel, rel.getCluster().traitSetOf(Convention.NONE));
+        if (!F.isEmpty(res))
+            return new ArrayList<>(res);
 
-            if (newRel != rel) {
-                for (RelNode rel0 : newRel.getRels())
-                    res.addAll(_deriveDistributions(rel0, mq));
-            }
-        }
+        newSubset = subset(rel, Convention.NONE);
 
-        return new ArrayList<>(res);
+        for (RelNode rel0 : newSubset.getRels())
+            res.addAll(_deriveDistributions(rel0, mq));
+
+        if (!F.isEmpty(res))
+            return new ArrayList<>(res);
+
+        return Collections.emptyList();
     }
 
     /**
@@ -176,35 +169,22 @@ public class IgniteMdDerivedDistribution implements MetadataHandler<DerivedDistr
      * See {@link IgniteMdDerivedDistribution#deriveDistributions(RelNode, RelMetadataQuery)}
      */
     public List<IgniteDistribution> deriveDistributions(LogicalJoin rel, RelMetadataQuery mq) {
-        List<IgniteDistribution> left = _deriveDistributions(rel.getLeft(), mq);
-        List<IgniteDistribution> right = _deriveDistributions(rel.getRight(), mq);
+        List<IgniteDistributions.BiSuggestion> suggestions = IgniteDistributions.suggestJoin(
+            rel.getLeft(), rel.getRight(), rel.analyzeCondition(), rel.getJoinType());
 
-        return Commons.transform(IgniteDistributions.suggestJoin(left, right, rel.analyzeCondition(), rel.getJoinType()),
-            IgniteDistributions.BiSuggestion::out);
-    }
-
-    /**
-     * Derivation entry point. Returns actual (or possible) distribution types of given relational node.
-     * @param rel Relational node.
-     * @param convention Required convention.
-     * @param mq Metadata query instance.
-     * @return List of distribution types the given relational node may have.
-     */
-    public static List<IgniteDistribution> deriveDistributions(RelNode rel, Convention convention, RelMetadataQuery mq) {
-        try {
-            REQUESTED_CONVENTION.set(convention);
-
-            return _deriveDistributions(rel, mq);
-        }
-        finally {
-            REQUESTED_CONVENTION.remove();
-        }
+        return Commons.transform(suggestions, IgniteDistributions.BiSuggestion::out);
     }
 
     /** */
-    private static List<IgniteDistribution> _deriveDistributions(RelNode rel, RelMetadataQuery mq) {
+    public static List<IgniteDistribution> _deriveDistributions(RelNode rel, RelMetadataQuery mq) {
         assert mq instanceof RelMetadataQueryEx;
 
         return ((RelMetadataQueryEx) mq).derivedDistributions(rel);
     }
+
+    /** */
+    private static RelSubset subset(RelSubset rel, Convention convention) {
+        VolcanoPlanner planner = (VolcanoPlanner) rel.getCluster().getPlanner();
+        return planner.getSubset(rel, rel.getCluster().traitSetOf(convention), true);
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDistribution.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDistribution.java
index 2aabb56..e94a31e 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDistribution.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/metadata/IgniteMdDistribution.java
@@ -79,7 +79,7 @@ public class IgniteMdDistribution implements MetadataHandler<BuiltInMetadata.Dis
      * See {@link IgniteMdDistribution#distribution(RelNode, RelMetadataQuery)}
      */
     public IgniteDistribution distribution(IgniteRel rel, RelMetadataQuery mq) {
-        return rel.getTraitSet().getTrait(DistributionTraitDef.INSTANCE);
+        return rel.distribution();
     }
 
     /**
@@ -177,14 +177,7 @@ public class IgniteMdDistribution implements MetadataHandler<BuiltInMetadata.Dis
      * @return Join relational node distribution calculated on the basis of its inputs and join information.
      */
     public static IgniteDistribution join(RelMetadataQuery mq, RelNode left, RelNode right, JoinInfo joinInfo, JoinRelType joinType) {
-        return join(_distribution(left, mq), _distribution(right, mq), joinInfo, joinType);
-    }
-
-    /**
-     * @return Join relational node distribution calculated on the basis of its inputs distributions and join information.
-     */
-    public static IgniteDistribution join(IgniteDistribution left, IgniteDistribution right, JoinInfo joinInfo, JoinRelType joinType) {
-        return F.first(IgniteDistributions.suggestJoin(left, right, joinInfo, joinType)).out();
+        return F.first(IgniteDistributions.suggestJoin(_distribution(left, mq), _distribution(right, mq), joinInfo, joinType)).out();
     }
 
     /**
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
index aaa4d41..c3ed639 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
@@ -20,7 +20,9 @@ package org.apache.ignite.internal.processors.query.calcite.prepare;
 import com.google.common.collect.ImmutableList;
 import java.io.Reader;
 import java.util.List;
+import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCostFactory;
 import org.apache.calcite.plan.RelOptLattice;
 import org.apache.calcite.plan.RelOptMaterialization;
 import org.apache.calcite.plan.RelOptPlanner;
@@ -29,7 +31,6 @@ import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.plan.volcano.VolcanoPlanner;
-import org.apache.calcite.plan.volcano.VolcanoUtils;
 import org.apache.calcite.prepare.CalciteCatalogReader;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelRoot;
@@ -52,6 +53,7 @@ import org.apache.calcite.tools.Program;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.ValidationException;
 import org.apache.calcite.util.Pair;
+import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
 import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetadata;
@@ -245,7 +247,7 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
     /** */
     private RelOptPlanner planner() {
         if (planner == null) {
-            planner = VolcanoUtils.impatient(new VolcanoPlanner(frameworkConfig.getCostFactory(), ctx));
+            planner = new VolcanoPlannerExt(frameworkConfig.getCostFactory(), ctx);
             planner.setExecutor(rexExecutor);
 
             for (RelTraitDef<?> def : traitDefs)
@@ -287,4 +289,21 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
     private List<RelOptMaterialization> materializations() {
         return ImmutableList.of(); // TODO
     }
+
+    /** */
+    private static class VolcanoPlannerExt extends VolcanoPlanner {
+        /** */
+        private static final Boolean IMPATIENT = IgniteSystemProperties.getBoolean("IGNITE_CALCITE_PLANER_IMPATIENT", true);
+
+        /** */
+        private static final Boolean AMBITIOUS = IgniteSystemProperties.getBoolean("IGNITE_CALCITE_PLANER_AMBITIOUS", true);
+
+        /** */
+        protected VolcanoPlannerExt(RelOptCostFactory costFactory, Context externalContext) {
+            super(costFactory, externalContext);
+
+            impatient = IMPATIENT;
+            ambitious = AMBITIOUS;
+        }
+    }
 }
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 e4c3dc0..9192bdc 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
@@ -17,16 +17,18 @@
 
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
-import org.apache.calcite.plan.volcano.AbstractConverter;
 import org.apache.calcite.rel.rules.SubQueryRemoveRule;
 import org.apache.calcite.tools.Program;
 import org.apache.calcite.tools.RuleSet;
 import org.apache.calcite.tools.RuleSets;
-import org.apache.ignite.internal.processors.query.calcite.rule.FilterConverter;
-import org.apache.ignite.internal.processors.query.calcite.rule.JoinConverter;
-import org.apache.ignite.internal.processors.query.calcite.rule.ProjectConverter;
-import org.apache.ignite.internal.processors.query.calcite.rule.TableModifyConverter;
-import org.apache.ignite.internal.processors.query.calcite.rule.ValuesConverter;
+import org.apache.ignite.internal.processors.query.calcite.rule.FilterConverterRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.FilterTraitsPropagationRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.JoinConverterRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.JoinTraitsPropagationRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.ProjectConverterRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.ProjectTraitsPropagationRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.TableModifyConverterRule;
+import org.apache.ignite.internal.processors.query.calcite.rule.ValuesConverterRule;
 
 import static org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePrograms.cbo;
 import static org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePrograms.decorrelate;
@@ -42,7 +44,7 @@ public enum PlannerPhase {
         /** {@inheritDoc} */
         @Override public RuleSet getRules(PlanningContext ctx) {
             return RuleSets.ofList(
-                ValuesConverter.INSTANCE,
+                ValuesConverterRule.INSTANCE,
                 SubQueryRemoveRule.FILTER,
                 SubQueryRemoveRule.PROJECT,
                 SubQueryRemoveRule.JOIN);
@@ -59,11 +61,13 @@ public enum PlannerPhase {
         /** {@inheritDoc} */
         @Override public RuleSet getRules(PlanningContext ctx) {
             return RuleSets.ofList(
-                AbstractConverter.ExpandConversionRule.INSTANCE,
-                JoinConverter.INSTANCE,
-                ProjectConverter.INSTANCE,
-                FilterConverter.INSTANCE,
-                TableModifyConverter.INSTANCE);
+                JoinConverterRule.INSTANCE,
+                JoinTraitsPropagationRule.INSTANCE,
+                ProjectConverterRule.INSTANCE,
+                ProjectTraitsPropagationRule.INSTANCE,
+                FilterConverterRule.INSTANCE,
+                FilterTraitsPropagationRule.INSTANCE,
+                TableModifyConverterRule.INSTANCE);
         }
 
         /** {@inheritDoc} */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteConvention.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteConvention.java
index 34c4e5e..3c0cc23 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteConvention.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteConvention.java
@@ -19,7 +19,6 @@ package org.apache.ignite.internal.processors.query.calcite.rel;
 
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
-import org.apache.calcite.plan.RelTraitSet;
 
 /**
  * Ignite convention trait.
@@ -36,10 +35,4 @@ public class IgniteConvention extends Convention.Impl {
     @Override public ConventionTraitDef getTraitDef() {
         return ConventionTraitDef.INSTANCE;
     }
-
-    /** {@inheritDoc} */
-    @Override public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits, RelTraitSet toTraits) {
-        // use converters for physical nodes only.
-        return toTraits.contains(INSTANCE) && fromTraits.contains(INSTANCE);
-    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteJoin.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteJoin.java
index 9834992..2c0ff3a 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteJoin.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteJoin.java
@@ -19,11 +19,15 @@ package org.apache.ignite.internal.processors.query.calcite.rel;
 
 import java.util.Set;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelDistribution;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexNode;
 
 /**
@@ -61,4 +65,12 @@ public class IgniteJoin extends Join implements IgniteRel {
     @Override public <T> T accept(IgniteRelVisitor<T> visitor) {
         return visitor.visit(this);
     }
+
+    /** {@inheritDoc} */
+    @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+        if (distribution().getType() == RelDistribution.Type.ANY)
+            return planner.getCostFactory().makeInfiniteCost();
+
+        return super.computeSelfCost(planner, mq);
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteReceiver.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteReceiver.java
index 0e474e3..ae86e74 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteReceiver.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteReceiver.java
@@ -21,13 +21,9 @@ import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.AbstractRelNode;
-import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.ignite.internal.processors.query.calcite.prepare.Fragment;
-import org.apache.ignite.internal.processors.query.calcite.trait.DistributionTraitDef;
-import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 
 /**
  * Relational expression that receives elements from remote {@link IgniteSender}
@@ -67,18 +63,4 @@ public class IgniteReceiver extends AbstractRelNode implements IgniteRel {
     public Fragment source() {
         return source;
     }
-
-    /**
-     * @return Node distribution.
-     */
-    public IgniteDistribution distribution() {
-        return getTraitSet().getTrait(DistributionTraitDef.INSTANCE);
-    }
-
-    /**
-     * @return Node collations.
-     */
-    public List<RelCollation> collations() {
-        return getTraitSet().getTraits(RelCollationTraitDef.INSTANCE);
-    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRel.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRel.java
index 9c827f2..7ecfacf 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRel.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteRel.java
@@ -17,7 +17,12 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rel;
 
+import java.util.List;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelNode;
+import org.apache.ignite.internal.processors.query.calcite.trait.DistributionTraitDef;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
 
 /**
  * A superinterface of all Ignite relational nodes.
@@ -30,4 +35,18 @@ public interface IgniteRel extends RelNode {
      * @return Visit result.
      */
     <T> T accept(IgniteRelVisitor<T> visitor);
+
+    /**
+     * @return Node distribution.
+     */
+    default IgniteDistribution distribution() {
+        return getTraitSet().getTrait(DistributionTraitDef.INSTANCE);
+    }
+
+    /**
+     * @return Node collations.
+     */
+    default List<RelCollation> collations() {
+        return getTraitSet().getTraits(RelCollationTraitDef.INSTANCE);
+    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSender.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSender.java
index 403f34b..b95af15 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSender.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/IgniteSender.java
@@ -20,8 +20,6 @@ package org.apache.ignite.internal.processors.query.calcite.rel;
 import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.SingleRel;
 import org.apache.ignite.internal.processors.query.calcite.prepare.RelTarget;
@@ -87,21 +85,7 @@ public class IgniteSender extends SingleRel implements IgniteRel, RelTargetAware
     /**
      * @return Node distribution.
      */
-    public IgniteDistribution targetDistribution() {
-        return getTraitSet().getTrait(DistributionTraitDef.INSTANCE);
-    }
-
-    /**
-     * @return Node distribution.
-     */
     public IgniteDistribution sourceDistribution() {
         return input.getTraitSet().getTrait(DistributionTraitDef.INSTANCE);
     }
-
-    /**
-     * @return Node collations.
-     */
-    public List<RelCollation> collations() {
-        return getTraitSet().getTraits(RelCollationTraitDef.INSTANCE);
-    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverter.java
deleted file mode 100644
index 7760c11..0000000
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 java.util.List;
-import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalFilter;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDerivedDistribution;
-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.IgniteDistribution;
-import org.apache.ignite.internal.processors.query.calcite.util.Commons;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Ignite Filter converter.
- */
-public class FilterConverter extends IgniteConverter {
-    /** */
-    public static final ConverterRule INSTANCE = new FilterConverter();
-
-    /**
-     * Creates a converter.
-     */
-    public FilterConverter() {
-        super(LogicalFilter.class, "FilterConverter");
-    }
-
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalFilter filter = (LogicalFilter) rel;
-
-        RelNode input = convert(filter.getInput(), IgniteConvention.INSTANCE);
-
-        RelOptCluster cluster = rel.getCluster();
-        RelMetadataQuery mq = cluster.getMetadataQuery();
-
-        List<IgniteDistribution> distrs = IgniteMdDerivedDistribution.deriveDistributions(input, IgniteConvention.INSTANCE, mq);
-
-        return Commons.transform(distrs, d -> create(filter, input, d));
-    }
-
-    /** */
-    private static IgniteFilter create(LogicalFilter filter, RelNode input, IgniteDistribution distr) {
-        RelTraitSet traits = filter.getTraitSet()
-            .replace(distr)
-            .replace(IgniteConvention.INSTANCE);
-
-        return new IgniteFilter(filter.getCluster(), traits, convert(input, distr), filter.getCondition());
-    }
-}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
similarity index 52%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
index 0bfd593..315bf46 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterConverterRule.java
@@ -17,41 +17,37 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalValues;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
+import org.apache.calcite.rel.logical.LogicalFilter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 
 /**
  *
  */
-public class ValuesConverter extends IgniteConverter {
+public class FilterConverterRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new ValuesConverter();
+    public static final RelOptRule INSTANCE = new FilterConverterRule();
 
-    /**
-     * Creates a ConverterRule.
-     */
-    protected ValuesConverter() {
-        super(LogicalValues.class, "ValuesConverter");
+    /** */
+    public FilterConverterRule() {
+        super(operand(LogicalFilter.class, any()));
     }
 
     /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalValues values = (LogicalValues) rel;
-
-        RelTraitSet traits = values.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteMdDistribution.values(values.getRowType(), values.getTuples()));
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalFilter rel = call.rel(0);
 
-        final IgniteValues newRel = new IgniteValues(values.getCluster(), values.getRowType(), values.getTuples(), traits);
+        RelOptCluster cluster = rel.getCluster();
+        RelNode input = convert(rel.getInput(), IgniteConvention.INSTANCE);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE);
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteFilter(cluster, traits, input, rel.getCondition()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterTraitsPropagationRule.java
similarity index 52%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterTraitsPropagationRule.java
index 0bfd593..14d5a34 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/FilterTraitsPropagationRule.java
@@ -17,41 +17,39 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.RelSubset;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
 
 /**
  *
  */
-public class ValuesConverter extends IgniteConverter {
+public class FilterTraitsPropagationRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new ValuesConverter();
+    public static final RelOptRule INSTANCE = new FilterTraitsPropagationRule();
 
-    /**
-     * Creates a ConverterRule.
-     */
-    protected ValuesConverter() {
-        super(LogicalValues.class, "ValuesConverter");
+    public FilterTraitsPropagationRule() {
+        super(operand(IgniteFilter.class, operand(RelSubset.class, any())));
     }
 
     /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalValues values = (LogicalValues) rel;
+    @Override public void onMatch(RelOptRuleCall call) {
+        IgniteFilter rel = call.rel(0);
+        RelNode input = call.rel(1);
 
-        RelTraitSet traits = values.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteMdDistribution.values(values.getRowType(), values.getTuples()));
+        RelOptCluster cluster = rel.getCluster();
+        RelMetadataQuery mq = cluster.getMetadataQuery();
 
-        final IgniteValues newRel = new IgniteValues(values.getCluster(), values.getRowType(), values.getTuples(), traits);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteMdDistribution.filter(mq, input, rel.getCondition()));
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteFilter(cluster, traits, input, rel.getCondition()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IgniteConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IgniteConverter.java
deleted file mode 100644
index 6c97f65..0000000
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/IgniteConverter.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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 com.google.common.collect.ImmutableMap;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelOptPlanner;
-import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.util.typedef.F;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Abstract converter, that converts logical relational expression into one or more physical ones.
- */
-public abstract class IgniteConverter extends ConverterRule {
-    /**
-     * Creates a ConverterRule.
-     *
-     * @param clazz Type of relational expression to consider converting
-     * @param descriptionPrefix Description prefix of rule
-     */
-    protected IgniteConverter(Class<? extends RelNode> clazz, String descriptionPrefix) {
-        super(clazz, Convention.NONE, IgniteConvention.INSTANCE, descriptionPrefix);
-    }
-
-    /** {@inheritDoc} */
-    @Override public void onMatch(@NotNull RelOptRuleCall call) {
-        RelNode rel = call.rel(0);
-
-        List<RelNode> rels = convert0(rel);
-        if (F.isEmpty(rels))
-            return;
-
-        Map<RelNode, RelNode> equiv = ImmutableMap.of();
-
-        if (rels.size() > 1) {
-            equiv = new HashMap<>();
-
-            for (int i = 1; i < rels.size(); i++) {
-                equiv.put(rels.get(i), rel);
-            }
-        }
-
-        call.transformTo(F.first(rels), equiv);
-    }
-
-    /** {@inheritDoc} */
-    @Override public RelNode convert(@NotNull RelNode rel) {
-        List<RelNode> converted = convert0(rel);
-
-        if (converted.size() > 1) {
-            RelOptPlanner planner = rel.getCluster().getPlanner();
-
-            for (int i = 1; i < converted.size(); i++)
-                planner.ensureRegistered(converted.get(i), rel);
-        }
-
-        return F.first(converted);
-    }
-
-    /**
-     * Converts logical relational expression into one or more physical ones.
-     * @param rel Logical relational expression.
-     * @return A list of physical expressions.
-     */
-    protected abstract List<RelNode> convert0(@NotNull RelNode rel);
-}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverter.java
deleted file mode 100644
index 9a79bbc..0000000
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverter.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 java.util.List;
-import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalJoin;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDerivedDistribution;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteJoin;
-import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
-import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
-import org.apache.ignite.internal.processors.query.calcite.util.Commons;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Ignite Join converter.
- */
-public class JoinConverter extends IgniteConverter {
-    public static final ConverterRule INSTANCE = new JoinConverter();
-
-    /**
-     * Creates a converter.
-     */
-    public JoinConverter() {
-        super(LogicalJoin.class, "JoinConverter");
-    }
-
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalJoin join = (LogicalJoin) rel;
-
-        RelNode left = convert(join.getLeft(), IgniteConvention.INSTANCE);
-        RelNode right = convert(join.getRight(), IgniteConvention.INSTANCE);
-
-        RelOptCluster cluster = join.getCluster();
-        RelMetadataQuery mq = cluster.getMetadataQuery();
-
-        List<IgniteDistribution> leftTraits = IgniteMdDerivedDistribution.deriveDistributions(left, IgniteConvention.INSTANCE, mq);
-        List<IgniteDistribution> rightTraits = IgniteMdDerivedDistribution.deriveDistributions(right, IgniteConvention.INSTANCE, mq);
-
-        List<IgniteDistributions.BiSuggestion> suggestions = IgniteDistributions.suggestJoin(leftTraits, rightTraits, join.analyzeCondition(), join.getJoinType());
-
-        return Commons.transform(suggestions, s -> create(join, left, right, s));
-    }
-
-    /** */
-    private static RelNode create(LogicalJoin join, RelNode left, RelNode right, IgniteDistributions.BiSuggestion suggest) {
-        left = convert(left, suggest.left());
-        right = convert(right, suggest.right());
-
-        RelTraitSet traitSet = join.getTraitSet().replace(IgniteConvention.INSTANCE).replace(suggest.out());
-
-        return new IgniteJoin(join.getCluster(), traitSet, left, right, join.getCondition(), join.getVariablesSet(), join.getJoinType());
-    }
-}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverterRule.java
similarity index 53%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverterRule.java
index 0bfd593..af9a508 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinConverterRule.java
@@ -17,41 +17,40 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalValues;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
+import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteJoin;
 
 /**
- *
+ * Ignite Join converter.
  */
-public class ValuesConverter extends IgniteConverter {
+public class JoinConverterRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new ValuesConverter();
+    public static final RelOptRule INSTANCE = new JoinConverterRule();
 
     /**
-     * Creates a ConverterRule.
+     * Creates a converter.
      */
-    protected ValuesConverter() {
-        super(LogicalValues.class, "ValuesConverter");
+    public JoinConverterRule() {
+        super(operand(LogicalJoin.class, any()));
     }
 
     /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalValues values = (LogicalValues) rel;
-
-        RelTraitSet traits = values.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteMdDistribution.values(values.getRowType(), values.getTuples()));
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalJoin rel = call.rel(0);
 
-        final IgniteValues newRel = new IgniteValues(values.getCluster(), values.getRowType(), values.getTuples(), traits);
+        RelOptCluster cluster = rel.getCluster();
+        RelNode left = convert(rel.getLeft(), IgniteConvention.INSTANCE);
+        RelNode right = convert(rel.getRight(), IgniteConvention.INSTANCE);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE);
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteJoin(cluster, traits, left, right, rel.getCondition(), rel.getVariablesSet(), rel.getJoinType()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinTraitsPropagationRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinTraitsPropagationRule.java
new file mode 100644
index 0000000..e6476d5
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/JoinTraitsPropagationRule.java
@@ -0,0 +1,75 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rex.RexNode;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteJoin;
+import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+
+/**
+ *
+ */
+public class JoinTraitsPropagationRule extends RelOptRule {
+    /** */
+    public static final RelOptRule INSTANCE = new JoinTraitsPropagationRule();
+
+    public JoinTraitsPropagationRule() {
+        super(operand(IgniteJoin.class, operand(RelSubset.class, any())));
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onMatch(RelOptRuleCall call) {
+        IgniteJoin rel = call.rel(0);
+
+        RelNode left = rel.getLeft();
+        RelNode right = rel.getRight();
+
+        RelOptCluster cluster = rel.getCluster();
+        RexNode condition = rel.getCondition();
+        Set<CorrelationId> variablesSet = rel.getVariablesSet();
+        JoinRelType joinType = rel.getJoinType();
+
+        List<IgniteDistributions.BiSuggestion> suggests = IgniteDistributions.suggestJoin(
+            left, right, rel.analyzeCondition(), joinType);
+
+        List<RelNode> newRels = new ArrayList<>(suggests.size());
+
+        for (IgniteDistributions.BiSuggestion suggest : suggests) {
+            RelTraitSet traits = rel.getTraitSet().replace(suggest.out());
+
+            RelNode left0 = RuleUtils.changeTraits(left, suggest.left());
+            RelNode right0 = RuleUtils.changeTraits(right, suggest.right());
+
+            newRels.add(new IgniteJoin(cluster, traits, left0, right0,
+                condition, variablesSet, joinType));
+        }
+
+        RuleUtils.transformTo(call, newRels);
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverter.java
deleted file mode 100644
index 3879766..0000000
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverter.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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 java.util.List;
-import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalProject;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDerivedDistribution;
-import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
-import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
-import org.apache.ignite.internal.processors.query.calcite.util.Commons;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Ignite Project converter.
- */
-public class ProjectConverter extends IgniteConverter {
-    /** */
-    public static final ConverterRule INSTANCE = new ProjectConverter();
-
-    /**
-     * Creates a converter.
-     */
-    public ProjectConverter() {
-        super(LogicalProject.class, "ProjectConverter");
-    }
-
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalProject project = (LogicalProject) rel;
-
-        RelNode input = convert(project.getInput(), IgniteConvention.INSTANCE);
-
-        RelOptCluster cluster = rel.getCluster();
-        RelMetadataQuery mq = cluster.getMetadataQuery();
-
-        List<IgniteDistribution> distrs = IgniteMdDerivedDistribution.deriveDistributions(input, IgniteConvention.INSTANCE, mq);
-
-        return Commons.transform(distrs, d -> create(project, input, d));
-    }
-
-    /** */
-    private static IgniteProject create(LogicalProject project, RelNode input, IgniteDistribution distr) {
-        RelTraitSet traits = project.getTraitSet()
-            .replace(IgniteMdDistribution.project(input.getRowType(), distr, project.getProjects()))
-            .replace(IgniteConvention.INSTANCE);
-
-        return new IgniteProject(project.getCluster(), traits, convert(input, distr), project.getProjects(), project.getRowType());
-    }
-}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverterRule.java
similarity index 51%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverterRule.java
index 0088d64..5c3dcee 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectConverterRule.java
@@ -17,46 +17,37 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalTableModify;
+import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
-import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
 
 /**
  *
  */
-public class TableModifyConverter extends IgniteConverter{
+public class ProjectConverterRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new TableModifyConverter();
+    public static final RelOptRule INSTANCE = new ProjectConverterRule();
 
-    /**
-     * Creates a ConverterRule.
-     */
-    public TableModifyConverter() {
-        super(LogicalTableModify.class, "TableModifyConverter");
+    /** */
+    public ProjectConverterRule() {
+        super(operand(LogicalProject.class, any()));
     }
 
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalTableModify modify = (LogicalTableModify) rel;
-
-        RelTraitSet traits = rel.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteDistributions.single());
+    /** */
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalProject rel = call.rel(0);
 
         RelOptCluster cluster = rel.getCluster();
-        RelNode input = convert(modify.getInput(), traits);
-
-        IgniteTableModify newRel = new IgniteTableModify(cluster, traits, modify.getTable(), modify.getCatalogReader(), input,
-            modify.getOperation(), modify.getUpdateColumnList(), modify.getSourceExpressionList(), modify.isFlattened());
+        RelNode input = convert(rel.getInput(), IgniteConvention.INSTANCE);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE);
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteProject(cluster, traits, input, rel.getProjects(), rel.getRowType()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectTraitsPropagationRule.java
similarity index 51%
copy from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
copy to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectTraitsPropagationRule.java
index 0bfd593..2a2b491 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ProjectTraitsPropagationRule.java
@@ -17,41 +17,41 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.volcano.RelSubset;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
-import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
-import org.jetbrains.annotations.NotNull;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
 
 /**
- *
+ * Ignite Project converter.
  */
-public class ValuesConverter extends IgniteConverter {
+public class ProjectTraitsPropagationRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new ValuesConverter();
+    public static final RelOptRule INSTANCE = new ProjectTraitsPropagationRule();
 
     /**
-     * Creates a ConverterRule.
+     * Creates a converter.
      */
-    protected ValuesConverter() {
-        super(LogicalValues.class, "ValuesConverter");
+    public ProjectTraitsPropagationRule() {
+        super(operand(IgniteProject.class, operand(RelSubset.class, any())));
     }
 
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalValues values = (LogicalValues) rel;
+    @Override public void onMatch(RelOptRuleCall call) {
+        IgniteProject rel = call.rel(0);
+        RelNode input = call.rel(1);
 
-        RelTraitSet traits = values.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteMdDistribution.values(values.getRowType(), values.getTuples()));
+        RelOptCluster cluster = rel.getCluster();
+        RelMetadataQuery mq = cluster.getMetadataQuery();
 
-        final IgniteValues newRel = new IgniteValues(values.getCluster(), values.getRowType(), values.getTuples(), traits);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteMdDistribution.project(mq, input, rel.getProjects()));
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteProject(cluster, traits, input, rel.getProjects(), rel.getRowType()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RuleUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RuleUtils.java
new file mode 100644
index 0000000..9bba103
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/RuleUtils.java
@@ -0,0 +1,196 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.hep.HepRelVertex;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.util.typedef.F;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ *
+ */
+public class RuleUtils {
+    /** */
+    public static RelNode convert(RelNode rel, @NotNull RelTrait toTrait) {
+        RelTraitSet toTraits = rel.getTraitSet().replace(toTrait);
+
+        if (rel.getTraitSet().matches(toTraits))
+            return rel;
+
+        RelOptPlanner planner = rel.getCluster().getPlanner();
+
+        return planner.changeTraits(rel, toTraits.simplify());
+    }
+
+    /** */
+    public static RelNode convert(RelNode rel, @NotNull RelTraitSet toTraits) {
+        RelTraitSet outTraits = rel.getTraitSet();
+        for (int i = 0; i < toTraits.size(); i++) {
+            RelTrait toTrait = toTraits.getTrait(i);
+
+            if (toTrait != null)
+                outTraits = outTraits.replace(i, toTrait);
+        }
+
+        if (rel.getTraitSet().matches(outTraits))
+            return rel;
+
+        RelOptPlanner planner = rel.getCluster().getPlanner();
+
+        return planner.changeTraits(rel, outTraits);
+    }
+
+    /** */
+    public static void transformTo(RelOptRuleCall call, RelNode newRel) {
+        transformTo(call, ImmutableList.of(newRel));
+    }
+
+    /** */
+    public static void transformTo(RelOptRuleCall call, List<RelNode> newRels) {
+        transformTo(call, newRels, ImmutableMap.of());
+    }
+
+    /** */
+    public static void transformTo(RelOptRuleCall call, List<RelNode> newRels, Map<RelNode, RelNode> additional) {
+        RelNode orig = call.rel(0);
+
+        if (F.isEmpty(newRels)) {
+            if (!F.isEmpty(additional))
+                // small trick to register the additional equivalence map entries only, we pass
+                // the original rel as transformed one, which will be skipped by the planner.
+                call.transformTo(orig, additional);
+
+            return;
+        }
+
+        if (isRoot(orig))
+            newRels = Commons.transform(newRels, RuleUtils::changeToRootTraits);
+
+        RelNode first = F.first(newRels);
+        List<RelNode> remaining = newRels.subList(1, newRels.size());
+        Map<RelNode, RelNode> equivMap = equivMap(orig, remaining, additional);
+
+        call.transformTo(first, equivMap);
+    }
+
+    /** */
+    public static RelNode changeTraits(RelNode rel, RelTrait... diff) {
+        RelTraitSet traits = rel.getTraitSet();
+
+        for (RelTrait trait : diff)
+            traits = traits.replace(trait);
+
+        return changeTraits(rel, traits);
+    }
+
+    /** */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public static RelNode changeTraits(RelNode rel, RelTraitSet toTraits) {
+        RelTraitSet fromTraits = rel.getTraitSet();
+
+        if (fromTraits.satisfies(toTraits))
+            return rel;
+
+        assert fromTraits.size() >= toTraits.size();
+
+        RelOptPlanner planner = rel.getCluster().getPlanner();
+
+        RelNode converted = rel;
+
+        for (int i = 0; (converted != null) && (i < toTraits.size()); i++) {
+            RelTrait fromTrait = converted.getTraitSet().getTrait(i);
+            RelTrait toTrait = toTraits.getTrait(i);
+
+            RelTraitDef traitDef = fromTrait.getTraitDef();
+
+            if (toTrait == null)
+                continue;
+
+            assert traitDef == toTrait.getTraitDef();
+
+            if (fromTrait.equals(toTrait))
+                continue;
+
+            rel = traitDef.convert(planner, converted, toTrait, true);
+
+            assert rel == null || rel.getTraitSet().getTrait(traitDef).satisfies(toTrait);
+
+            if (rel != null)
+                planner.register(rel, converted);
+
+            converted = rel;
+        }
+
+        assert converted == null || converted.getTraitSet().satisfies(toTraits);
+
+        return converted;
+    }
+
+    /** */
+    private static boolean isRoot(RelNode rel) {
+        RelOptPlanner planner = rel.getCluster().getPlanner();
+        RelNode root = planner.getRoot();
+
+        if (root instanceof RelSubset)
+            return ((VolcanoPlanner) planner).getSubset(rel, root.getTraitSet()) == root;
+
+        if (root instanceof HepRelVertex)
+            return root == rel || ((HepRelVertex) root).getCurrentRel() == rel;
+
+        return root == rel;
+    }
+
+    /** */
+    private static RelNode changeToRootTraits(RelNode rel) {
+        RelTraitSet rootTraits = rel.getCluster().getPlanner().getRoot().getTraitSet();
+
+        return changeTraits(rel, rootTraits);
+    }
+
+    /** */
+    private static @NotNull Map<RelNode, RelNode> equivMap(RelNode orig, List<RelNode> equivList, Map<RelNode, RelNode> additional) {
+        assert orig != null;
+        assert equivList != null;
+        assert additional != null;
+
+        if(F.isEmpty(equivList))
+            return additional;
+
+        ImmutableMap.Builder<RelNode, RelNode> b = ImmutableMap.builder();
+
+        for (RelNode equiv : equivList)
+            b.put(equiv, orig);
+
+        b.putAll(additional);
+
+        return b.build();
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverterRule.java
similarity index 61%
rename from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java
rename to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverterRule.java
index 0088d64..dded683 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/TableModifyConverterRule.java
@@ -17,46 +17,45 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalTableModify;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
 import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
-import org.jetbrains.annotations.NotNull;
 
 /**
  *
  */
-public class TableModifyConverter extends IgniteConverter{
+public class TableModifyConverterRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new TableModifyConverter();
+    public static final RelOptRule INSTANCE = new TableModifyConverterRule();
 
     /**
      * Creates a ConverterRule.
      */
-    public TableModifyConverter() {
-        super(LogicalTableModify.class, "TableModifyConverter");
+    public TableModifyConverterRule() {
+        super(operand(LogicalTableModify.class, any()));
     }
 
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalTableModify modify = (LogicalTableModify) rel;
-
-        RelTraitSet traits = rel.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteDistributions.single());
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalTableModify rel = call.rel(0);
 
         RelOptCluster cluster = rel.getCluster();
-        RelNode input = convert(modify.getInput(), traits);
 
-        IgniteTableModify newRel = new IgniteTableModify(cluster, traits, modify.getTable(), modify.getCatalogReader(), input,
-            modify.getOperation(), modify.getUpdateColumnList(), modify.getSourceExpressionList(), modify.isFlattened());
+        RelNode input = convert(rel.getInput(), IgniteConvention.INSTANCE);
+
+        input = RuleUtils.changeTraits(input, IgniteDistributions.single());
+
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE)
+            .replace(IgniteDistributions.single()); // TODO move to IgniteMdDistributions
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteTableModify(cluster, traits, rel.getTable(), rel.getCatalogReader(), input,
+            rel.getOperation(), rel.getUpdateColumnList(), rel.getSourceExpressionList(), rel.isFlattened()));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverterRule.java
similarity index 60%
rename from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
rename to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverterRule.java
index 0bfd593..5d59a58 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/ValuesConverterRule.java
@@ -17,41 +17,39 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.Collections;
-import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDistribution;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues;
-import org.jetbrains.annotations.NotNull;
 
 /**
  *
  */
-public class ValuesConverter extends IgniteConverter {
+public class ValuesConverterRule extends RelOptRule {
     /** */
-    public static final ConverterRule INSTANCE = new ValuesConverter();
+    public static final RelOptRule INSTANCE = new ValuesConverterRule();
 
     /**
      * Creates a ConverterRule.
      */
-    protected ValuesConverter() {
-        super(LogicalValues.class, "ValuesConverter");
+    protected ValuesConverterRule() {
+        super(operand(LogicalValues.class, none()));
     }
 
-    /** {@inheritDoc} */
-    @Override protected List<RelNode> convert0(@NotNull RelNode rel) {
-        LogicalValues values = (LogicalValues) rel;
+    @Override public void onMatch(RelOptRuleCall call) {
+        LogicalValues rel = call.rel(0);
 
-        RelTraitSet traits = values.getTraitSet()
-            .replace(IgniteConvention.INSTANCE)
-            .replace(IgniteMdDistribution.values(values.getRowType(), values.getTuples()));
+        RelOptCluster cluster = rel.getCluster();
 
-        final IgniteValues newRel = new IgniteValues(values.getCluster(), values.getRowType(), values.getTuples(), traits);
+        RelTraitSet traits = rel.getTraitSet()
+            .replace(IgniteConvention.INSTANCE)
+            .replace(IgniteMdDistribution.values(rel.getRowType(), rel.getTuples()));
 
-        return Collections.singletonList(newRel);
+        RuleUtils.transformTo(call,
+            new IgniteValues(cluster, rel.getRowType(), rel.getTuples(), traits));
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/serialize/RelToPhysicalConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/serialize/RelToPhysicalConverter.java
index 04a32af..a5effbf 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/serialize/RelToPhysicalConverter.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/serialize/RelToPhysicalConverter.java
@@ -66,8 +66,8 @@ public class RelToPhysicalConverter implements IgniteRelVisitor<PhysicalRel> {
     @Override public PhysicalRel visit(IgniteSender rel) {
         long fragmentId = rel.target().fragmentId();
         NodesMapping mapping = rel.target().mapping();
-        DistributionFunction fun = rel.targetDistribution().function();
-        ImmutableIntList keys = rel.targetDistribution().getKeys();
+        DistributionFunction fun = rel.distribution().function();
+        ImmutableIntList keys = rel.distribution().getKeys();
 
         return new SenderPhysicalRel(fragmentId, mapping, fun, keys, visit((IgniteRel) rel.getInput()));
     }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/DistributionTraitDef.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/DistributionTraitDef.java
index b97794b..f94c0d9 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/DistributionTraitDef.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/DistributionTraitDef.java
@@ -20,9 +20,10 @@ package org.apache.ignite.internal.processors.query.calcite.trait;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitDef;
-import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
+import org.apache.ignite.internal.processors.query.calcite.rule.RuleUtils;
 
 /**
  *
@@ -42,26 +43,23 @@ public class DistributionTraitDef extends RelTraitDef<IgniteDistribution> {
     }
 
     /** {@inheritDoc} */
-    @Override public RelNode convert(RelOptPlanner planner, RelNode rel, IgniteDistribution targetDist, boolean allowInfiniteCostConverters) {
+    @Override public RelNode convert(RelOptPlanner planner, RelNode rel, IgniteDistribution toDist, boolean allowInfiniteCostConverters) {
         if (rel.getConvention() == Convention.NONE)
             return null;
 
-        RelDistribution srcDist = rel.getTraitSet().getTrait(INSTANCE);
+        IgniteDistribution fromDist = rel.getTraitSet().getTrait(INSTANCE);
 
-        if (srcDist == targetDist) // has to be interned
+        if (fromDist.satisfies(toDist))
             return rel;
 
-        switch(targetDist.getType()){
-            case HASH_DISTRIBUTED:
-            case BROADCAST_DISTRIBUTED:
-            case SINGLETON:
-                return register(planner, rel,
-                    new IgniteExchange(rel.getCluster(), rel.getTraitSet().replace(targetDist), rel, targetDist));
-            case ANY:
-                return rel;
-            default:
-                return null;
-        }
+        RelTraitSet newTraits = rel.getTraitSet().replace(toDist);
+        RelNode input = RuleUtils.convert(rel, IgniteDistributions.any()); // erasing source distribution a bit reduces search space
+        RelNode newRel = planner.register(new IgniteExchange(rel.getCluster(), newTraits, input, toDist), rel);
+
+        if (!newRel.getTraitSet().equals(newTraits))
+            newRel = planner.changeTraits(newRel, newTraits);
+
+        return newRel;
     }
 
     /** {@inheritDoc} */
@@ -73,14 +71,4 @@ public class DistributionTraitDef extends RelTraitDef<IgniteDistribution> {
     @Override public IgniteDistribution getDefault() {
         return IgniteDistributions.any();
     }
-
-    /** */
-    private RelNode register(RelOptPlanner planner, RelNode rel, RelNode replace) {
-        RelNode registered = planner.register(replace, rel);
-
-        if (!registered.getTraitSet().equals(replace.getTraitSet()))
-            registered = planner.changeTraits(registered, replace.getTraitSet());
-
-        return registered;
-    }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/IgniteDistributions.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/IgniteDistributions.java
index 4fe2777..2385a19 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/IgniteDistributions.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/trait/IgniteDistributions.java
@@ -18,21 +18,28 @@
 package org.apache.ignite.internal.processors.query.calcite.trait;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.JoinInfo;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.mapping.Mappings;
+import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMdDerivedDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.AffinityDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.AnyDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.BroadcastDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.HashDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.RandomDistribution;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionFunction.SingletonDistribution;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -47,63 +54,59 @@ import static org.apache.calcite.rel.core.JoinRelType.RIGHT;
  */
 public class IgniteDistributions {
     /** */
-    private static final int BEST_CNT = 3;
+    private static final int BEST_CNT = IgniteSystemProperties.getInteger("IGNITE_CALCITE_JOIN_SUGGESTS_COUNT", 0);
 
     /** */
-    private static final IgniteDistribution BROADCAST = new DistributionTrait(BroadcastDistribution.INSTANCE);
+    private static final Integer[] INTS = new Integer[]{0, 1, 2};
 
     /** */
-    private static final IgniteDistribution SINGLETON = new DistributionTrait(SingletonDistribution.INSTANCE);
+    private static final IgniteDistribution BROADCAST = canonize(new DistributionTrait(BroadcastDistribution.INSTANCE));
 
     /** */
-    private static final IgniteDistribution RANDOM = new DistributionTrait(RandomDistribution.INSTANCE);
+    private static final IgniteDistribution SINGLETON = canonize(new DistributionTrait(SingletonDistribution.INSTANCE));
 
     /** */
-    private static final IgniteDistribution ANY = new DistributionTrait(AnyDistribution.INSTANCE);
+    private static final IgniteDistribution RANDOM = canonize(new DistributionTrait(RandomDistribution.INSTANCE));
+
+    /** */
+    private static final IgniteDistribution ANY = canonize(new DistributionTrait(AnyDistribution.INSTANCE));
 
     /**
      * @return Any distribution.
      */
     public static IgniteDistribution any() {
-        return canonize(ANY);
+        return ANY;
     }
 
     /**
      * @return Random distribution.
      */
     public static IgniteDistribution random() {
-        return canonize(RANDOM);
+        return RANDOM;
     }
 
     /**
      * @return Single distribution.
      */
     public static IgniteDistribution single() {
-        return canonize(SINGLETON);
+        return SINGLETON;
     }
 
     /**
      * @return Broadcast distribution.
      */
     public static IgniteDistribution broadcast() {
-        return canonize(BROADCAST);
-    }
-
-    /**
-     * @param keys Distribution keys.
-     * @return Hash distribution.
-     */
-    public static IgniteDistribution hash(List<Integer> keys) {
-        return canonize(new DistributionTrait(ImmutableIntList.copyOf(keys), HashDistribution.INSTANCE));
+        return BROADCAST;
     }
 
     /**
-     * @param keys Distribution keys.
-     * @param function Specific hash function.
-     * @return Hash distribution.
+     * @param key Affinity key.
+     * @param cacheName Affinity cache name.
+     * @param identity Affinity identity key.
+     * @return Affinity distribution.
      */
-    public static IgniteDistribution hash(List<Integer> keys, DistributionFunction function) {
-        return canonize(new DistributionTrait(ImmutableIntList.copyOf(keys), function));
+    public static IgniteDistribution affinity(int key, String cacheName, Object identity) {
+        return affinity(key, CU.cacheId(cacheName), identity);
     }
 
     /**
@@ -113,65 +116,62 @@ public class IgniteDistributions {
      * @return Affinity distribution.
      */
     public static IgniteDistribution affinity(int key, int cacheId, Object identity) {
-        return canonize(new DistributionTrait(ImmutableIntList.of(key), new AffinityDistribution(cacheId, identity)));
+        return hash(ImmutableIntList.of(key), new AffinityDistribution(cacheId, identity));
     }
 
     /**
-     * @param key Affinity key.
-     * @param cacheName Affinity cache name.
-     * @param identity Affinity identity key.
-     * @return Affinity distribution.
+     * @param keys Distribution keys.
+     * @return Hash distribution.
      */
-    public static IgniteDistribution affinity(int key, String cacheName, Object identity) {
-        return affinity(key, CU.cacheId(cacheName), identity);
+    public static IgniteDistribution hash(List<Integer> keys) {
+        return canonize(new DistributionTrait(ImmutableIntList.copyOf(keys), HashDistribution.INSTANCE));
     }
 
     /**
-     * See {@link RelTraitDef#canonize(org.apache.calcite.plan.RelTrait)}.
+     * @param keys Distribution keys.
+     * @param function Specific hash function.
+     * @return Hash distribution.
      */
-    public static IgniteDistribution canonize(IgniteDistribution distr) {
-        return DistributionTraitDef.INSTANCE.canonize(distr);
+    public static IgniteDistribution hash(List<Integer> keys, DistributionFunction function) {
+        return canonize(new DistributionTrait(ImmutableIntList.copyOf(keys), function));
     }
 
     /**
      * Suggests possible join distributions.
      *
-     * @param leftIn Left distribution.
-     * @param rightIn Right distribution.
+     * @param left Left node.
+     * @param right Right node.
      * @param joinInfo Join info.
      * @param joinType Join type.
      * @return Array of possible distributions, sorted by their efficiency (cheaper first).
      */
-    public static List<BiSuggestion> suggestJoin(IgniteDistribution leftIn, IgniteDistribution rightIn,
-        JoinInfo joinInfo, JoinRelType joinType) {
-        return topN(suggestJoin0(leftIn, rightIn, joinInfo, joinType), BEST_CNT);
+    public static List<BiSuggestion> suggestJoin(RelNode left, RelNode right, JoinInfo joinInfo, JoinRelType joinType) {
+        RelMetadataQuery mq = left.getCluster().getMetadataQuery();
+
+        List<IgniteDistribution> leftIn = IgniteMdDerivedDistribution._deriveDistributions(left, mq);
+        List<IgniteDistribution> rightIn = IgniteMdDerivedDistribution._deriveDistributions(right, mq);
+
+        Map<BiSuggestion, Integer> suggestions = new LinkedHashMap<>();
+
+        for (IgniteDistribution leftIn0 : leftIn)
+            for (IgniteDistribution rightIn0 : rightIn)
+                suggestions = suggestJoin0(suggestions, leftIn0, rightIn0, joinInfo, joinType);
+
+        return sorted(suggestions);
     }
 
     /**
      * Suggests possible join distributions.
      *
-     * @param leftIn Left distributions.
-     * @param rightIn Right distributions.
+     * @param leftIn Left distribution.
+     * @param rightIn Right distribution.
      * @param joinInfo Join info.
      * @param joinType Join type.
      * @return Array of possible distributions, sorted by their efficiency (cheaper first).
      */
-    public static List<BiSuggestion> suggestJoin(List<IgniteDistribution> leftIn, List<IgniteDistribution> rightIn,
+    public static List<BiSuggestion> suggestJoin(IgniteDistribution leftIn, IgniteDistribution rightIn,
         JoinInfo joinInfo, JoinRelType joinType) {
-        HashSet<BiSuggestion> suggestions = new HashSet<>();
-
-        int bestCnt = 0;
-
-        for (IgniteDistribution leftIn0 : leftIn) {
-            for (IgniteDistribution rightIn0 : rightIn) {
-                for (BiSuggestion suggest : suggestJoin0(leftIn0, rightIn0, joinInfo, joinType)) {
-                    if (suggestions.add(suggest) && suggest.needExchange == 0 && (++bestCnt) == BEST_CNT)
-                        return topN(new ArrayList<>(suggestions), BEST_CNT);
-                }
-            }
-        }
-
-        return topN(new ArrayList<>(suggestions), BEST_CNT);
+        return sorted(suggestJoin0(new LinkedHashMap<>(), leftIn, rightIn, joinInfo, joinType));
     }
 
     /**
@@ -207,22 +207,20 @@ public class IgniteDistributions {
      * @param rightIn Right distribution.
      * @param joinInfo Join info.
      * @param joinType Join type.
-     * @return Array of possible distributions, sorted by their efficiency (cheaper first).
      */
-    private static ArrayList<BiSuggestion> suggestJoin0(IgniteDistribution leftIn, IgniteDistribution rightIn,
+    private static Map<BiSuggestion, Integer> suggestJoin0(Map<BiSuggestion, Integer> dst, IgniteDistribution leftIn, IgniteDistribution rightIn,
         JoinInfo joinInfo, JoinRelType joinType) {
-
-        ArrayList<BiSuggestion> res = new ArrayList<>();
-
         IgniteDistribution out, left, right;
 
         if (joinType == LEFT || joinType == RIGHT || (joinType == INNER && !F.isEmpty(joinInfo.pairs()))) {
             HashSet<DistributionFunction> factories = U.newHashSet(3);
 
-            if (Objects.equals(joinInfo.leftKeys, leftIn.getKeys()))
+            if (leftIn.getType() == RelDistribution.Type.HASH_DISTRIBUTED
+                && Objects.equals(joinInfo.leftKeys, leftIn.getKeys()))
                 factories.add(leftIn.function());
 
-            if (Objects.equals(joinInfo.rightKeys, rightIn.getKeys()))
+            if (rightIn.getType() == RelDistribution.Type.HASH_DISTRIBUTED
+                && Objects.equals(joinInfo.rightKeys, rightIn.getKeys()))
                 factories.add(rightIn.function());
 
             factories.add(HashDistribution.INSTANCE);
@@ -231,56 +229,76 @@ public class IgniteDistributions {
                 out = hash(joinInfo.leftKeys, factory);
 
                 left = hash(joinInfo.leftKeys, factory); right = hash(joinInfo.rightKeys, factory);
-                add(res, out, leftIn, rightIn, left, right);
+                dst = add(dst, out, leftIn, rightIn, left, right);
 
                 if (joinType == INNER || joinType == LEFT) {
                     left = hash(joinInfo.leftKeys, factory); right = broadcast();
-                    add(res, out, leftIn, rightIn, left, right);
+                    dst = add(dst, out, leftIn, rightIn, left, right);
                 }
 
                 if (joinType == INNER || joinType == RIGHT) {
                     left = broadcast(); right = hash(joinInfo.rightKeys, factory);
-                    add(res, out, leftIn, rightIn, left, right);
+                    dst = add(dst, out, leftIn, rightIn, left, right);
                 }
             }
         }
 
         out = left = right = broadcast();
-        add(res, out, leftIn, rightIn, left, right);
+        dst = add(dst, out, leftIn, rightIn, left, right);
 
         out = left = right = single();
-        add(res, out, leftIn, rightIn, left, right);
+        dst = add(dst, out, leftIn, rightIn, left, right);
 
-        return res;
+        return dst;
     }
 
     /** */
-    private static int add(ArrayList<BiSuggestion> dst, IgniteDistribution out, IgniteDistribution left, IgniteDistribution right,
+    private static Map<BiSuggestion, Integer> add(Map<BiSuggestion, Integer> dst, IgniteDistribution out, IgniteDistribution left, IgniteDistribution right,
         IgniteDistribution newLeft, IgniteDistribution newRight) {
-        int exch = 0;
+        if (BEST_CNT > 0) {
+            int exch = 0;
 
-        if (needsExchange(left, newLeft))
-            exch++;
+            if (!left.satisfies(newLeft))
+                exch++;
 
-        if (needsExchange(right, newRight))
-            exch++;
+            if (!right.satisfies(newRight))
+                exch++;
 
-        dst.add(new BiSuggestion(out, newLeft, newRight, exch));
-
-        return exch;
+            return add(dst, new BiSuggestion(out, newLeft, newRight), INTS[exch]);
+        }
+        else
+            return add(dst, new BiSuggestion(out, newLeft, newRight), INTS[0]);
     }
 
     /** */
-    private static boolean needsExchange(IgniteDistribution sourceDist, IgniteDistribution targetDist) {
-        return !sourceDist.satisfies(targetDist);
+    private static Map<BiSuggestion, Integer> add(Map<BiSuggestion, Integer> dst, BiSuggestion suggest, Integer exchCnt) {
+        if (dst == null)
+            dst = new LinkedHashMap<>();
+
+        dst.merge(suggest, exchCnt, IgniteDistributions::min);
+
+        return dst;
     }
 
     /** */
-    @SuppressWarnings("SameParameterValue")
-    private static List<BiSuggestion> topN(ArrayList<BiSuggestion> src, int n) {
-        Collections.sort(src);
+    private static List<BiSuggestion> sorted(Map<BiSuggestion, Integer> src) {
+        if (BEST_CNT > 0) {
+            List<Map.Entry<BiSuggestion, Integer>> entries = new ArrayList<>(src.entrySet());
+
+            entries.sort(Map.Entry.comparingByValue());
+
+            if (entries.size() >= BEST_CNT)
+                entries = entries.subList(0, BEST_CNT);
+
+            return Commons.transform(entries, Map.Entry::getKey);
+        }
 
-        return src.size() <= n ? src : src.subList(0, n);
+        return new ArrayList<>(src.keySet());
+    }
+
+    /** */
+    private static @NotNull Integer min(@NotNull Integer i1, @NotNull Integer i2) {
+        return i1 < i2 ? i1 : i2;
     }
 
     /**
@@ -319,9 +337,16 @@ public class IgniteDistributions {
     }
 
     /**
+     * See {@link RelTraitDef#canonize(org.apache.calcite.plan.RelTrait)}.
+     */
+    private static IgniteDistribution canonize(IgniteDistribution distr) {
+        return DistributionTraitDef.INSTANCE.canonize(distr);
+    }
+
+    /**
      * Distribution suggestion for BiRel.
      */
-    public static class BiSuggestion implements Comparable<BiSuggestion> {
+    public static class BiSuggestion {
         /** */
         private final IgniteDistribution out;
 
@@ -331,20 +356,15 @@ public class IgniteDistributions {
         /** */
         private final IgniteDistribution right;
 
-        /** */
-        private final int needExchange;
-
         /**
          * @param out Result distribution.
          * @param left Required left distribution.
          * @param right Required right distribution.
-         * @param needExchange Exchanges count (for ordering).
          */
-        public BiSuggestion(IgniteDistribution out, IgniteDistribution left, IgniteDistribution right, int needExchange) {
+        public BiSuggestion(IgniteDistribution out, IgniteDistribution left, IgniteDistribution right) {
             this.out = out;
             this.left = left;
             this.right = right;
-            this.needExchange = needExchange;
         }
 
         /**
@@ -369,19 +389,13 @@ public class IgniteDistributions {
         }
 
         /** {@inheritDoc} */
-        @Override public int compareTo(@NotNull IgniteDistributions.BiSuggestion o) {
-            return Integer.compare(needExchange, o.needExchange);
-        }
-
-        /** {@inheritDoc} */
         @Override public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
 
             BiSuggestion that = (BiSuggestion) o;
 
-            return needExchange == that.needExchange
-                && out == that.out
+            return out == that.out
                 && left == that.left
                 && right == that.right;
         }
@@ -391,7 +405,6 @@ public class IgniteDistributions {
             int result = out.hashCode();
             result = 31 * result + left.hashCode();
             result = 31 * result + right.hashCode();
-            result = 31 * result + needExchange;
             return result;
         }
     }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/PlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/PlannerTest.java
index 62ce15a..25bbe86 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/PlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/PlannerTest.java
@@ -90,6 +90,7 @@ import org.jetbrains.annotations.Nullable;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import static org.apache.calcite.tools.Frameworks.createRootSchema;
@@ -589,6 +590,7 @@ public class PlannerTest extends GridCommonAbstractTest {
      * @throws Exception If failed.
      */
     @Test
+    @Ignore("https://issues.apache.org/jira/browse/IGNITE-12819")
     public void testSplitterCollocatedPartitionedPartitioned() throws Exception {
         IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);