You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by hy...@apache.org on 2020/06/18 03:55:12 UTC

[calcite] branch master updated: [CALCITE-4056] Remove Digest from RelNode and RexCall

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b00c1fd  [CALCITE-4056] Remove Digest from RelNode and RexCall
b00c1fd is described below

commit b00c1fd6e26b5e3cc1a810c9f862b184649cd1a6
Author: Haisheng Yuan <h....@alibaba-inc.com>
AuthorDate: Wed Jun 17 06:07:00 2020 -0500

    [CALCITE-4056] Remove Digest from RelNode and RexCall
    
    Close #2032
---
 .../adapter/enumerable/EnumerableAggregate.java    |   8 +
 .../adapter/enumerable/EnumerableFilter.java       |   8 +
 .../adapter/enumerable/EnumerableHashJoin.java     |   8 +
 .../adapter/enumerable/EnumerableMergeJoin.java    |   8 +
 .../enumerable/EnumerableNestedLoopJoin.java       |   8 +
 .../adapter/enumerable/EnumerableProject.java      |   8 +
 .../enumerable/EnumerableSortedAggregate.java      |   8 +
 .../main/java/org/apache/calcite/plan/Digest.java  | 256 ---------------------
 .../java/org/apache/calcite/plan/RelDigest.java    |  52 +++++
 .../java/org/apache/calcite/plan/RelOptNode.java   |  15 +-
 .../java/org/apache/calcite/plan/RelOptUtil.java   |   1 +
 .../org/apache/calcite/plan/hep/HepPlanner.java    |  12 +-
 .../org/apache/calcite/plan/hep/HepRelVertex.java  |  15 +-
 .../org/apache/calcite/plan/volcano/RelSubset.java |  36 ++-
 .../calcite/plan/volcano/VolcanoPlanner.java       |  55 +++--
 .../org/apache/calcite/rel/AbstractRelNode.java    | 135 +++++++++--
 .../main/java/org/apache/calcite/rel/RelNode.java  |   7 +-
 .../java/org/apache/calcite/rel/SingleRel.java     |   1 +
 .../org/apache/calcite/rel/core/Aggregate.java     |  21 ++
 .../java/org/apache/calcite/rel/core/Filter.java   |  19 ++
 .../java/org/apache/calcite/rel/core/Join.java     |  22 ++
 .../java/org/apache/calcite/rel/core/Project.java  |  21 ++
 .../java/org/apache/calcite/rel/core/Values.java   |  18 ++
 .../java/org/apache/calcite/rel/core/Window.java   |   5 +-
 .../calcite/rel/logical/LogicalAggregate.java      |   8 +
 .../apache/calcite/rel/logical/LogicalFilter.java  |  12 +
 .../apache/calcite/rel/logical/LogicalJoin.java    |  13 ++
 .../apache/calcite/rel/logical/LogicalProject.java |   8 +
 .../rel/metadata/JaninoRelMetadataProvider.java    |   7 +-
 .../org/apache/calcite/rel/rules/MultiJoin.java    |   1 +
 .../main/java/org/apache/calcite/rex/RexCall.java  |  18 +-
 .../org/apache/calcite/rex/RexDynamicParam.java    |   6 +-
 .../java/org/apache/calcite/rex/RexLiteral.java    |   8 +-
 .../main/java/org/apache/calcite/rex/RexNode.java  |   5 +
 .../main/java/org/apache/calcite/rex/RexOver.java  |   6 +-
 .../java/org/apache/calcite/rex/RexSubQuery.java   |   8 +-
 .../org/apache/calcite/test/HepPlannerTest.java    |   2 +-
 37 files changed, 495 insertions(+), 354 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
index a0dee72..9967e2a 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
@@ -105,6 +105,14 @@ public class EnumerableAggregate extends Aggregate implements EnumerableRel {
     }
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
     final JavaTypeFactory typeFactory = implementor.getTypeFactory();
     final BlockBuilder builder = new BlockBuilder();
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilter.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilter.java
index 5af195c..8d26c7e 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilter.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilter.java
@@ -71,6 +71,14 @@ public class EnumerableFilter
     return new EnumerableFilter(getCluster(), traitSet, input, condition);
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
     // EnumerableCalc is always better
     throw new UnsupportedOperationException();
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
index 918919e..41d5d50 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
@@ -166,6 +166,14 @@ public class EnumerableHashJoin extends Join implements EnumerableRel {
     }
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   @Override public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
     switch (joinType) {
     case SEMI:
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
index f9f4d1a..d9e04c4 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
@@ -130,6 +130,14 @@ public class EnumerableMergeJoin extends Join implements EnumerableRel {
         CorrelationId.setOf(variablesStopped), joinType);
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   @Override public Pair<RelTraitSet, List<RelTraitSet>> passThroughTraits(
       final RelTraitSet required) {
     // Required collation keys can be subset or superset of merge join keys.
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableNestedLoopJoin.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableNestedLoopJoin.java
index 1561d8f..0bd8aef 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableNestedLoopJoin.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableNestedLoopJoin.java
@@ -121,6 +121,14 @@ public class EnumerableNestedLoopJoin extends Join implements EnumerableRel {
     return cost;
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   @Override public Pair<RelTraitSet, List<RelTraitSet>> passThroughTraits(
       final RelTraitSet required) {
     // EnumerableNestedLoopJoin traits passdown shall only pass through collation to
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
index 7c62e2f..5bc8a8b 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProject.java
@@ -84,6 +84,14 @@ public class EnumerableProject extends Project implements EnumerableRel {
         projects, rowType);
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
     // EnumerableCalcRel is always better
     throw new UnsupportedOperationException();
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
index 00ca78a..f341921 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregate.java
@@ -56,6 +56,14 @@ public class EnumerableSortedAggregate extends Aggregate implements EnumerableRe
         groupSet, groupSets, aggCalls);
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
+
   @Override public Pair<RelTraitSet, List<RelTraitSet>> passThroughTraits(
       final RelTraitSet required) {
     if (!isSimple(this)) {
diff --git a/core/src/main/java/org/apache/calcite/plan/Digest.java b/core/src/main/java/org/apache/calcite/plan/Digest.java
deleted file mode 100644
index 494cb14..0000000
--- a/core/src/main/java/org/apache/calcite/plan/Digest.java
+++ /dev/null
@@ -1,256 +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;
-
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.hint.Hintable;
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.util.Pair;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import javax.annotation.Nonnull;
-
-/**
- * A short description of relational expression's type, inputs, and
- * other properties. The digest uniquely identifies the node; another node
- * is equivalent if and only if it has the same value.
- *
- * <p>Row type is part of the digest for the rare occasion that similar
- * expressions have different types, e.g. variants of
- * {@code Project(child=rel#1, a=null)} where a is a null INTEGER or a
- * null VARCHAR(10). Row type is represented as fieldTypes only, so {@code RelNode}
- * that differ with field names only are treated equal.
- * For instance, {@code Project(input=rel#1,empid=$0)} and {@code Project(input=rel#1,deptno=$0)}
- * are equal.
- *
- * <p>Computed by {@code org.apache.calcite.rel.AbstractRelNode#computeDigest},
- * assigned by {@link org.apache.calcite.rel.AbstractRelNode#onRegister},
- * returned by {@link org.apache.calcite.rel.AbstractRelNode#getDigest()}.
- */
-public class Digest implements Comparable<Digest> {
-
-  //~ Instance fields --------------------------------------------------------
-
-  private final int hashCode;
-  private final List<Pair<String, Object>> items;
-  private final RelNode rel;
-
-  // Used for debugging, computed lazily.
-  private String digest = null;
-
-  //~ Constructors -----------------------------------------------------------
-
-  /**
-   * Creates a digest with given rel and properties.
-   *
-   * @param rel   The rel
-   * @param items The properties, e.g. the inputs, the type, the traits and so on
-   */
-  private Digest(RelNode rel, List<Pair<String, Object>> items) {
-    this.rel = rel;
-    this.items = normalizeContents(items);
-    this.hashCode = computeIdentity(rel, this.items);
-  }
-
-  /**
-   * Creates a digest with given rel, the digest is computed as simple,
-   * see {@link #simpleRelDigest(RelNode)}.
-   */
-  private Digest(RelNode rel) {
-    this(rel, simpleRelDigest(rel));
-  }
-
-  /** Creates a digest with given rel and string format digest. */
-  private Digest(RelNode rel, String digest) {
-    this.rel = rel;
-    this.items = Collections.emptyList();
-    this.digest = digest;
-    this.hashCode = this.digest.hashCode();
-  }
-
-  /** Returns the identity of this digest which is used to speedup hashCode and equals. */
-  private static int computeIdentity(RelNode rel, List<Pair<String, Object>> contents) {
-    return Objects.hash(collect(rel, contents, false));
-  }
-
-  /**
-   * Collects the items used for {@link #hashCode} and {@link #equals}.
-   *
-   * <p>Generally, the items used for hashCode and equals should be the same. The exception
-   * is the row type of the relational expression: the row type is needed because during
-   * planning, new equivalent rels may be produced with changed fields nullability
-   * (i.e. most of them comes from the rex simplify or constant reduction).
-   * This expects to be rare case, so the hashcode is computed without row type
-   * but when it conflicts, we compare with the row type involved(sans field names).
-   *
-   * @param rel      The rel to compute digest
-   * @param contents The rel properties should be considered in digest
-   * @param withType Whether to involve the row type
-   */
-  private static Object[] collect(
-      RelNode rel,
-      List<Pair<String, Object>> contents,
-      boolean withType) {
-    List<Object> hashCodeItems = new ArrayList<>();
-    // The type name.
-    hashCodeItems.add(rel.getRelTypeName());
-    // The traits.
-    hashCodeItems.addAll(rel.getTraitSet());
-    // The hints.
-    if (rel instanceof Hintable) {
-      hashCodeItems.addAll(((Hintable) rel).getHints());
-    }
-    if (withType) {
-      // The row type sans field names.
-      RelDataType relType = rel.getRowType();
-      if (relType.isStruct()) {
-        hashCodeItems.addAll(Pair.right(relType.getFieldList()));
-      } else {
-        // Make a decision here because
-        // some downstream projects have custom rel type which has no explicit fields.
-        hashCodeItems.add(relType);
-      }
-    }
-    // The rel node contents(e.g. the inputs or exprs).
-    hashCodeItems.addAll(contents);
-    return hashCodeItems.toArray();
-  }
-
-  /** Normalizes the rel node properties, currently, just to replace the
-   * {@link RelNode} with a simple string format digest. **/
-  private static List<Pair<String, Object>> normalizeContents(
-      List<Pair<String, Object>> items) {
-    List<Pair<String, Object>> normalized = new ArrayList<>();
-    for (Pair<String, Object> item : items) {
-      if (item.right instanceof RelNode) {
-        RelNode input = (RelNode) item.right;
-        normalized.add(Pair.of(item.left, simpleRelDigest(input)));
-      } else {
-        normalized.add(item);
-      }
-    }
-    return normalized;
-  }
-
-  /**
-   * Returns a simple string format digest.
-   *
-   * <p>Currently, returns composition of class name and id.
-   *
-   * @param rel The rel
-   */
-  private static String simpleRelDigest(RelNode rel) {
-    return rel.getRelTypeName() + '#' + rel.getId();
-  }
-
-  @Override public String toString() {
-    if (null != digest) {
-      return digest;
-    }
-    StringBuilder sb = new StringBuilder();
-    sb.append(rel.getRelTypeName());
-
-    for (RelTrait trait : rel.getTraitSet()) {
-      sb.append('.');
-      sb.append(trait.toString());
-    }
-
-    sb.append('(');
-    int j = 0;
-    for (Pair<String, Object> item : items) {
-      if (j++ > 0) {
-        sb.append(',');
-      }
-      sb.append(item.left);
-      sb.append('=');
-      sb.append(item.right);
-    }
-    sb.append(')');
-    digest = sb.toString();
-    return digest;
-  }
-
-  @Override public int compareTo(@Nonnull Digest other) {
-    return this.equals(other) ? 0 : this.rel.getId() - other.rel.getId();
-  }
-
-  @Override public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    Digest that = (Digest) o;
-    return hashCode == that.hashCode && deepEquals(that);
-  }
-
-  /**
-   * The method is used to resolve hash conflict, in current 6000+ tests, there are about 8
-   * tests with conflict, so we do not cache the hash code items in order to
-   * reduce mem consumption.
-   */
-  private boolean deepEquals(Digest other) {
-    Object[] thisItems = collect(this.rel, this.items, true);
-    Object[] thatItems = collect(other.rel, other.items, true);
-    if (thisItems.length != thatItems.length) {
-      return false;
-    }
-    for (int i = 0; i < thisItems.length; i++) {
-      if (!Objects.equals(thisItems[i], thatItems[i])) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @Override public int hashCode() {
-    return hashCode;
-  }
-
-  /**
-   * Creates a digest with given rel and properties.
-   */
-  public static Digest create(RelNode rel, List<Pair<String, Object>> contents) {
-    return new Digest(rel, contents);
-  }
-
-  /**
-   * Creates a digest with given rel.
-   */
-  public static Digest create(RelNode rel) {
-    return new Digest(rel);
-  }
-
-  /**
-   * Creates a digest with given rel and string format digest
-   */
-  public static Digest create(RelNode rel, String digest) {
-    return new Digest(rel, digest);
-  }
-
-  /**
-   * Instantiates a digest with solid string format digest, this digest should only
-   * be used as a initial.
-   */
-  public static Digest initial(RelNode rel) {
-    return new Digest(rel, simpleRelDigest(rel));
-  }
-}
diff --git a/core/src/main/java/org/apache/calcite/plan/RelDigest.java b/core/src/main/java/org/apache/calcite/plan/RelDigest.java
new file mode 100644
index 0000000..d2716b1
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/RelDigest.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+
+import org.apiguardian.api.API;
+
+/**
+ * The digest is the exact representation of the corresponding {@code RelNode},
+ * at anytime, anywhere. The only difference is that digest is compared using
+ * {@code #equals} and {@code #hashCode}, which are prohibited for RelNode,
+ * for legacy reasons.
+ *
+ * <p>It is highly recommended to override {@link AbstractRelNode#digestHash}
+ * and {@link AbstractRelNode#digestEquals(Object)}, instead of relying on
+ * {@link AbstractRelNode#explainTerms(RelWriter)}, which is used as the
+ * default source for equivalent comparison, for backward compatibility.</p>
+ *
+ * <p>INTERNAL USE ONLY.</p>
+ *
+ * @see AbstractRelNode#digestHash()
+ * @see AbstractRelNode#digestEquals(Object)
+ */
+@API(since = "1.24", status = API.Status.INTERNAL)
+public interface RelDigest {
+  /**
+   * Reset state, possibly cache of hash code.
+   */
+  void clear();
+
+  /**
+   * Returns the relnode that this digest is associated with.
+   */
+  RelNode getRel();
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
index 8484f54..208a87c 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
@@ -18,6 +18,8 @@ package org.apache.calcite.plan;
 
 import org.apache.calcite.rel.type.RelDataType;
 
+import org.apiguardian.api.API;
+
 import java.util.List;
 
 /**
@@ -45,9 +47,20 @@ public interface RelOptNode {
    * <p>If you want a descriptive string which contains the identity, call
    * {@link Object#toString()}, which always returns "rel#{id}:{digest}".
    *
+   * @return Digest string of this {@code RelNode}
+   */
+  default String getDigest() {
+    return getRelDigest().toString();
+  }
+
+  /**
+   * Digest of the {@code RelNode}, for planner internal use only.
+   *
    * @return Digest of this {@code RelNode}
+   * @see #getDigest()
    */
-  Digest getDigest();
+  @API(since = "1.24", status = API.Status.INTERNAL)
+  RelDigest getRelDigest();
 
   /**
    * Retrieves this RelNode's traits. Note that although the RelTraitSet
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index 1083047..c3a8e4e 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -2068,6 +2068,7 @@ public abstract class RelOptUtil {
 
   }
 
+  @Deprecated // to be removed before 1.25
   public static StringBuilder appendRelDescription(
       StringBuilder sb, RelNode rel) {
     sb.append("rel#").append(rel.getId())
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
index 36bd186..97cf205 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
@@ -21,7 +21,7 @@ import org.apache.calcite.linq4j.function.Functions;
 import org.apache.calcite.plan.AbstractRelOptPlanner;
 import org.apache.calcite.plan.CommonRelSubExprRule;
 import org.apache.calcite.plan.Context;
-import org.apache.calcite.plan.Digest;
+import org.apache.calcite.plan.RelDigest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptCostFactory;
 import org.apache.calcite.plan.RelOptCostImpl;
@@ -85,7 +85,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
    * {@link RelDataType} is represented with its field types as {@code List<RelDataType>}.
    * This enables to treat as equal projects that differ in expression names only.
    */
-  private final Map<Digest, HepRelVertex> mapDigestToVertex =
+  private final Map<RelDigest, HepRelVertex> mapDigestToVertex =
       new HashMap<>();
 
   private int nTransformations;
@@ -811,7 +811,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // try to find equivalent rel only if DAG is allowed
     if (!noDag) {
       // Now, check if an equivalent vertex already exists in graph.
-      HepRelVertex equivVertex = mapDigestToVertex.get(rel.getDigest());
+      HepRelVertex equivVertex = mapDigestToVertex.get(rel.getRelDigest());
       if (equivVertex != null) {
         // Use existing vertex.
         return equivVertex;
@@ -900,7 +900,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
       // reachable from here.
       notifyDiscard(vertex.getCurrentRel());
     }
-    Digest oldKey = vertex.getCurrentRel().getDigest();
+    RelDigest oldKey = vertex.getCurrentRel().getRelDigest();
     if (mapDigestToVertex.get(oldKey) == vertex) {
       mapDigestToVertex.remove(oldKey);
     }
@@ -911,7 +911,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // otherwise the digest will be removed wrongly in the mapDigestToVertex
     //  when collectGC
     // so it must update the digest that map to vertex
-    mapDigestToVertex.put(rel.getDigest(), vertex);
+    mapDigestToVertex.put(rel.getRelDigest(), vertex);
     if (rel != vertex.getCurrentRel()) {
       vertex.replaceRel(rel);
     }
@@ -977,7 +977,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     graphSizeLastGC = graph.vertexSet().size();
 
     // Clean up digest map too.
-    Iterator<Map.Entry<Digest, HepRelVertex>> digestIter =
+    Iterator<Map.Entry<RelDigest, HepRelVertex>> digestIter =
         mapDigestToVertex.entrySet().iterator();
     while (digestIter.hasNext()) {
       HepRelVertex vertex = digestIter.next().getValue();
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
index 92394b1..5d52e42 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
@@ -16,7 +16,6 @@
  */
 package org.apache.calcite.plan.hep;
 
-import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitSet;
@@ -76,8 +75,18 @@ public class HepRelVertex extends AbstractRelNode {
     return currentRel.getRowType();
   }
 
-  @Override protected Digest computeDigest() {
-    return Digest.create(this, getRelTypeName() + '#' + getCurrentRel().getId());
+  @Override public String toString() {
+    return "rel#" + id + ':' + "HepRelVertex(" + currentRel + ')';
+  }
+
+  @Override public boolean digestEquals(Object obj) {
+    return this == obj
+        || (obj instanceof HepRelVertex
+            && this.currentRel == ((HepRelVertex) obj).currentRel);
+  }
+
+  @Override public int digestHash() {
+    return this.currentRel.getId();
   }
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java b/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
index e821496..32486ca 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
@@ -17,7 +17,7 @@
 package org.apache.calcite.plan.volcano;
 
 import org.apache.calcite.linq4j.Linq4j;
-import org.apache.calcite.plan.Digest;
+import org.apache.calcite.plan.RelDigest;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptListener;
@@ -129,9 +129,9 @@ public class RelSubset extends AbstractRelNode {
       RelTraitSet traits) {
     super(cluster, traits);
     this.set = set;
+    this.digest = new RelDigest0();
     assert traits.allSimple();
     computeBestCost(cluster.getPlanner());
-    recomputeDigest();
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -225,16 +225,6 @@ public class RelSubset extends AbstractRelNode {
     pw.done(input);
   }
 
-  @Override protected Digest computeDigest() {
-    StringBuilder digest = new StringBuilder(getRelTypeName());
-    digest.append('#');
-    digest.append(set.id);
-    for (RelTrait trait : getTraitSet()) {
-      digest.append('.').append(trait);
-    }
-    return Digest.create(this, digest.toString());
-  }
-
   @Override protected RelDataType deriveRowType() {
     return set.rel.getRowType();
   }
@@ -547,6 +537,28 @@ public class RelSubset extends AbstractRelNode {
     }
   }
 
+  private class RelDigest0 implements RelDigest {
+
+    @Override public RelNode getRel() {
+      return RelSubset.this;
+    }
+
+    @Override public void clear() {
+    }
+
+    @Override public boolean equals(final Object o) {
+      return this == o;
+    }
+
+    @Override public int hashCode() {
+      return id;
+    }
+
+    @Override public String toString() {
+      return "RelSubset#" + set.id + '.' + getTraitSet();
+    }
+  }
+
   /**
    * Visitor which walks over a tree of {@link RelSet}s, replacing each node
    * with the cheapest implementation of the expression.
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
index 4842932..7227293 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
@@ -22,7 +22,7 @@ import org.apache.calcite.plan.AbstractRelOptPlanner;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
-import org.apache.calcite.plan.Digest;
+import org.apache.calcite.plan.RelDigest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptCostFactory;
 import org.apache.calcite.plan.RelOptLattice;
@@ -103,7 +103,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
    * Canonical map from {@link String digest} to the unique
    * {@link RelNode relational expression} with that digest.
    */
-  private final Map<Digest, RelNode> mapDigestToRel =
+  private final Map<RelDigest, RelNode> mapDigestToRel =
       new HashMap<>();
 
   /**
@@ -889,11 +889,12 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
    * @param rel Relational expression
    */
   void rename(RelNode rel) {
-    final Digest oldDigest = rel.getDigest();
+    String oldDigest = null;
+    if (LOGGER.isTraceEnabled()) {
+      oldDigest = rel.getDigest();
+    }
     if (fixUpInputs(rel)) {
-      final RelNode removed = mapDigestToRel.remove(oldDigest);
-      assert removed == rel;
-      final Digest newDigest = rel.recomputeDigest();
+      final RelDigest newDigest = rel.recomputeDigest();
       LOGGER.trace("Rename #{} from '{}' to '{}'", rel.getId(), oldDigest, newDigest);
       final RelNode equivRel = mapDigestToRel.put(newDigest, rel);
       if (equivRel != null) {
@@ -959,7 +960,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     // Is there an equivalent relational expression? (This might have
     // just occurred because the relational expression's child was just
     // found to be equivalent to another set.)
-    RelNode equivRel = mapDigestToRel.get(rel.getDigest());
+    RelNode equivRel = mapDigestToRel.get(rel.getRelDigest());
     if (equivRel != null && equivRel != rel) {
       assert equivRel.getClass() == rel.getClass();
       assert equivRel.getTraitSet().equals(rel.getTraitSet());
@@ -1022,25 +1023,33 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
 
   private boolean fixUpInputs(RelNode rel) {
     List<RelNode> inputs = rel.getInputs();
-    int i = -1;
+    List<RelNode> newInputs = new ArrayList<>(inputs.size());
     int changeCount = 0;
     for (RelNode input : inputs) {
-      ++i;
-      if (input instanceof RelSubset) {
-        final RelSubset subset = (RelSubset) input;
-        RelSubset newSubset = canonize(subset);
-        if (newSubset != subset) {
-          rel.replaceInput(i, newSubset);
-          if (subset.set != newSubset.set) {
-            subset.set.parents.remove(rel);
-            newSubset.set.parents.add(rel);
-          }
-          changeCount++;
+      assert input instanceof RelSubset;
+      final RelSubset subset = (RelSubset) input;
+      RelSubset newSubset = canonize(subset);
+      newInputs.add(newSubset);
+      if (newSubset != subset) {
+        if (subset.set != newSubset.set) {
+          subset.set.parents.remove(rel);
+          newSubset.set.parents.add(rel);
         }
+        changeCount++;
       }
     }
-    RelMdUtil.clearCache(rel);
-    return changeCount > 0;
+
+    if (changeCount > 0) {
+      RelMdUtil.clearCache(rel);
+      RelNode removed = mapDigestToRel.remove(rel.getRelDigest());
+      assert removed == rel;
+      for (int i = 0; i < inputs.size(); i++) {
+        rel.replaceInput(i, newInputs.get(i));
+      }
+      rel.recomputeDigest();
+      return true;
+    }
+    return false;
   }
 
   private RelSet merge(RelSet set, RelSet set2) {
@@ -1168,7 +1177,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
 
     // If it is equivalent to an existing expression, return the set that
     // the equivalent expression belongs to.
-    Digest digest = rel.getDigest();
+    RelDigest digest = rel.getRelDigest();
     RelNode equivExp = mapDigestToRel.get(digest);
     if (equivExp == null) {
       // do nothing
@@ -1198,7 +1207,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
           && (set.equivalentSet == null)) {
         LOGGER.trace(
             "Register #{} {} (and merge sets, because it is a conversion)",
-            rel.getId(), rel.getDigest());
+            rel.getId(), rel.getRelDigest());
         merge(set, childSet);
 
         // During the mergers, the child set may have changed, and since
diff --git a/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java b/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
index c5ec873..1ce29f1 100644
--- a/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
+++ b/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
@@ -18,7 +18,7 @@ package org.apache.calcite.rel;
 
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
-import org.apache.calcite.plan.Digest;
+import org.apache.calcite.plan.RelDigest;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
@@ -27,6 +27,7 @@ import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.hint.Hintable;
 import org.apache.calcite.rel.metadata.Metadata;
 import org.apache.calcite.rel.metadata.MetadataFactory;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
@@ -43,11 +44,13 @@ import org.apache.calcite.util.trace.CalciteTrace;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
+import org.apiguardian.api.API;
 import org.slf4j.Logger;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -72,7 +75,8 @@ public abstract class AbstractRelNode implements RelNode {
   /**
    * The digest that uniquely identifies the node.
    */
-  protected Digest digest;
+  @API(since = "1.24", status = API.Status.INTERNAL)
+  protected RelDigest digest;
 
   private final RelOptCluster cluster;
 
@@ -97,8 +101,7 @@ public abstract class AbstractRelNode implements RelNode {
     this.cluster = cluster;
     this.traitSet = traitSet;
     this.id = NEXT_ID.getAndIncrement();
-    this.digest = Digest.initial(this);
-    LOGGER.trace("new {}", digest);
+    this.digest = new RelDigest0();
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -334,9 +337,8 @@ public abstract class AbstractRelNode implements RelNode {
     return r;
   }
 
-  public Digest recomputeDigest() {
-    digest = computeDigest();
-    assert digest != null : "computeDigest() should be non-null";
+  public RelDigest recomputeDigest() {
+    digest.clear();
     return digest;
   }
 
@@ -348,9 +350,7 @@ public abstract class AbstractRelNode implements RelNode {
 
   /** Description, consists of id plus digest */
   public String toString() {
-    StringBuilder sb = new StringBuilder();
-    RelOptUtil.appendRelDescription(sb, this);
-    return sb.toString();
+    return "rel#" + id + ':' + getDigest();
   }
 
   /** Description, consists of id plus digest */
@@ -359,7 +359,11 @@ public abstract class AbstractRelNode implements RelNode {
     return this.toString();
   }
 
-  public final Digest getDigest() {
+  public final String getDigest() {
+    return digest.toString();
+  }
+
+  public final RelDigest getRelDigest() {
     return digest;
   }
 
@@ -368,17 +372,6 @@ public abstract class AbstractRelNode implements RelNode {
   }
 
   /**
-   * Computes the digest. Does not modify this object.
-   *
-   * @return Digest
-   */
-  protected Digest computeDigest() {
-    RelDigestWriter rdw = new RelDigestWriter();
-    explain(rdw);
-    return rdw.digest;
-  }
-
-  /**
    * {@inheritDoc}
    *
    * <p>This method (and {@link #hashCode} is intentionally final. We do not want
@@ -400,6 +393,73 @@ public abstract class AbstractRelNode implements RelNode {
     return super.hashCode();
   }
 
+  public boolean digestEquals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    AbstractRelNode that = (AbstractRelNode) obj;
+    return this.getTraitSet() == that.getTraitSet()
+        && this.getDigestItems().equals(that.getDigestItems())
+        && Pair.right(getRowType().getFieldList()).equals(
+        Pair.right(that.getRowType().getFieldList()))
+        && (!(that instanceof Hintable)
+            || ((Hintable) this).getHints().equals(
+                ((Hintable) that).getHints()));
+  }
+
+  public int digestHash() {
+    return Objects.hash(getTraitSet(), getDigestItems(),
+        this instanceof Hintable ? ((Hintable) this).getHints() : null);
+  }
+
+  private List<Pair<String, Object>> getDigestItems() {
+    RelDigestWriter rdw = new RelDigestWriter();
+    explainTerms(rdw);
+    return rdw.values;
+  }
+
+  private class RelDigest0 implements RelDigest {
+    /**
+     * Cache of hash code.
+     */
+    private int hash = 0;
+
+    @Override public RelNode getRel() {
+      return AbstractRelNode.this;
+    }
+
+    @Override public void clear() {
+      hash = 0;
+    }
+
+    @Override public boolean equals(final Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      final RelDigest0 relDigest = (RelDigest0) o;
+      return digestEquals(relDigest.getRel());
+    }
+
+    @Override public int hashCode() {
+      if (hash == 0) {
+        hash = digestHash();
+      }
+      return hash;
+    }
+
+    @Override public String toString() {
+      RelDigestWriter rdw = new RelDigestWriter();
+      explain(rdw);
+      return rdw.digest;
+    }
+  }
+
   /**
    * A writer object used exclusively for computing the digest of a RelNode.
    *
@@ -412,7 +472,7 @@ public abstract class AbstractRelNode implements RelNode {
 
     private final List<Pair<String, Object>> values = new ArrayList<>();
 
-    Digest digest = null;
+    String digest = null;
 
     @Override public void explain(final RelNode rel, final List<Pair<String, Object>> valueList) {
       throw new IllegalStateException("Should not be called for computing digest");
@@ -423,12 +483,39 @@ public abstract class AbstractRelNode implements RelNode {
     }
 
     @Override public RelWriter item(String term, Object value) {
+      if (value != null && value.getClass().isArray()) {
+        // We can't call hashCode and equals on Array, so
+        // convert it to String to keep the same behaviour.
+        value = "" + value;
+      }
       values.add(Pair.of(term, value));
       return this;
     }
 
     @Override public RelWriter done(RelNode node) {
-      digest = Digest.create(node, values);
+      StringBuilder sb = new StringBuilder();
+      sb.append(node.getRelTypeName());
+      sb.append('.');
+      sb.append(node.getTraitSet());
+      sb.append('(');
+      int j = 0;
+      for (Pair<String, Object> value : values) {
+        if (j++ > 0) {
+          sb.append(',');
+        }
+        sb.append(value.left);
+        sb.append('=');
+        if (value.right instanceof RelNode) {
+          RelNode input = (RelNode) value.right;
+          sb.append(input.getRelTypeName());
+          sb.append('#');
+          sb.append(input.getId());
+        } else {
+          sb.append(value.right);
+        }
+      }
+      sb.append(')');
+      digest = sb.toString();
       return this;
     }
   }
diff --git a/core/src/main/java/org/apache/calcite/rel/RelNode.java b/core/src/main/java/org/apache/calcite/rel/RelNode.java
index 18ff76f..3d5a08e 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelNode.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelNode.java
@@ -17,7 +17,7 @@
 package org.apache.calcite.rel;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.Digest;
+import org.apache.calcite.plan.RelDigest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptNode;
 import org.apache.calcite.plan.RelOptPlanner;
@@ -33,6 +33,8 @@ import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Litmus;
 
+import org.apiguardian.api.API;
+
 import java.util.List;
 import java.util.Set;
 
@@ -309,7 +311,8 @@ public interface RelNode extends RelOptNode, Cloneable {
    *
    * @return Digest of this relational expression
    */
-  Digest recomputeDigest();
+  @API(since = "1.24", status = API.Status.INTERNAL)
+  RelDigest recomputeDigest();
 
   /**
    * Replaces the <code>ordinalInParent</code><sup>th</sup> input. You must
diff --git a/core/src/main/java/org/apache/calcite/rel/SingleRel.java b/core/src/main/java/org/apache/calcite/rel/SingleRel.java
index a650543..ef06c96 100644
--- a/core/src/main/java/org/apache/calcite/rel/SingleRel.java
+++ b/core/src/main/java/org/apache/calcite/rel/SingleRel.java
@@ -82,6 +82,7 @@ public abstract class SingleRel extends AbstractRelNode {
       RelNode rel) {
     assert ordinalInParent == 0;
     this.input = rel;
+    recomputeDigest();
   }
 
   protected RelDataType deriveRowType() {
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Aggregate.java b/core/src/main/java/org/apache/calcite/rel/core/Aggregate.java
index 82de9fc..ec97781 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Aggregate.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Aggregate.java
@@ -332,6 +332,27 @@ public abstract class Aggregate extends SingleRel implements Hintable {
     return pw;
   }
 
+  protected boolean digestEquals0(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    Aggregate o = (Aggregate) obj;
+    return getTraitSet() == o.getTraitSet()
+        && getInput().equals(o.getInput())
+        && groupSet.equals(o.groupSet)
+        && groupSets.equals(o.groupSets)
+        && aggCalls.equals(o.aggCalls)
+        && hints.equals(o.hints)
+        && getRowType().equals(o.getRowType());
+  }
+
+  protected int digestHash0() {
+    return Objects.hash(traitSet, input, groupSet, groupSets, aggCalls, hints);
+  }
+
   @Override public double estimateRowCount(RelMetadataQuery mq) {
     // Assume that each sort column has 50% of the value count.
     // Therefore one sort column has .5 * rowCount,
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Filter.java b/core/src/main/java/org/apache/calcite/rel/core/Filter.java
index dd13485..d56daf7 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Filter.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Filter.java
@@ -37,6 +37,7 @@ import org.apache.calcite.util.Litmus;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Relational expression that iterates over its input
@@ -99,6 +100,24 @@ public abstract class Filter extends SingleRel {
     return ImmutableList.of(condition);
   }
 
+  protected boolean digestEquals0(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    Filter o = (Filter) obj;
+    return getTraitSet() == o.getTraitSet()
+        && getInput().equals(o.getInput())
+        && condition.equals(o.condition)
+        && getRowType().equals(o.getRowType());
+  }
+
+  protected int digestHash0() {
+    return Objects.hash(traitSet, input, condition);
+  }
+
   public RelNode accept(RexShuttle shuttle) {
     RexNode condition = shuttle.apply(this.condition);
     if (this.condition == condition) {
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Join.java b/core/src/main/java/org/apache/calcite/rel/core/Join.java
index 5d615a8..9f14f97 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Join.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Join.java
@@ -239,6 +239,28 @@ public abstract class Join extends BiRel implements Hintable {
         getSystemFieldList());
   }
 
+  protected boolean digestEquals0(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    Join o = (Join) obj;
+    return getTraitSet() == o.getTraitSet()
+        && getInputs().equals(o.getInputs())
+        && condition.equals(o.condition)
+        && joinType == o.joinType
+        && hints.equals(o.hints)
+        && variablesSet.equals(o.variablesSet)
+        && getRowType().equals(o.getRowType());
+  }
+
+  protected int digestHash0() {
+    return Objects.hash(traitSet, left, right,
+        condition, joinType, hints, variablesSet);
+  }
+
   /**
    * Returns whether this LogicalJoin has already spawned a
    * {@code SemiJoin} via
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Project.java b/core/src/main/java/org/apache/calcite/rel/core/Project.java
index e3c1412..d9abce7 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Project.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Project.java
@@ -48,6 +48,7 @@ import com.google.common.collect.Lists;
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -154,6 +155,26 @@ public abstract class Project extends SingleRel implements Hintable {
     return exps;
   }
 
+  protected boolean digestEquals0(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    Project o = (Project) obj;
+    return getTraitSet() == o.getTraitSet()
+        && getInput().equals(o.getInput())
+        && exps.equals(o.exps)
+        && hints.equals(o.hints)
+        && Pair.right(getRowType().getFieldList()).equals(
+        Pair.right(o.getRowType().getFieldList()));
+  }
+
+  protected int digestHash0() {
+    return Objects.hash(traitSet, input, exps, hints);
+  }
+
   public RelNode accept(RexShuttle shuttle) {
     List<RexNode> exps = shuttle.apply(this.exps);
     if (this.exps == exps) {
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Values.java b/core/src/main/java/org/apache/calcite/rel/core/Values.java
index 8f25815..6386318 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Values.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Values.java
@@ -36,6 +36,7 @@ import org.apache.calcite.util.Pair;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -151,6 +152,23 @@ public abstract class Values extends AbstractRelNode {
     return true;
   }
 
+  @Override public boolean digestEquals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (this.getClass() != obj.getClass()) {
+      return false;
+    }
+    Values o = (Values) obj;
+    return getTraitSet() == o.getTraitSet()
+        && tuples.equals(o.tuples)
+        && getRowType().equals(o.getRowType());
+  }
+
+  @Override public int digestHash() {
+    return Objects.hash(traitSet, tuples);
+  }
+
   @Override protected RelDataType deriveRowType() {
     return rowType;
   }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Window.java b/core/src/main/java/org/apache/calcite/rel/core/Window.java
index 620afe6..dc364ea 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Window.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Window.java
@@ -411,7 +411,10 @@ public abstract class Window extends SingleRel {
     }
 
     @Override public int hashCode() {
-      return Objects.hash(super.hashCode(), ordinal, distinct, ignoreNulls);
+      if (hash == 0) {
+        hash = Objects.hash(super.hashCode(), ordinal, distinct, ignoreNulls);
+      }
+      return hash;
     }
 
     @Override public RexCall clone(RelDataType type, List<RexNode> operands) {
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalAggregate.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalAggregate.java
index 115e423..5940e97 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalAggregate.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalAggregate.java
@@ -161,4 +161,12 @@ public final class LogicalAggregate extends Aggregate {
     return new LogicalAggregate(getCluster(), traitSet, hintList, input,
         groupSet, groupSets, aggCalls);
   }
+
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalFilter.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalFilter.java
index 8995241..894fd17 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalFilter.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalFilter.java
@@ -135,4 +135,16 @@ public final class LogicalFilter extends Filter {
     return super.explainTerms(pw)
         .itemIf("variablesSet", variablesSet, !variablesSet.isEmpty());
   }
+
+  @Override public boolean digestEquals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    return digestEquals0(obj)
+        && variablesSet.equals(((LogicalFilter) obj).variablesSet);
+  }
+
+  @Override public int digestHash() {
+    return Objects.hash(digestHash0(), variablesSet);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
index 4b55785..ec34121 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalJoin.java
@@ -203,4 +203,17 @@ public final class LogicalJoin extends Join {
     return new LogicalJoin(getCluster(), traitSet, hintList,
         left, right, condition, variablesSet, joinType, semiJoinDone, systemFieldList);
   }
+
+  @Override public boolean digestEquals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    return digestEquals0(obj)
+        && semiJoinDone == ((LogicalJoin) obj).semiJoinDone
+        && systemFieldList.equals(((LogicalJoin) obj).systemFieldList);
+  }
+
+  @Override public int digestHash() {
+    return Objects.hash(digestHash0(), semiJoinDone, systemFieldList);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
index 2696dba..22fc72d 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalProject.java
@@ -136,4 +136,12 @@ public final class LogicalProject extends Project {
     return new LogicalProject(getCluster(), traitSet, hintList,
         input, getProjects(), rowType);
   }
+
+  @Override public boolean digestEquals(Object obj) {
+    return digestEquals0(obj);
+  }
+
+  @Override public int digestHash() {
+    return digestHash0();
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
index 5e087fc..1ab362b 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
@@ -400,13 +400,8 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider {
   /** Returns e.g. ", ignoreNulls". */
   private static StringBuilder safeArgList(StringBuilder buff, Method method) {
     for (Ord<Class<?>> t : Ord.zip(method.getParameterTypes())) {
-      if (Primitive.is(t.e)) {
+      if (Primitive.is(t.e) || RexNode.class.isAssignableFrom(t.e)) {
         buff.append(", a").append(t.i);
-      } else if (RexNode.class.isAssignableFrom(t.e)) {
-        // For RexNode, convert to string, because equals does not look deep.
-        //   a1 == null ? "" : a1.toString()
-        buff.append(", a").append(t.i).append(" == null ? \"\" : a")
-            .append(t.i).append(".toString()");
       } else {
         buff.append(", ") .append(NullSentinel.class.getName())
             .append(".mask(a").append(t.i).append(")");
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/MultiJoin.java b/core/src/main/java/org/apache/calcite/rel/rules/MultiJoin.java
index 1b09be8..ebf669b 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/MultiJoin.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/MultiJoin.java
@@ -113,6 +113,7 @@ public final class MultiJoin extends AbstractRelNode {
 
   @Override public void replaceInput(int ordinalInParent, RelNode p) {
     inputs.set(ordinalInParent, p);
+    recomputeDigest();
   }
 
   @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexCall.java b/core/src/main/java/org/apache/calcite/rex/RexCall.java
index 6b8f2ac..3dfe714 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexCall.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexCall.java
@@ -231,17 +231,7 @@ public class RexCall extends RexNode {
   }
 
   @Override public final @Nonnull String toString() {
-    if (!needNormalize()) {
-      // Non-normalize describe is requested
-      return computeDigest(digestWithType());
-    }
-    // This data race is intentional
-    String localDigest = digest;
-    if (localDigest == null) {
-      localDigest = computeDigest(digestWithType());
-      digest = Objects.requireNonNull(localDigest);
-    }
-    return localDigest;
+    return computeDigest(digestWithType());
   }
 
   private boolean digestWithType() {
@@ -336,6 +326,10 @@ public class RexCall extends RexNode {
   }
 
   @Override public int hashCode() {
-    return Objects.hash(op, operands);
+    if (hash == 0) {
+      assert digest == null;
+      hash = Objects.hash(op, operands);
+    }
+    return hash;
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexDynamicParam.java b/core/src/main/java/org/apache/calcite/rex/RexDynamicParam.java
index 7a59021..6e2b1d7 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexDynamicParam.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexDynamicParam.java
@@ -65,12 +65,14 @@ public class RexDynamicParam extends RexVariable {
   @Override public boolean equals(Object obj) {
     return this == obj
         || obj instanceof RexDynamicParam
-        && digest.equals(((RexDynamicParam) obj).digest)
         && type.equals(((RexDynamicParam) obj).type)
         && index == ((RexDynamicParam) obj).index;
   }
 
   @Override public int hashCode() {
-    return Objects.hash(digest, type, index);
+    if (hash == 0) {
+      hash = Objects.hash(type, index);
+    }
+    return hash;
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
index 5617d86..92da77b 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
@@ -1083,13 +1083,19 @@ public class RexLiteral extends RexNode {
   }
 
   public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
     return (obj instanceof RexLiteral)
         && equals(((RexLiteral) obj).value, value)
         && equals(((RexLiteral) obj).type, type);
   }
 
   public int hashCode() {
-    return Objects.hash(value, type);
+    if (hash == 0) {
+      hash = Objects.hash(value, type);
+    }
+    return hash;
   }
 
   public static Comparable value(RexNode node) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexNode.java b/core/src/main/java/org/apache/calcite/rex/RexNode.java
index 88332a2..f4dd69c 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexNode.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexNode.java
@@ -56,6 +56,11 @@ public abstract class RexNode {
   // Effectively final. Set in each sub-class constructor, and never re-set.
   protected String digest;
 
+  /**
+   * Cache of hash code.
+   */
+  protected int hash = 0;
+
   //~ Methods ----------------------------------------------------------------
 
   public abstract RelDataType getType();
diff --git a/core/src/main/java/org/apache/calcite/rex/RexOver.java b/core/src/main/java/org/apache/calcite/rex/RexOver.java
index e90fa3c..6e006c9 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexOver.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexOver.java
@@ -215,6 +215,10 @@ public class RexOver extends RexCall {
   }
 
   @Override public int hashCode() {
-    return Objects.hash(super.hashCode(), window, distinct, ignoreNulls, op.allowsFraming());
+    if (hash == 0) {
+      hash = Objects.hash(super.hashCode(), window,
+          distinct, ignoreNulls, op.allowsFraming());
+    }
+    return hash;
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
index d702387..0dbdbcc 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
@@ -42,7 +42,6 @@ public class RexSubQuery extends RexCall {
       ImmutableList<RexNode> operands, RelNode rel) {
     super(type, op, operands);
     this.rel = rel;
-    this.digest = computeDigest(false);
   }
 
   /** Creates an IN sub-query. */
@@ -139,11 +138,14 @@ public class RexSubQuery extends RexCall {
 
   @Override public boolean equals(Object obj) {
     return obj == this
-        || obj instanceof RexCall
+        || obj instanceof RexSubQuery
         && toString().equals(obj.toString());
   }
 
   @Override public int hashCode() {
-    return toString().hashCode();
+    if (hash == 0) {
+      hash = toString().hashCode();
+    }
+    return hash;
   }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
index a4269c5..90651fa 100644
--- a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
@@ -153,7 +153,7 @@ class HepPlannerTest extends RelOptTestBase {
     assertIncludesExactlyOnce("best.getDescription()",
         best.toString(), "LogicalUnion");
     assertIncludesExactlyOnce("best.getDigest()",
-        best.getDigest().toString(), "LogicalUnion");
+        best.getDigest(), "LogicalUnion");
   }
 
   private void assertIncludesExactlyOnce(String message, String digest, String substring) {