You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by da...@apache.org on 2019/11/12 02:21:48 UTC

[calcite] branch master updated: [CALCITE-482] Implement sql and planner hints

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

danny0405 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 52bc246  [CALCITE-482] Implement sql and planner hints
52bc246 is described below

commit 52bc246d5e07510689635f407f4d9dc494b0af4d
Author: yuzhao.cyz <yu...@alibaba-inc.com>
AuthorDate: Sun Aug 4 15:52:20 2019 +0800

    [CALCITE-482] Implement sql and planner hints
    
    * Define a ORACLE stype hint syntax for both query(after SELECT keyword) hint and table(after table reference) hint
    * The hints would be translated to RelHint instances and propagated
    right after the sql node was converted to relational expression
    * Introduce HintStrategyTable and HintStrategy to customize the hints
    propagation
    * Add a new interface RelOptRuleCall.transformTo(RelNode, Map, BiFunction) to
    make the hints copy strategy overridable
    * Cache the hint strategies into RelOptCluster so that user can query the
    strategies during rule planning
---
 core/src/main/codegen/templates/Parser.jj          | 141 +++++-
 .../adapter/enumerable/EnumerableProject.java      |  11 -
 .../calcite/adapter/enumerable/EnumerableRel.java  |  11 -
 .../org/apache/calcite/adapter/jdbc/JdbcTable.java |   2 +-
 .../org/apache/calcite/plan/RelOptCluster.java     |  31 +-
 .../org/apache/calcite/plan/RelOptRuleCall.java    |  37 +-
 .../java/org/apache/calcite/plan/RelOptUtil.java   |  26 ++
 .../org/apache/calcite/plan/hep/HepRuleCall.java   |   7 +-
 .../calcite/plan/volcano/VolcanoRuleCall.java      |   5 +-
 .../calcite/prepare/CalciteMaterializer.java       |   2 +-
 .../apache/calcite/prepare/CalcitePrepareImpl.java |   2 +-
 .../main/java/org/apache/calcite/rel/RelRoot.java  |  21 +-
 .../java/org/apache/calcite/rel/core/Join.java     |  42 +-
 .../java/org/apache/calcite/rel/core/Project.java  |  28 +-
 .../org/apache/calcite/rel/core/TableScan.java     |  24 +-
 .../calcite/rel/hint/ExplicitHintMatcher.java      |  44 ++
 .../calcite/rel/hint/ExplicitHintStrategy.java     |  53 +++
 .../apache/calcite/rel/hint/HintStrategies.java    |  63 +++
 .../org/apache/calcite/rel/hint/HintStrategy.java  |  48 ++
 .../calcite/rel/hint/HintStrategyCascade.java      |  61 +++
 .../apache/calcite/rel/hint/HintStrategyTable.java | 150 ++++++
 .../java/org/apache/calcite/rel/hint/Hintable.java |  90 ++++
 .../calcite/rel/hint/NodeTypeHintStrategy.java     |  82 ++++
 .../java/org/apache/calcite/rel/hint/RelHint.java  | 112 +++++
 .../org/apache/calcite/rel/hint/package-info.java  |  97 ++++
 .../apache/calcite/rel/logical/LogicalJoin.java    |  25 +-
 .../apache/calcite/rel/logical/LogicalProject.java |  29 +-
 .../calcite/rel/logical/LogicalTableScan.java      |  35 +-
 .../calcite/rel/rel2sql/RelToSqlConverter.java     |  10 +-
 .../apache/calcite/rel/rel2sql/SqlImplementor.java |   2 +-
 .../main/java/org/apache/calcite/sql/SqlHint.java  | 163 +++++++
 .../main/java/org/apache/calcite/sql/SqlKind.java  |  10 +
 .../calcite/sql/SqlProcedureCallOperator.java      |   1 +
 .../java/org/apache/calcite/sql/SqlSelect.java     |  18 +-
 .../org/apache/calcite/sql/SqlSelectOperator.java  |  15 +-
 .../java/org/apache/calcite/sql/SqlTableRef.java   |  72 +++
 .../main/java/org/apache/calcite/sql/SqlUtil.java  |  49 ++
 .../calcite/sql/dialect/MysqlSqlDialect.java       |   3 +-
 .../calcite/sql/validate/IdentifierNamespace.java  |  11 +-
 .../calcite/sql/validate/SqlValidatorImpl.java     |  44 +-
 .../calcite/sql/validate/SqlValidatorUtil.java     |   2 +-
 .../sql2rel/RelStructuredTypeFlattener.java        |   4 +
 .../apache/calcite/sql2rel/SqlToRelConverter.java  | 211 ++++++++-
 .../apache/calcite/sql/parser/SqlParserTest.java   | 116 +++++
 .../java/org/apache/calcite/test/CalciteSuite.java |   1 +
 .../org/apache/calcite/test/MockRelOptPlanner.java |   4 +-
 .../apache/calcite/test/SqlHintsConverterTest.java | 516 +++++++++++++++++++++
 .../org/apache/calcite/test/SqlToRelTestBase.java  |   7 +-
 .../calcite/test/catalog/MockCatalogReader.java    |   4 +-
 .../org/apache/calcite/sql/ddl/SqlDdlNodes.java    |   2 +-
 site/_docs/reference.md                            |  15 +-
 51 files changed, 2458 insertions(+), 101 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 3f80693..9e14098 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -58,6 +58,7 @@ import org.apache.calcite.sql.SqlExplainFormat;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlHint;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlInsert;
 import org.apache.calcite.sql.SqlInsertKeyword;
@@ -90,6 +91,7 @@ import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSelectKeyword;
 import org.apache.calcite.sql.SqlSetOption;
 import org.apache.calcite.sql.SqlSnapshot;
+import org.apache.calcite.sql.SqlTableRef;
 import org.apache.calcite.sql.SqlTimeLiteral;
 import org.apache.calcite.sql.SqlTimestampLiteral;
 import org.apache.calcite.sql.SqlTypeNameSpec;
@@ -1056,6 +1058,115 @@ SqlNode SqlStmtEof() :
     <#include "/@includes/"+file />
 </#list>
 
+SqlNodeList ParenthesizedKeyValueOptionCommaList() :
+{
+    final Span s;
+    final List<SqlNode> list = new ArrayList<SqlNode>();
+}
+{
+    { s = span(); }
+    <LPAREN>
+    [
+        KeyValueOption(list)
+        (
+            <COMMA>
+            KeyValueOption(list)
+        )*
+    ]
+    <RPAREN> {
+        return new SqlNodeList(list, s.end(this));
+    }
+}
+
+/**
+* Parses an option with format key=val whose key is a simple identifier
+* and value is a string literal.
+*/
+void KeyValueOption(List<SqlNode> list) :
+{
+    final SqlIdentifier id;
+    final SqlNode value;
+}
+{
+    id = SimpleIdentifier()
+    <EQ>
+    value = StringLiteral() {
+        list.add(id);
+        list.add(value);
+    }
+}
+
+void CommaSepatatedSqlHints(List<SqlNode> hints) :
+{
+    SqlIdentifier hintName;
+    SqlNodeList hintOptions;
+}
+{
+    hintName = SimpleIdentifier()
+    (   LOOKAHEAD(5)
+        hintOptions = ParenthesizedKeyValueOptionCommaList() {
+            hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+        }
+    |
+        hintOptions = ParenthesizedSimpleIdentifierList() {
+            hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+        }
+    |
+        {
+            hintOptions = SqlNodeList.EMPTY;
+            hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+        }
+    )
+    (
+        <COMMA>
+        hintName = SimpleIdentifier()
+        (
+            LOOKAHEAD(5)
+            hintOptions = ParenthesizedKeyValueOptionCommaList() {
+                hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+            }
+        |
+            hintOptions = ParenthesizedSimpleIdentifierList() {
+                hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+            }
+        |
+            {
+                hintOptions = SqlNodeList.EMPTY;
+                hints.add(new SqlHint(Span.of(hintOptions).end(this), hintName, hintOptions));
+            }
+        )
+    )*
+}
+
+/**
+ * Parses a table reference with optional hints.
+ */
+SqlNode TableRefWithHintsOpt() :
+{
+    SqlNode tableRef;
+    SqlNodeList hintList;
+    final List<SqlNode> hints = new ArrayList<SqlNode>();
+    final Span s;
+}
+{
+    { s = span(); }
+    tableRef = CompoundIdentifier()
+    [
+        LOOKAHEAD(2)
+        <HINT_BEG>
+        CommaSepatatedSqlHints(hints)
+        <COMMENT_END>
+        {
+            hintList = new SqlNodeList(hints, s.addAll(hints).end(this));
+            tableRef = new SqlTableRef(Span.of(tableRef, hintList).pos(),
+                    (SqlIdentifier) tableRef, hintList);
+        }
+    ]
+    {
+        return tableRef;
+    }
+}
+
 /**
  * Parses a leaf SELECT expression without ORDER BY.
  */
@@ -1069,6 +1180,7 @@ SqlSelect SqlSelect() :
     final SqlNodeList groupBy;
     final SqlNode having;
     final SqlNodeList windowDecls;
+    final List<SqlNode> hints = new ArrayList<SqlNode>();
     final Span s;
 }
 {
@@ -1076,6 +1188,11 @@ SqlSelect SqlSelect() :
     {
         s = span();
     }
+    [
+        <HINT_BEG>
+        CommaSepatatedSqlHints(hints)
+        <COMMENT_END>
+    ]
     SqlSelectKeywords(keywords)
     (
         <STREAM> {
@@ -1112,7 +1229,8 @@ SqlSelect SqlSelect() :
     {
         return new SqlSelect(s.end(this), keywordList,
             new SqlNodeList(selectList, Span.of(selectList).pos()),
-            fromClause, where, groupBy, having, windowDecls, null, null, null);
+            fromClause, where, groupBy, having, windowDecls, null, null, null,
+            new SqlNodeList(hints, getPos()));
     }
 }
 
@@ -1360,7 +1478,7 @@ SqlNode SqlInsert() :
     SqlInsertKeywords(keywords) {
         keywordList = new SqlNodeList(keywords, s.addAll(keywords).pos());
     }
-    <INTO> table = CompoundIdentifier()
+    <INTO> table = TableRefWithHintsOpt()
     [
         LOOKAHEAD(5)
         [ <EXTEND> ]
@@ -1409,7 +1527,7 @@ SqlNode SqlDelete() :
     <DELETE> {
         s = span();
     }
-    <FROM> table = CompoundIdentifier()
+    <FROM> table = TableRefWithHintsOpt()
     [
         [ <EXTEND> ]
         extendList = ExtendList() {
@@ -1441,7 +1559,7 @@ SqlNode SqlUpdate() :
 }
 {
     <UPDATE> { s = span(); }
-    table = CompoundIdentifier() {
+    table = TableRefWithHintsOpt() {
         targetColumnList = new SqlNodeList(s.pos());
         sourceExpressionList = new SqlNodeList(s.pos());
     }
@@ -1493,7 +1611,7 @@ SqlNode SqlMerge() :
     final Span s;
 }
 {
-    <MERGE> { s = span(); } <INTO> table = CompoundIdentifier()
+    <MERGE> { s = span(); } <INTO> table = TableRefWithHintsOpt()
     [
         [ <EXTEND> ]
         extendList = ExtendList() {
@@ -1891,7 +2009,7 @@ SqlNode TableRef2(boolean lateral) :
 {
     (
         LOOKAHEAD(2)
-        tableRef = CompoundIdentifier()
+        tableRef = TableRefWithHintsOpt()
         [
             [ <EXTEND> ]
             extendList = ExtendList() {
@@ -3631,6 +3749,7 @@ SqlCall SequenceExpression() :
     (
         <NEXT> { f = SqlStdOperatorTable.NEXT_VALUE; s = span(); }
     |
+        LOOKAHEAD(3)
         <CURRENT> { f = SqlStdOperatorTable.CURRENT_VALUE; s = span(); }
     )
     <VALUE> <FOR> sequenceRef = CompoundIdentifier() {
@@ -7441,6 +7560,12 @@ of the 'normal states'.
 
 /* COMMENTS */
 
+<DEFAULT, DQID, BTID> TOKEN :
+{
+    < HINT_BEG: "/*+" ~["/"] >
+|   < COMMENT_END: "*/" >
+}
+
 <DEFAULT, DQID, BTID> MORE :
 {
     <"/**" ~["/"]> { pushState(); } : IN_FORMAL_COMMENT
@@ -7459,13 +7584,13 @@ of the 'normal states'.
 <IN_FORMAL_COMMENT>
 SPECIAL_TOKEN :
 {
-    <FORMAL_COMMENT: "*/" > { popState(); }
+    <FORMAL_COMMENT: <COMMENT_END> > { popState(); }
 }
 
 <IN_MULTI_LINE_COMMENT>
 SPECIAL_TOKEN :
 {
-    <MULTI_LINE_COMMENT: "*/" > { popState(); }
+    <MULTI_LINE_COMMENT: <COMMENT_END> > { popState(); }
 }
 
 <IN_FORMAL_COMMENT,IN_MULTI_LINE_COMMENT>
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 9418a20..c37dfe6 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
@@ -25,8 +25,6 @@ import org.apache.calcite.rel.metadata.RelMdCollation;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.util.Util;
 
 import java.util.List;
@@ -76,15 +74,6 @@ public class EnumerableProject extends Project implements EnumerableRel {
     return new EnumerableProject(cluster, traitSet, input, projects, rowType);
   }
 
-  static RelNode create(RelNode child, List<? extends RexNode> projects,
-      List<String> fieldNames) {
-    final RelOptCluster cluster = child.getCluster();
-    final RelDataType rowType =
-        RexUtil.createStructType(cluster.getTypeFactory(), projects,
-          fieldNames, SqlValidatorUtil.F_SUGGESTER);
-    return create(child, projects, rowType);
-  }
-
   public EnumerableProject copy(RelTraitSet traitSet, RelNode input,
       List<RexNode> projects, RelDataType rowType) {
     return new EnumerableProject(getCluster(), traitSet, input,
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
index fd80af7..346eb9c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRel.java
@@ -18,9 +18,6 @@ package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.linq4j.tree.BlockStatement;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.RelFactories;
-
-import com.google.common.base.Preconditions;
 
 /**
  * A relational expression of one of the
@@ -29,14 +26,6 @@ import com.google.common.base.Preconditions;
  */
 public interface EnumerableRel
     extends RelNode {
-  RelFactories.FilterFactory FILTER_FACTORY =
-      (input, condition, variablesSet) -> {
-        Preconditions.checkArgument(variablesSet.isEmpty(),
-            "EnumerableFilter does not allow variables");
-        return EnumerableFilter.create(input, condition);
-      };
-
-  RelFactories.ProjectFactory PROJECT_FACTORY = EnumerableProject::create;
 
   //~ Methods ----------------------------------------------------------------
 
diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
index c21d8b0..e14de38 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcTable.java
@@ -147,7 +147,7 @@ public class JdbcTable extends AbstractQueryableTable
             SqlParserPos.ZERO);
     SqlSelect node =
         new SqlSelect(SqlParserPos.ZERO, SqlNodeList.EMPTY, selectList,
-            tableName(), null, null, null, null, null, null, null);
+            tableName(), null, null, null, null, null, null, null, null);
     final SqlPrettyWriter writer = new SqlPrettyWriter(jdbcSchema.dialect);
     node.unparse(writer, 0, 0);
     return writer.toSqlString();
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java b/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
index 79dcc5c..d68a36c 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptCluster.java
@@ -18,6 +18,7 @@ package org.apache.calcite.plan;
 
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.hint.HintStrategyTable;
 import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
 import org.apache.calcite.rel.metadata.JaninoRelMetadataProvider;
 import org.apache.calcite.rel.metadata.MetadataFactory;
@@ -50,6 +51,7 @@ public class RelOptCluster {
   private final RexBuilder rexBuilder;
   private RelMetadataProvider metadataProvider;
   private MetadataFactory metadataFactory;
+  private HintStrategyTable hintStrategies;
   private final RelTraitSet emptyTraitSet;
   private RelMetadataQuery mq;
   private Supplier<RelMetadataQuery> mqSupplier;
@@ -179,7 +181,6 @@ public class RelOptCluster {
   }
 
   /**
-   * Returns the RelMetadataQuery supplier
    * @return The supplier of RelMetadataQuery
    */
   public Supplier<RelMetadataQuery> getMetadataQuerySupplier() {
@@ -195,6 +196,34 @@ public class RelOptCluster {
   }
 
   /**
+   * Setup the hint propagation strategies to be used during rule planning.
+   *
+   * <p>Use <code>RelOptNode.getCluster().getHintStrategies()</code> to fetch
+   * the hint strategies.
+   *
+   * <p>Note that this method is only for internal use, the cluster {@code hintStrategies}
+   * would be always set up with the instance configured by
+   * {@link org.apache.calcite.sql2rel.SqlToRelConverter.ConfigBuilder}.
+   *
+   * @param hintStrategies The specified hint strategies to override the default one(empty)
+   */
+  public RelOptCluster withHintStrategies(HintStrategyTable hintStrategies) {
+    Objects.requireNonNull(hintStrategies);
+    this.hintStrategies = hintStrategies;
+    return this;
+  }
+
+  /**
+   * Returns the hint strategies of this cluster. It is immutable during the whole planning phrase.
+   */
+  public HintStrategyTable getHintStrategies() {
+    if (this.hintStrategies == null) {
+      this.hintStrategies = HintStrategyTable.EMPTY;
+    }
+    return this.hintStrategies;
+  }
+
+  /**
    * Constructs a new id for a correlating variable. It is unique within the
    * whole query.
    */
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
index 3de7571..9a3db76 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
@@ -18,6 +18,7 @@ package org.apache.calcite.plan;
 
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.hint.Hintable;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.trace.CalciteTrace;
@@ -30,6 +31,7 @@ import org.slf4j.Logger;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 
 /**
  * A <code>RelOptRuleCall</code> is an invocation of a {@link RelOptRule} with a
@@ -220,16 +222,49 @@ public abstract class RelOptRuleCall {
    * rel.getTraits()</code> will be copied from <code>
    * this.rels[0].getTraitSet()</code>.
    *
+   * <p>The implementation of this method also guarantees that the original
+   * relational expression (that is, <code>this.rels[0]</code>) has its hints propagated to
+   * the new relational expression (<code>rel</code>). The hints propagation strategy can be
+   * customized through specified {@code handler}.
+   *
+   * @param rel     Relational expression equivalent to the root relational
+   *                expression of the rule call, {@code call.rels(0)}
+   * @param equiv   Map of other equivalences
+   * @param handler Handler to customize the relational expression that would
+   *                be registered into the planner, the 1th argument is the
+   *                original expression and the 2th argument is the new relational
+   *                expression
+   */
+  public abstract void transformTo(RelNode rel,
+      Map<RelNode, RelNode> equiv,
+      BiFunction<RelNode, RelNode, RelNode> handler);
+
+  /**
+   * Registers that a rule has produced an equivalent relational expression,
+   * with specified equivalences.
+   *
+   * <p>The hints are copied fully from the original expression
+   * (that is, <code>this.rels[0]</code>) to the new relational expression
+   * (<code>rel</code>) if both of them are all instances of
+   * {@link Hintable}.
+   *
    * @param rel   Relational expression equivalent to the root relational
    *              expression of the rule call, {@code call.rels(0)}
    * @param equiv Map of other equivalences
    */
-  public abstract void transformTo(RelNode rel, Map<RelNode, RelNode> equiv);
+  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+    transformTo(rel, equiv, RelOptUtil::copyRelHints);
+  }
 
   /**
    * Registers that a rule has produced an equivalent relational expression,
    * but no other equivalences.
    *
+   * <p>The hints are copied fully from the original expression
+   * (that is, <code>this.rels[0]</code>) to the new relational expression
+   * (<code>rel</code>) if both of them are all instances of
+   * {@link Hintable}.
+   *
    * @param rel Relational expression equivalent to the root relational
    *            expression of the rule call, {@code call.rels(0)}
    */
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 44fa568..042c0f6 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -44,6 +44,9 @@ import org.apache.calcite.rel.core.TableScan;
 import org.apache.calcite.rel.externalize.RelJsonWriter;
 import org.apache.calcite.rel.externalize.RelWriterImpl;
 import org.apache.calcite.rel.externalize.RelXmlWriter;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.rel.logical.LogicalCalc;
 import org.apache.calcite.rel.logical.LogicalFilter;
@@ -384,6 +387,29 @@ public abstract class RelOptUtil {
   }
 
   /**
+   * Copy the {@link org.apache.calcite.rel.hint.RelHint}s from {@code originalRel}
+   * to {@code newRel} if both of them are {@link Hintable}.
+   *
+   * <p>The hints are also filtered by the specified hint strategies.
+   *
+   * @param originalRel Original relational expression
+   * @param newRel      New relational expression
+   * @return A copy of {@code newRel} with hints of {@code originalRel}, or {@code newRel}
+   * directly if one of them are not {@link Hintable}
+   */
+  public static RelNode copyRelHints(RelNode originalRel, RelNode newRel) {
+    if (originalRel instanceof Hintable
+        && newRel instanceof Hintable
+        && ((Hintable) originalRel).getHints().size() > 0) {
+      HintStrategyTable hintStrategies = originalRel.getCluster().getHintStrategies();
+      final List<RelHint> hints = ((Hintable) originalRel).getHints();
+      return ((Hintable) newRel)
+          .attachHints(hintStrategies.apply(hints, newRel));
+    }
+    return newRel;
+  }
+
+  /**
    * Returns a permutation describing where output fields come from. In
    * the returned map, value of {@code map.getTargetOpt(i)} is {@code n} if
    * field {@code i} projects input field {@code n} or applies a cast on
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
index 3a40ec0..38f846a 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.RelNode;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 
 /**
  * HepRuleCall implements {@link RelOptRuleCall} for a {@link HepPlanner}. It
@@ -52,11 +53,13 @@ public class HepRuleCall extends RelOptRuleCall {
   //~ Methods ----------------------------------------------------------------
 
   // implement RelOptRuleCall
-  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+      BiFunction<RelNode, RelNode, RelNode> handler) {
     final RelNode rel0 = rels[0];
     RelOptUtil.verifyTypeEquivalence(rel0, rel, rel0);
+    rel = handler.apply(rel0, rel);
     results.add(rel);
-    rel(0).getCluster().invalidateMetadataQuery();
+    rel0.getCluster().invalidateMetadataQuery();
   }
 
   List<RelNode> getResults() {
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
index aff1eb4..786a036 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
@@ -33,6 +33,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 
 /**
  * <code>VolcanoRuleCall</code> implements the {@link RelOptRuleCall} interface
@@ -87,7 +88,9 @@ public class VolcanoRuleCall extends RelOptRuleCall {
   //~ Methods ----------------------------------------------------------------
 
   // implement RelOptRuleCall
-  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+      BiFunction<RelNode, RelNode, RelNode> handler) {
+    rel = handler.apply(rels[0], rel);
     if (LOGGER.isDebugEnabled()) {
       LOGGER.debug("Transform to: rel#{} via {}{}", rel.getId(), getRule(),
           equiv.isEmpty() ? "" : " with equivalences " + equiv);
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java b/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
index 86bbca9..ae02dcd 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalciteMaterializer.java
@@ -99,7 +99,7 @@ class CalciteMaterializer extends CalcitePrepareImpl.CalcitePreparingStmt {
 
     RelOptTable table =
         this.catalogReader.getTable(materialization.materializedTable.path());
-    materialization.tableRel = sqlToRelConverter2.toRel(table);
+    materialization.tableRel = sqlToRelConverter2.toRel(table, null);
   }
 
   /** Converts a relational expression to use a
diff --git a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
index 8f076a2..898f955 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalcitePrepareImpl.java
@@ -985,7 +985,7 @@ public class CalcitePrepareImpl implements CalcitePrepare {
               ? ((Sort) rel).collation
               : RelCollations.EMPTY;
       RelRoot root = new RelRoot(rel, resultType, SqlKind.SELECT, fields,
-          collation);
+          collation, new ArrayList<>());
 
       if (timingTracer != null) {
         timingTracer.traceTime("end sql2rel");
diff --git a/core/src/main/java/org/apache/calcite/rel/RelRoot.java b/core/src/main/java/org/apache/calcite/rel/RelRoot.java
index c744492..887a476 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelRoot.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelRoot.java
@@ -16,6 +16,8 @@
  */
 package org.apache.calcite.rel;
 
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
@@ -78,6 +80,7 @@ public class RelRoot {
   public final SqlKind kind;
   public final ImmutableList<Pair<Integer, String>> fields;
   public final RelCollation collation;
+  public final ImmutableList<RelHint> hints;
 
   /**
    * Creates a RelRoot.
@@ -87,12 +90,13 @@ public class RelRoot {
    */
 
   public RelRoot(RelNode rel, RelDataType validatedRowType, SqlKind kind,
-       List<Pair<Integer, String>> fields, RelCollation collation) {
+       List<Pair<Integer, String>> fields, RelCollation collation, List<RelHint> hints) {
     this.rel = rel;
     this.validatedRowType = validatedRowType;
     this.kind = kind;
     this.fields = ImmutableList.copyOf(fields);
     this.collation = Objects.requireNonNull(collation);
+    this.hints = ImmutableList.copyOf(hints);
   }
 
   /** Creates a simple RelRoot. */
@@ -106,7 +110,7 @@ public class RelRoot {
         ImmutableIntList.identity(rowType.getFieldCount());
     final List<String> names = rowType.getFieldNames();
     return new RelRoot(rel, rowType, kind, Pair.zip(refs, names),
-        RelCollations.EMPTY);
+        RelCollations.EMPTY, new ArrayList<>());
   }
 
   @Override public String toString() {
@@ -122,7 +126,7 @@ public class RelRoot {
     if (rel == this.rel) {
       return this;
     }
-    return new RelRoot(rel, validatedRowType, kind, fields, collation);
+    return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
   }
 
   /** Creates a copy, assigning a new kind. */
@@ -130,11 +134,16 @@ public class RelRoot {
     if (kind == this.kind) {
       return this;
     }
-    return new RelRoot(rel, validatedRowType, kind, fields, collation);
+    return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
   }
 
   public RelRoot withCollation(RelCollation collation) {
-    return new RelRoot(rel, validatedRowType, kind, fields, collation);
+    return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
+  }
+
+  /** Creates a copy, assigning the query hints. */
+  public RelRoot withHints(List<RelHint> hints) {
+    return new RelRoot(rel, validatedRowType, kind, fields, collation, hints);
   }
 
   /** Returns the root relational expression, creating a {@link LogicalProject}
@@ -158,7 +167,7 @@ public class RelRoot {
     for (Pair<Integer, String> field : fields) {
       projects.add(rexBuilder.makeInputRef(rel, field.left));
     }
-    return LogicalProject.create(rel, projects, Pair.right(fields));
+    return RelOptUtil.copyRelHints(rel, LogicalProject.create(rel, projects, Pair.right(fields)));
   }
 
   public boolean isNameTrivial() {
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 57339d9..6ffc395 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
@@ -23,6 +23,8 @@ import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.BiRel;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
@@ -39,6 +41,7 @@ import org.apache.calcite.util.Util;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -52,11 +55,12 @@ import java.util.Set;
  * The set of output rows is a subset of the cartesian product of the two
  * inputs; precisely which subset depends on the join condition.
  */
-public abstract class Join extends BiRel {
+public abstract class Join extends BiRel implements Hintable {
   //~ Instance fields --------------------------------------------------------
 
   protected final RexNode condition;
   protected final ImmutableSet<CorrelationId> variablesSet;
+  protected final ImmutableList<RelHint> hints;
 
   /**
    * Values must be of enumeration {@link JoinRelType}, except that
@@ -86,6 +90,7 @@ public abstract class Join extends BiRel {
    *
    * @param cluster          Cluster
    * @param traitSet         Trait set
+   * @param hints            Hints
    * @param left             Left input
    * @param right            Right input
    * @param condition        Join condition
@@ -97,6 +102,7 @@ public abstract class Join extends BiRel {
   protected Join(
       RelOptCluster cluster,
       RelTraitSet traitSet,
+      List<RelHint> hints,
       RelNode left,
       RelNode right,
       RexNode condition,
@@ -107,6 +113,36 @@ public abstract class Join extends BiRel {
     this.variablesSet = ImmutableSet.copyOf(variablesSet);
     this.joinType = Objects.requireNonNull(joinType);
     this.joinInfo = JoinInfo.of(left, right, condition);
+    this.hints = ImmutableList.copyOf(hints);
+  }
+
+  /**
+   * Creates a Join.
+   *
+   * <p>Note: We plan to change the {@code variablesStopped} parameter to
+   * {@code Set&lt;CorrelationId&gt; variablesSet}
+   * {@link org.apache.calcite.util.Bug#upgrade(String) before version 2.0},
+   * because {@link #getVariablesSet()}
+   * is preferred over {@link #getVariablesStopped()}.
+   * This constructor is not deprecated, for now, because maintaining overloaded
+   * constructors in multiple sub-classes would be onerous.
+   *
+   * @param cluster          Cluster
+   * @param traitSet         Trait set
+   * @param left             Left input
+   * @param right            Right input
+   * @param condition        Join condition
+   * @param joinType         Join type
+   * @param variablesSet     Set variables that are set by the
+   *                         LHS and used by the RHS and are not available to
+   *                         nodes above this Join in the tree
+   */
+  protected Join(
+      RelOptCluster cluster, RelTraitSet traitSet, RelNode left,
+      RelNode right, RexNode condition, Set<CorrelationId> variablesSet,
+      JoinRelType joinType) {
+    this(cluster, traitSet, new ArrayList<>(), left, right,
+        condition, variablesSet, joinType);
   }
 
   @Deprecated // to be removed before 2.0
@@ -311,6 +347,10 @@ public abstract class Join extends BiRel {
   public JoinInfo analyzeCondition() {
     return joinInfo;
   }
+
+  @Override public ImmutableList<RelHint> getHints() {
+    return hints;
+  }
 }
 
 // End Join.java
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 eee00b3..381bba2 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
@@ -25,6 +25,8 @@ import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.SingleRel;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
@@ -44,6 +46,7 @@ import org.apache.calcite.util.mapping.Mappings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -54,11 +57,13 @@ import java.util.Set;
  *
  * @see org.apache.calcite.rel.logical.LogicalProject
  */
-public abstract class Project extends SingleRel {
+public abstract class Project extends SingleRel implements Hintable {
   //~ Instance fields --------------------------------------------------------
 
   protected final ImmutableList<RexNode> exps;
 
+  protected final ImmutableList<RelHint> hints;
+
   //~ Constructors -----------------------------------------------------------
 
   /**
@@ -66,6 +71,7 @@ public abstract class Project extends SingleRel {
    *
    * @param cluster  Cluster that this relational expression belongs to
    * @param traits   Traits of this relational expression
+   * @param hints    Hints of this relation expression
    * @param input    Input relational expression
    * @param projects List of expressions for the input columns
    * @param rowType  Output row type
@@ -73,16 +79,32 @@ public abstract class Project extends SingleRel {
   protected Project(
       RelOptCluster cluster,
       RelTraitSet traits,
+      List<RelHint> hints,
       RelNode input,
       List<? extends RexNode> projects,
       RelDataType rowType) {
     super(cluster, traits, input);
     assert rowType != null;
     this.exps = ImmutableList.copyOf(projects);
+    this.hints = ImmutableList.copyOf(hints);
     this.rowType = rowType;
     assert isValid(Litmus.THROW, null);
   }
 
+  /**
+   * Creates a Project.
+   *
+   * @param cluster  Cluster that this relational expression belongs to
+   * @param traits   Traits of this relational expression
+   * @param input    Input relational expression
+   * @param projects List of expressions for the input columns
+   * @param rowType  Output row type
+   */
+  protected Project(RelOptCluster cluster, RelTraitSet traits,
+      RelNode input, List<? extends RexNode> projects, RelDataType rowType) {
+    this(cluster, traits, new ArrayList<>(), input, projects, rowType);
+  }
+
   @Deprecated // to be removed before 2.0
   protected Project(RelOptCluster cluster, RelTraitSet traitSet, RelNode input,
       List<? extends RexNode> projects, RelDataType rowType, int flags) {
@@ -173,6 +195,10 @@ public abstract class Project extends SingleRel {
     return Pair.zip(getProjects(), getRowType().getFieldNames());
   }
 
+  @Override public ImmutableList<RelHint> getHints() {
+    return hints;
+  }
+
   @Deprecated // to be removed before 2.0
   public int getFlags() {
     return 1;
diff --git a/core/src/main/java/org/apache/calcite/rel/core/TableScan.java b/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
index 07dce01..b461915 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/TableScan.java
@@ -27,6 +27,8 @@ import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelShuttle;
 import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
@@ -36,6 +38,8 @@ import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -43,7 +47,8 @@ import java.util.Set;
 /**
  * Relational operator that returns the contents of a table.
  */
-public abstract class TableScan extends AbstractRelNode {
+public abstract class TableScan
+    extends AbstractRelNode implements Hintable {
   //~ Instance fields --------------------------------------------------------
 
   /**
@@ -51,15 +56,26 @@ public abstract class TableScan extends AbstractRelNode {
    */
   protected final RelOptTable table;
 
+  /**
+   * The table hints.
+   */
+  protected final ImmutableList<RelHint> hints;
+
   //~ Constructors -----------------------------------------------------------
 
   protected TableScan(RelOptCluster cluster, RelTraitSet traitSet,
-      RelOptTable table) {
+      List<RelHint> hints, RelOptTable table) {
     super(cluster, traitSet);
     this.table = table;
     if (table.getRelOptSchema() != null) {
       cluster.getPlanner().registerSchema(table.getRelOptSchema());
     }
+    this.hints = ImmutableList.copyOf(hints);
+  }
+
+  protected TableScan(RelOptCluster cluster, RelTraitSet traitSet,
+      RelOptTable table) {
+    this(cluster, traitSet, new ArrayList<>(), table);
   }
 
   /**
@@ -162,6 +178,10 @@ public abstract class TableScan extends AbstractRelNode {
   @Override public RelNode accept(RelShuttle shuttle) {
     return shuttle.visit(this);
   }
+
+  @Override public ImmutableList<RelHint> getHints() {
+    return hints;
+  }
 }
 
 // End TableScan.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java
new file mode 100644
index 0000000..b366545
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A function to customize whether a relational expression should match a hint.
+ *
+ * <p>Usually you may not need to implement this function,
+ * {@link NodeTypeHintStrategy} is enough for most of the {@link RelHint}s.
+ *
+ * <p>Some of the hints can only be matched to the relational
+ * expression with special match conditions(not only the relational expression type).
+ * i.e. "hash_join(r, st)", this hint can only be applied to JOIN expression that
+ * has "r" and "st" as the input table names. To implement this, you may need to customize an
+ * {@link ExplicitHintStrategy} with the {@link ExplicitHintMatcher}.
+ *
+ * @param <R> Relational expression to test if the hint matches
+ *
+ * @see ExplicitHintStrategy
+ * @see HintStrategies
+ */
+@FunctionalInterface
+public interface ExplicitHintMatcher<R extends RelNode> {
+  boolean apply(RelHint hint, R node);
+}
+
+// End ExplicitHintMatcher.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java
new file mode 100644
index 0000000..a70187d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/ExplicitHintStrategy.java
@@ -0,0 +1,53 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A hint strategy whose rules are totally customized.
+ *
+ * @see ExplicitHintMatcher
+ */
+public class ExplicitHintStrategy implements HintStrategy {
+  //~ Instance fields --------------------------------------------------------
+
+  private final ExplicitHintMatcher matcher;
+
+  /**
+   * Creates an {@code ExplicitHintStrategy} with specified {@code matcher}.
+   *
+   * <p>Make this constructor package-protected intentionally, use
+   * {@link HintStrategies#explicit(ExplicitHintMatcher)}.
+   *
+   * @param matcher ExplicitHintMatcher instance to test
+   *                if a hint can be applied to a rel
+   */
+  ExplicitHintStrategy(ExplicitHintMatcher matcher) {
+    this.matcher = matcher;
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+    //noinspection unchecked
+    return this.matcher.apply(hint, rel);
+  }
+}
+
+// End ExplicitHintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java
new file mode 100644
index 0000000..4d97786
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategies.java
@@ -0,0 +1,63 @@
+/*
+ * 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.rel.hint;
+
+/**
+ * A collection of hint strategies.
+ */
+public abstract class HintStrategies {
+  /** A hint strategy that indicates a hint can only be used to
+   * the whole query(no specific nodes). */
+  public static final HintStrategy SET_VAR =
+      new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.SET_VAR);
+
+  /** A hint strategy that indicates a hint can only be used to
+   * {@link org.apache.calcite.rel.core.Join} nodes. */
+  public static final HintStrategy JOIN =
+      new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.JOIN);
+
+  /** A hint strategy that indicates a hint can only be used to
+   * {@link org.apache.calcite.rel.core.TableScan} nodes. */
+  public static final HintStrategy TABLE_SCAN =
+      new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.TABLE_SCAN);
+
+  /** A hint strategy that indicates a hint can only be used to
+   * {@link org.apache.calcite.rel.core.Project} nodes. */
+  public static final HintStrategy PROJECT =
+      new NodeTypeHintStrategy(NodeTypeHintStrategy.NodeType.PROJECT);
+
+  /**
+   * Create a hint strategy from a specific matcher whose rules are totally customized.
+   *
+   * @param matcher The strategy matcher
+   * @return A ExplicitHintStrategy instance.
+   */
+  public static HintStrategy explicit(ExplicitHintMatcher matcher) {
+    return new ExplicitHintStrategy(matcher);
+  }
+
+  /**
+   * Creates a HintStrategyCascade instance whose strategy rules are satisfied only if
+   * all the {@code hintStrategies} are satisfied.
+   */
+  public static HintStrategy cascade(HintStrategy... hintStrategies) {
+    return new HintStrategyCascade(hintStrategies);
+  }
+}
+
+// End HintStrategies.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java
new file mode 100644
index 0000000..8e7623b
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategy.java
@@ -0,0 +1,48 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * A {@code HintStrategy} indicates whether a {@link org.apache.calcite.rel.RelNode}
+ * can apply the specified hint.
+ *
+ * <p>Typically, every supported hint should register a {@code HintStrategy}
+ * into the {@link HintStrategyTable}. For example, {@link HintStrategies#JOIN} implies
+ * that this hint would be propagated and applied to the {@link org.apache.calcite.rel.core.Join}
+ * relational expressions.
+ *
+ * <p>In {@link HintStrategyTable} the strategy is used for
+ * hints registration.
+ *
+ * @see HintStrategyTable
+ */
+public interface HintStrategy {
+
+  /**
+   * Decides if the given {@code hint} can be applied to
+   * the relational expression {@code rel}.
+   *
+   * @param hint The hint
+   * @param rel  The relational expression
+   * @return True if the {@code hint} can be applied to the {@code rel}
+   */
+  boolean supportsRel(RelHint hint, RelNode rel);
+}
+
+// End HintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java
new file mode 100644
index 0000000..7494f7b
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyCascade.java
@@ -0,0 +1,61 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Strategy to decide if a hint should apply to a relational expression
+ * by using a series of {@link HintStrategy} rules in a given order.
+ * If a rule fails, returns false early, else return true.
+ */
+public class HintStrategyCascade implements HintStrategy {
+  //~ Instance fields --------------------------------------------------------
+
+  private ImmutableList<HintStrategy> strategies;
+
+  /**
+   * Creates a HintStrategyCascade from an array of hint strategies.
+   *
+   * <p>Make this constructor package-protected intentionally.
+   * Use {@link HintStrategies#cascade}.</p>
+   */
+  HintStrategyCascade(HintStrategy... strategies) {
+    assert strategies != null;
+    assert strategies.length > 1;
+    for (HintStrategy strategy : strategies) {
+      assert strategy != null;
+    }
+    this.strategies = ImmutableList.copyOf(strategies);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+    for (HintStrategy strategy : strategies) {
+      if (!strategy.supportsRel(hint, rel)) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
+
+// End HintStrategyCascade.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java
new file mode 100644
index 0000000..c6ce9be
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/HintStrategyTable.java
@@ -0,0 +1,150 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * {@code HintStrategy} collection indicating which kind of
+ * {@link org.apache.calcite.rel.RelNode} a hint can apply to.
+ *
+ * <p>Typically, every supported hints should register a {@code HintStrategy}
+ * in this collection. For example, {@link HintStrategies#JOIN} implies that this hint
+ * would be propagated and applied to the {@link org.apache.calcite.rel.core.Join}
+ * relational expressions.
+ *
+ * <p>A {@code HintStrategy} can be used independently or cascaded with other strategies
+ * with method {@link HintStrategies#cascade}.
+ *
+ * <p>The matching for hint name is case in-sensitive.
+ *
+ * @see HintStrategy
+ */
+public class HintStrategyTable {
+  //~ Static fields/initializers ---------------------------------------------
+
+  /** Empty strategies. */
+  // Need to replace the EMPTY with DEFAULT if we have any hint implementations.
+  public static final HintStrategyTable EMPTY = new HintStrategyTable(new HashMap<>());
+
+  //~ Instance fields --------------------------------------------------------
+
+  /** Mapping from hint name to strategy. */
+  private final Map<Key, HintStrategy> hintStrategyMap;
+
+  private HintStrategyTable(Map<Key, HintStrategy> strategies) {
+    this.hintStrategyMap = ImmutableMap.copyOf(strategies);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  /**
+   * Apply this {@link HintStrategyTable} to the given relational
+   * expression for the {@code hints}.
+   *
+   * @param hints Hints that may attach to the {@code rel}
+   * @param rel   Relational expression
+   * @return A hints list that can be attached to the {@code rel}
+   */
+  public List<RelHint> apply(List<RelHint> hints, RelNode rel) {
+    return hints.stream()
+        .filter(relHint -> supportsRel(relHint, rel))
+        .collect(Collectors.toList());
+  }
+
+  /**
+   * Check if the give hint name is valid.
+   *
+   * @param name The hint name
+   */
+  public void validateHint(String name) {
+    if (!this.hintStrategyMap.containsKey(Key.of(name))) {
+      throw new RuntimeException("Hint: " + name + " should be registered in the HintStrategies.");
+    }
+  }
+
+  private boolean supportsRel(RelHint hint, RelNode rel) {
+    final Key key = Key.of(hint.hintName);
+    assert this.hintStrategyMap.containsKey(key);
+    return this.hintStrategyMap.get(key).supportsRel(hint, rel);
+  }
+
+  /**
+   * @return A strategies builder
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Key used to keep the strategies which ignores the case sensitivity.
+   */
+  private static class Key {
+    private String name;
+    private Key(String name) {
+      this.name = name;
+    }
+
+    static Key of(String name) {
+      return new Key(name.toLowerCase(Locale.ROOT));
+    }
+
+    @Override public boolean equals(Object obj) {
+      if (!(obj instanceof Key)) {
+        return false;
+      }
+      return Objects.equals(this.name, ((Key) obj).name);
+    }
+
+    @Override public int hashCode() {
+      return this.name.hashCode();
+    }
+  }
+
+  //~ Inner Class ------------------------------------------------------------
+
+  /**
+   * Builder for {@code HintStrategyTable}.
+   */
+  public static class Builder {
+    private Map<Key, HintStrategy> hintStrategyMap;
+
+    public Builder() {
+      this.hintStrategyMap = new HashMap<>();
+    }
+
+    public Builder addHintStrategy(String hintName, HintStrategy strategy) {
+      this.hintStrategyMap.put(Key.of(hintName), strategy);
+      return this;
+    }
+
+    public HintStrategyTable build() {
+      return new HintStrategyTable(this.hintStrategyMap);
+    }
+  }
+}
+
+// End HintStrategyTable.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java b/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java
new file mode 100644
index 0000000..20e1864
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/Hintable.java
@@ -0,0 +1,90 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.linq4j.function.Experimental;
+import org.apache.calcite.rel.RelNode;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Base class for relational expressions with {@link RelHint}s.
+ *
+ * <p>Relational expressions that can attach hints should implement
+ * this interface.
+ *
+ * <p>This interface is experimental, currently, we make
+ * {@link org.apache.calcite.rel.core.Project}
+ * {@link org.apache.calcite.rel.core.Join}
+ * {@link org.apache.calcite.rel.core.TableScan}
+ * implement this interface and add an argument named "hints" to construct these
+ * relational expressions if hints are attached.
+ *
+ * <p>This design is not that elegant and mature, because we have to copy the hints whenever these
+ * relational expressions are copied or used to derive new relational expressions.
+ * Even though we have implemented the mechanism to propagate the hints, for large queries,
+ * there would be many cases where the hints are not copied to the right RelNode,
+ * and the effort/memory is wasted if we are copying the hint to a RelNode
+ * but the hint is not used.
+ */
+@Experimental
+public interface Hintable {
+
+  /**
+   * Attach list of hints to this relational expression, should be overridden by
+   * every logical node that supports hint. This method is only for
+   * internal use during sql-to-rel conversion.
+   *
+   * <p>The sub-class should return a new copy of the relational expression. We make
+   * the default implementation return the relational expression directly only
+   * because not every kind of relational expression supports hints.
+   *
+   * @param hintList The hints to attach to this relational expression
+   * @return Relational expression with the hints {@code hintList} attached
+   */
+  default RelNode attachHints(List<RelHint> hintList) {
+    return (RelNode) this;
+  }
+
+  /**
+   * @return The hints list of this relational expressions
+   */
+  ImmutableList<RelHint> getHints();
+
+  /**
+   * Merge this relation expression's hints with the given hint list.
+   *
+   * <p>The default behavior is to put them in one list and eliminate the duplicates.
+   *
+   * @param hintList Hints to be merged
+   * @return A merged hint list
+   */
+  default List<RelHint> mergeHints(List<RelHint> hintList) {
+    Objects.requireNonNull(hintList);
+    final Set<RelHint> hints = new LinkedHashSet<>(getHints());
+    hints.addAll(hintList);
+    return new ArrayList<>(hints);
+  }
+}
+
+// End Hintable.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java b/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java
new file mode 100644
index 0000000..3b95492
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/NodeTypeHintStrategy.java
@@ -0,0 +1,82 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.TableScan;
+
+/**
+ * A hint strategy that specifies which kind of relational
+ * expression the hint can be applied to.
+ */
+public class NodeTypeHintStrategy implements HintStrategy {
+
+  /**
+   * Enumeration of the relational expression types that the hints
+   * may be propagated to.
+   */
+  enum NodeType {
+    /**
+     * The hint is used for the whole query, kind of like a query config.
+     * This kind of hints would never be propagated.
+     */
+    SET_VAR(RelNode.class),
+
+    /**
+     * The hint would be propagated to the Join nodes.
+     */
+    JOIN(Join.class),
+
+    /**
+     * The hint would be propagated to the TableScan nodes.
+     */
+    TABLE_SCAN(TableScan.class),
+
+    /**
+     * The hint would be propagated to the Project nodes.
+     */
+    PROJECT(Project.class);
+
+    /** Relational expression clazz that the hint can apply to. */
+    private Class<?> relClazz;
+
+    NodeType(Class relClazz) {
+      this.relClazz = relClazz;
+    }
+  }
+
+  private NodeType nodeType;
+
+  public NodeTypeHintStrategy(NodeType nodeType) {
+    this.nodeType = nodeType;
+  }
+
+  @Override public boolean supportsRel(RelHint hint, RelNode rel) {
+    switch (this.nodeType) {
+    // The SET_VAR type never propagate any hints.
+    case SET_VAR:
+      return false;
+    default:
+      return this.nodeType.relClazz.isInstance(rel);
+    }
+  }
+}
+
+// End NodeTypeHintStrategy.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java b/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java
new file mode 100644
index 0000000..62ea6a2
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/RelHint.java
@@ -0,0 +1,112 @@
+/*
+ * 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.rel.hint;
+
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a hint within a relation expression.
+ */
+public class RelHint {
+  //~ Instance fields --------------------------------------------------------
+
+  public final ImmutableBitSet inheritPath;
+  public final String hintName;
+  public final List<String> listOptions;
+  public final Map<String, String> kvOptions;
+
+  //~ Constructors -----------------------------------------------------------
+
+  /**
+   * Creates a {@code RelHint}.
+   *
+   * @param inheritPath Hint inherit path
+   * @param hintName    Hint name
+   * @param listOption  Hint options as string list
+   * @param kvOptions   Hint options as string key value pair
+   */
+  public RelHint(
+      Iterable<Integer> inheritPath,
+      String hintName,
+      @Nullable List<String> listOption,
+      @Nullable Map<String, String> kvOptions) {
+    Objects.requireNonNull(inheritPath);
+    Objects.requireNonNull(hintName);
+    this.inheritPath = ImmutableBitSet.of(inheritPath);
+    this.hintName = hintName;
+    this.listOptions = listOption == null ? ImmutableList.of() : ImmutableList.copyOf(listOption);
+    this.kvOptions = kvOptions == null ? ImmutableMap.of() : ImmutableMap.copyOf(kvOptions);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  /**
+   * Represents a copy of this hint that has a specified inherit path.
+   *
+   * @param inheritPath Hint path
+   * @return the new {@code RelHint}
+   */
+  public RelHint copy(List<Integer> inheritPath) {
+    Objects.requireNonNull(inheritPath);
+    return new RelHint(inheritPath, hintName, listOptions, kvOptions);
+  }
+
+  @Override public boolean equals(Object obj) {
+    if (!(obj instanceof RelHint)) {
+      return false;
+    }
+    final RelHint that = (RelHint) obj;
+    return this.hintName.equals(that.hintName)
+        && this.inheritPath.equals(that.inheritPath)
+        && Objects.equals(this.listOptions, that.listOptions)
+        && Objects.equals(this.kvOptions, that.kvOptions);
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(this.hintName, this.inheritPath,
+        this.listOptions, this.kvOptions);
+  }
+
+  @Override public String toString() {
+    StringBuilder builder = new StringBuilder();
+    builder.append("[")
+        .append(this.hintName)
+        .append(" inheritPath:")
+        .append(this.inheritPath);
+    if (this.listOptions.size() > 0 || this.kvOptions.size() > 0) {
+      builder.append(" options:");
+      if (this.listOptions.size() > 0) {
+        builder.append(this.listOptions.toString());
+      } else {
+        builder.append(this.kvOptions.toString());
+      }
+    }
+    builder.append("]");
+    return builder.toString();
+  }
+}
+
+// End RelHint.java
diff --git a/core/src/main/java/org/apache/calcite/rel/hint/package-info.java b/core/src/main/java/org/apache/calcite/rel/hint/package-info.java
new file mode 100644
index 0000000..e00c6d6
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/hint/package-info.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+/**
+ * Defines hints interfaces and utilities for relational expressions.
+ *
+ * <h2>The Syntax</h2>
+ * We support the Oracle style hint grammar for both query hint(right after the "SELECT" keyword)
+ * and the table hint(right after the table name reference). i.e.
+ *
+ * <pre>
+ *   select &#47;&#42;&#43; NO_HASH_JOIN, RESOURCE(mem='128mb', parallelism='24') &#42;&#47;
+ *   from
+ *     emp &#47;&#42;&#43; INDEX(idx1, idx2) &#42;&#47;
+ *     join
+ *     dept &#47;&#42;&#43; PROPERTIES(k1='v1', k2='v2') &#42;&#47;
+ *     on emp.deptno=dept.deptno
+ * </pre>
+ *
+ * <h2>Customize Hint Matching Rules</h2>
+ * Calcite implements a framework to define and propagate the hints. In order to make the hints
+ *  propagate efficiently, every hint referenced in the sql statement needs to
+ *  register the propagation rules.
+ *
+ * <p>We support two kinds of matching rules for rule registration:
+ *
+ * <ol>
+ *   <li>One is {@link org.apache.calcite.rel.hint.NodeTypeHintStrategy}, this rule matches a
+ *   relational expression by the relational node type.</li>
+ *   <li>Another is {@link org.apache.calcite.rel.hint.ExplicitHintStrategy},
+ *   this rule's matching logic can be totally customized.</li>
+ * </ol>
+ *
+ * <p>Here is the code snippet to illustrate how to config the strategies:
+ *
+ * <pre>
+ *       // Initialize a HintStrategyTable.
+ *       HintStrategyTable strategies = HintStrategyTable.builder()
+ *         .addHintStrategy("time_zone", HintStrategies.SET_VAR)
+ *         .addHintStrategy("index", HintStrategies.TABLE_SCAN)
+ *         .addHintStrategy("resource", HintStrategies.PROJECT)
+ *         .addHintStrategy("use_hash_join",
+ *             HintStrategies.cascade(HintStrategies.JOIN,
+ *                 HintStrategies.explicit((hint, rel) -&gt; {
+ *                   ...
+ *                 })))
+ *         .build();
+ *      // Config the strategies in the config.
+ *      SqlToRelConverter.Config config = SqlToRelConverter.configBuilder()
+ *          .withHintStrategyTable(strategies)
+ *          .build();
+ *      // Use the config to initialize the SqlToRelConverter.
+ *   ...
+ * </pre>
+ *
+ * <h2>Hints Propagation</h2>
+ * There are two cases we need to consider the hints propagation:
+ *
+ * <ul>
+ *   <li>Right after a {@code SqlNode} was converted to {@code RelNode}, we would
+ *   propagate the hints from the attaching to its input(children) nodes. The hints was
+ *   propagated recursively with a {@code RelShuttle}, see
+ *   SqlToRelConverter#RelHintPropagateShuttle for how it works.</li>
+ *   <li>During rule planning, in the transforming phrase of a {@code RelOptRule},
+ *   you should copy the hints by hand if it is needed;
+ *   We make some effort to make the thing easier: right before the new relational expression
+ *   was registered into the planner, the hints of the old relational expression was
+ *   copied into the new expression if both of them all implement
+ *   {@link org.apache.calcite.rel.hint.Hintable}.</li>
+ * </ul>
+ *
+ * <h2>Caution</h2>
+ * We do not support hints propagation with decorrelation(the hints would all be dropped).
+ *
+ * <h2>Design Doc</h2>
+ * <a href="https://docs.google.com/document/d/1mykz-w2t1Yw7CH6NjUWpWqCAf_6YNKxSc59gXafrNCs/edit?usp=sharing">Calcite SQL and Planner Hints Design</a>.
+ */
+@PackageMarker
+package org.apache.calcite.rel.hint;
+
+import org.apache.calcite.avatica.util.PackageMarker;
+
+// End package-info.java
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 696d0b6..6f6f7c7 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
@@ -26,12 +26,14 @@ import org.apache.calcite.rel.RelWriter;
 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.hint.RelHint;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rex.RexNode;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -71,6 +73,7 @@ public final class LogicalJoin extends Join {
    *
    * @param cluster          Cluster
    * @param traitSet         Trait set
+   * @param hints            Hints
    * @param left             Left input
    * @param right            Right input
    * @param condition        Join condition
@@ -88,6 +91,7 @@ public final class LogicalJoin extends Join {
   public LogicalJoin(
       RelOptCluster cluster,
       RelTraitSet traitSet,
+      List<RelHint> hints,
       RelNode left,
       RelNode right,
       RexNode condition,
@@ -95,12 +99,21 @@ public final class LogicalJoin extends Join {
       JoinRelType joinType,
       boolean semiJoinDone,
       ImmutableList<RelDataTypeField> systemFieldList) {
-    super(cluster, traitSet, left, right, condition, variablesSet, joinType);
+    super(cluster, traitSet, hints, left, right, condition, variablesSet, joinType);
     this.semiJoinDone = semiJoinDone;
     this.systemFieldList = Objects.requireNonNull(systemFieldList);
   }
 
   @Deprecated // to be removed before 2.0
+  public LogicalJoin(RelOptCluster cluster, RelTraitSet traitSet,
+      RelNode left, RelNode right, RexNode condition, Set<CorrelationId> variablesSet,
+      JoinRelType joinType, boolean semiJoinDone,
+      ImmutableList<RelDataTypeField> systemFieldList) {
+    this(cluster, traitSet, new ArrayList<>(), left, right, condition,
+        variablesSet, joinType, semiJoinDone, systemFieldList);
+  }
+
+  @Deprecated // to be removed before 2.0
   public LogicalJoin(RelOptCluster cluster, RelTraitSet traitSet, RelNode left,
       RelNode right, RexNode condition, JoinRelType joinType,
       Set<String> variablesStopped, boolean semiJoinDone,
@@ -132,6 +145,7 @@ public final class LogicalJoin extends Join {
    */
   public LogicalJoin(RelInput input) {
     this(input.getCluster(), input.getCluster().traitSetOf(Convention.NONE),
+        new ArrayList<>(),
         input.getInputs().get(0), input.getInputs().get(1),
         input.getExpression("condition"), ImmutableSet.of(),
         input.getEnum("joinType", JoinRelType.class), false,
@@ -145,7 +159,7 @@ public final class LogicalJoin extends Join {
       boolean semiJoinDone, ImmutableList<RelDataTypeField> systemFieldList) {
     final RelOptCluster cluster = left.getCluster();
     final RelTraitSet traitSet = cluster.traitSetOf(Convention.NONE);
-    return new LogicalJoin(cluster, traitSet, left, right, condition,
+    return new LogicalJoin(cluster, traitSet, ImmutableList.of(), left, right, condition,
         variablesSet, joinType, semiJoinDone, systemFieldList);
   }
 
@@ -177,7 +191,7 @@ public final class LogicalJoin extends Join {
       RelNode left, RelNode right, JoinRelType joinType, boolean semiJoinDone) {
     assert traitSet.containsIfApplicable(Convention.NONE);
     return new LogicalJoin(getCluster(),
-        getCluster().traitSetOf(Convention.NONE), left, right, conditionExpr,
+        getCluster().traitSetOf(Convention.NONE), hints, left, right, conditionExpr,
         variablesSet, joinType, semiJoinDone, systemFieldList);
   }
 
@@ -199,6 +213,11 @@ public final class LogicalJoin extends Join {
   public List<RelDataTypeField> getSystemFieldList() {
     return systemFieldList;
   }
+
+  @Override public RelNode attachHints(List<RelHint> hintList) {
+    return new LogicalJoin(getCluster(), traitSet, mergeHints(hintList),
+        left, right, condition, variablesSet, joinType, semiJoinDone, systemFieldList);
+  }
 }
 
 // End LogicalJoin.java
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 c5bbd4e..d7d129e 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
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelShuttle;
 import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.metadata.RelMdCollation;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
@@ -33,6 +34,7 @@ import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.util.Util;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -49,6 +51,7 @@ public final class LogicalProject extends Project {
    *
    * @param cluster  Cluster this relational expression belongs to
    * @param traitSet Traits of this relational expression
+   * @param hints    Hints of this relational expression
    * @param input    Input relational expression
    * @param projects List of expressions for the input columns
    * @param rowType  Output row type
@@ -56,13 +59,30 @@ public final class LogicalProject extends Project {
   public LogicalProject(
       RelOptCluster cluster,
       RelTraitSet traitSet,
+      List<RelHint> hints,
       RelNode input,
       List<? extends RexNode> projects,
       RelDataType rowType) {
-    super(cluster, traitSet, input, projects, rowType);
+    super(cluster, traitSet, hints, input, projects, rowType);
     assert traitSet.containsIfApplicable(Convention.NONE);
   }
 
+  /**
+   * Creates a LogicalProject.
+   *
+   * <p>Use {@link #create} unless you know what you're doing.
+   *
+   * @param cluster  Cluster this relational expression belongs to
+   * @param traitSet Traits of this relational expression
+   * @param input    Input relational expression
+   * @param projects List of expressions for the input columns
+   * @param rowType  Output row type
+   */
+  public LogicalProject(RelOptCluster cluster, RelTraitSet traitSet,
+      RelNode input, List<? extends RexNode> projects, RelDataType rowType) {
+    this(cluster, traitSet, new ArrayList<>(), input, projects, rowType);
+  }
+
   @Deprecated // to be removed before 2.0
   public LogicalProject(RelOptCluster cluster, RelTraitSet traitSet,
       RelNode input, List<? extends RexNode> projects, RelDataType rowType,
@@ -114,12 +134,17 @@ public final class LogicalProject extends Project {
 
   @Override public LogicalProject copy(RelTraitSet traitSet, RelNode input,
       List<RexNode> projects, RelDataType rowType) {
-    return new LogicalProject(getCluster(), traitSet, input, projects, rowType);
+    return new LogicalProject(getCluster(), traitSet, hints, input, projects, rowType);
   }
 
   @Override public RelNode accept(RelShuttle shuttle) {
     return shuttle.visit(this);
   }
+
+  @Override public RelNode attachHints(List<RelHint> hintList) {
+    return new LogicalProject(getCluster(), traitSet, mergeHints(hintList),
+        input, getProjects(), rowType);
+  }
 }
 
 // End LogicalProject.java
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
index 6b6b74a..958f171 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableScan.java
@@ -24,10 +24,12 @@ import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelInput;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.schema.Table;
 
 import com.google.common.collect.ImmutableList;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -65,8 +67,18 @@ public final class LogicalTableScan extends TableScan {
    * <p>Use {@link #create} unless you know what you're doing.
    */
   public LogicalTableScan(RelOptCluster cluster, RelTraitSet traitSet,
+      List<RelHint> hints, RelOptTable table) {
+    super(cluster, traitSet, hints, table);
+  }
+
+  /**
+   * Creates a LogicalTableScan.
+   *
+   * <p>Use {@link #create} unless you know what you're doing.
+   */
+  public LogicalTableScan(RelOptCluster cluster, RelTraitSet traitSet,
       RelOptTable table) {
-    super(cluster, traitSet, table);
+    this(cluster, traitSet, new ArrayList<>(), table);
   }
 
   @Deprecated // to be removed before 2.0
@@ -89,11 +101,12 @@ public final class LogicalTableScan extends TableScan {
 
   /** Creates a LogicalTableScan.
    *
-   * @param cluster Cluster
+   * @param cluster     Cluster
    * @param relOptTable Table
+   * @param hints       The hints
    */
   public static LogicalTableScan create(RelOptCluster cluster,
-      final RelOptTable relOptTable) {
+      final RelOptTable relOptTable, List<RelHint> hints) {
     final Table table = relOptTable.unwrap(Table.class);
     final RelTraitSet traitSet =
         cluster.traitSetOf(Convention.NONE)
@@ -103,7 +116,21 @@ public final class LogicalTableScan extends TableScan {
               }
               return ImmutableList.of();
             });
-    return new LogicalTableScan(cluster, traitSet, relOptTable);
+    return new LogicalTableScan(cluster, traitSet, hints, relOptTable);
+  }
+
+  /** Creates a LogicalTableScan.
+   *
+   * @param cluster Cluster
+   * @param relOptTable Table
+   */
+  public static LogicalTableScan create(RelOptCluster cluster,
+      final RelOptTable relOptTable) {
+    return create(cluster, relOptTable, new ArrayList<>());
+  }
+
+  @Override public RelNode attachHints(List<RelHint> hintList) {
+    return new LogicalTableScan(getCluster(), traitSet, mergeHints(hintList), table);
   }
 }
 
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index 074471d..cf5cbc2 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -475,7 +475,7 @@ public class RelToSqlConverter extends SqlImplementor
             new SqlSelect(POS, null,
                 new SqlNodeList(values2, POS),
                 getDual(), null, null,
-                null, null, null, null, null));
+                null, null, null, null, null, null));
       }
       if (list.isEmpty()) {
         // In this case we need to construct the following query:
@@ -490,19 +490,19 @@ public class RelToSqlConverter extends SqlImplementor
         if (dual == null) {
           query = new SqlSelect(POS, null,
               new SqlNodeList(nullColumnNames, POS), null, null, null, null,
-              null, null, null, null);
+              null, null, null, null, null);
 
           // Wrap "SELECT 1 AS x"
           // as "SELECT * FROM (SELECT 1 AS x) AS t WHERE false"
           query = new SqlSelect(POS, null,
               new SqlNodeList(ImmutableList.of(SqlIdentifier.star(POS)), POS),
               as(query, "t"), createAlwaysFalseCondition(), null, null,
-              null, null, null, null);
+              null, null, null, null, null);
         } else {
           query = new SqlSelect(POS, null,
               new SqlNodeList(nullColumnNames, POS),
               dual, createAlwaysFalseCondition(), null,
-              null, null, null, null, null);
+              null, null, null, null, null, null);
         }
       } else if (list.size() == 1) {
         query = list.get(0);
@@ -542,7 +542,7 @@ public class RelToSqlConverter extends SqlImplementor
                 null, query,
                 createAlwaysFalseCondition(),
                 null, null, null,
-                null, null, null);
+                null, null, null, null);
       }
     }
     return result(query, clauses, e, null);
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 7198e49..a2f273d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -433,7 +433,7 @@ public abstract class SqlImplementor {
       node = as(node, "t");
     }
     return new SqlSelect(POS, SqlNodeList.EMPTY, null, node, null, null, null,
-        SqlNodeList.EMPTY, null, null, null);
+        SqlNodeList.EMPTY, null, null, null, null);
   }
 
   /** Returns whether we need to add an alias if this node is to be the FROM
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlHint.java b/core/src/main/java/org/apache/calcite/sql/SqlHint.java
new file mode 100644
index 0000000..8716ed4
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlHint.java
@@ -0,0 +1,163 @@
+/*
+ * 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.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A <code>SqlHint</code> is a node of a parse tree which represents
+ * a sql hint expression.
+ *
+ * <p>Basic hint grammar is: hint_name[(option1, option2 ...)].
+ * The hint_name should be a simple identifier, the options part is optional.
+ * For every option, it can be a simple identifier or a key value pair whose key
+ * is a simple identifier and value is a string literal. The identifier option and key
+ * value pair can not be mixed in, they should be either all simple identifiers
+ * or all key value pairs.
+ *
+ * <p>We support 2 kinds of hints in the parser:
+ * <ul>
+ *   <li>Top mode hint, right after the select keyword, i.e.:
+ *   <pre>
+ *     select &#47;&#42;&#43; hint1, hint2, ... &#42;&#47; ...
+ *   </pre>
+ *   </li>
+ *   <li>Table hint: right after the referenced table name, i.e.:
+ *   <pre>
+ *     select f0, f1, f2 from t1 &#47;&#42;&#43; hint1, hint2, ... &#42;&#47; ...
+ *   </pre>
+ *   </li>
+ * </ul>
+ */
+public class SqlHint extends SqlCall {
+  //~ Instance fields --------------------------------------------------------
+
+  private final SqlIdentifier name;
+  private final SqlNodeList options;
+  private final List<String> optionList;
+  private final Map<String, String> optionKVPairs;
+
+  private static final SqlOperator OPERATOR =
+      new SqlSpecialOperator("HINT", SqlKind.HINT);
+
+  //~ Constructors -----------------------------------------------------------
+
+  public SqlHint(SqlParserPos pos, SqlIdentifier name, SqlNodeList options) {
+    super(pos);
+    this.name = name;
+    this.options = options;
+    this.optionList = getOptionAsList();
+    this.optionKVPairs = getOptionAsKVPairs();
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  public SqlOperator getOperator() {
+    return OPERATOR;
+  }
+
+  public List<SqlNode> getOperandList() {
+    return ImmutableList.of(name, options);
+  }
+
+  /**
+   * @return The sql hint name
+   */
+  public String getName() {
+    return name.getSimple();
+  }
+
+  public List<String> getOptionList() {
+    return optionList;
+  }
+
+  public Map<String, String> getOptionKVPairs() {
+    return optionKVPairs;
+  }
+
+  /**
+   * @return A string list if the hint option is a list of
+   * simple SQL identifier, else an empty list
+   */
+  private List<String> getOptionAsList() {
+    if (this.options.size() == 0 || isOptionsAsKVPairs()) {
+      return ImmutableList.of();
+    } else {
+      final List<String> attrs = this.options.getList().stream()
+          .map(node -> ((SqlIdentifier) node).getSimple())
+          .collect(Collectors.toList());
+      return ImmutableList.copyOf(attrs);
+    }
+  }
+
+  /**
+   * @return A key value string map if the hint option is a list of
+   * pair that contains a simple SQL identifier and a string literal,
+   * else an empty map
+   */
+  private Map<String, String> getOptionAsKVPairs() {
+    if (this.options.size() == 0 || !isOptionsAsKVPairs()) {
+      return ImmutableMap.of();
+    } else {
+      final Map<String, String> attrs = new HashMap<>();
+      for (int i = 0; i < this.options.size() - 1; i += 2) {
+        final SqlNode k = this.options.get(i);
+        final SqlNode v = this.options.get(i + 1);
+        assert k instanceof SqlIdentifier;
+        assert v instanceof SqlLiteral;
+        attrs.put(((SqlIdentifier) k).getSimple(),
+            ((SqlLiteral) v).getValueAs(String.class));
+      }
+      return ImmutableMap.copyOf(attrs);
+    }
+  }
+
+  private boolean isOptionsAsKVPairs() {
+    return this.options.getList()
+        .stream()
+        .anyMatch(node -> node instanceof SqlLiteral);
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+    name.unparse(writer, leftPrec, rightPrec);
+    if (this.options.size() > 0) {
+      SqlWriter.Frame frame = writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+      for (int i = 0; i < options.size(); i++) {
+        SqlNode option = options.get(i);
+        SqlNode nextOption = i < options.size() - 1 ? options.get(i + 1) : null;
+        writer.sep(",", false);
+        option.unparse(writer, leftPrec, rightPrec);
+        if (nextOption instanceof SqlLiteral) {
+          writer.print("=");
+          nextOption.unparse(writer, leftPrec, rightPrec);
+          i += 1;
+        }
+      }
+      writer.endList(frame);
+    }
+  }
+}
+
+// End SqlHint.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index 1a27ada..5ae3cab 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -92,6 +92,16 @@ public enum SqlKind {
   SELECT,
 
   /**
+   * Sql Hint statement.
+   */
+  HINT,
+
+  /**
+   * Table reference.
+   */
+  TABLE_REF,
+
+  /**
    * JOIN operator or compound FROM clause.
    *
    * <p>A FROM clause with more than one table is represented as if it were a
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
index ef02914..e3537b4 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlProcedureCallOperator.java
@@ -56,6 +56,7 @@ public class SqlProcedureCallOperator extends SqlPrefixOperator {
         null,
         null,
         null,
+        null,
         null);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
index f98ec2e..3f9dbc6 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
@@ -48,6 +48,7 @@ public class SqlSelect extends SqlCall {
   SqlNodeList orderBy;
   SqlNode offset;
   SqlNode fetch;
+  SqlNodeList hints;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -61,7 +62,8 @@ public class SqlSelect extends SqlCall {
       SqlNodeList windowDecls,
       SqlNodeList orderBy,
       SqlNode offset,
-      SqlNode fetch) {
+      SqlNode fetch,
+      SqlNodeList hints) {
     super(pos);
     this.keywordList = Objects.requireNonNull(keywordList != null
         ? keywordList : new SqlNodeList(pos));
@@ -75,6 +77,7 @@ public class SqlSelect extends SqlCall {
     this.orderBy = orderBy;
     this.offset = offset;
     this.fetch = fetch;
+    this.hints = hints;
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -212,6 +215,19 @@ public class SqlSelect extends SqlCall {
     this.fetch = fetch;
   }
 
+  public void setHints(SqlNodeList hints) {
+    this.hints = hints;
+  }
+
+  public SqlNodeList getHints() {
+    return this.hints;
+  }
+
+  public boolean hasHints() {
+    // The hints may be passed as null explicitly.
+    return this.hints != null && this.hints.size() > 0;
+  }
+
   public void validate(SqlValidator validator, SqlValidatorScope scope) {
     validator.validateQuery(this, scope, validator.getUnknownType());
   }
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
index 80961d4..856eeec 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
@@ -71,7 +71,8 @@ public class SqlSelectOperator extends SqlOperator {
         (SqlNodeList) operands[6],
         (SqlNodeList) operands[7],
         operands[8],
-        operands[9]);
+        operands[9],
+        (SqlNodeList) operands[10]);
   }
 
   /**
@@ -104,6 +105,7 @@ public class SqlSelectOperator extends SqlOperator {
       SqlNodeList orderBy,
       SqlNode offset,
       SqlNode fetch,
+      SqlNodeList hints,
       SqlParserPos pos) {
     return new SqlSelect(
         pos,
@@ -116,7 +118,8 @@ public class SqlSelectOperator extends SqlOperator {
         windowDecls,
         orderBy,
         offset,
-        fetch);
+        fetch,
+        hints);
   }
 
   public <R> void acceptCall(
@@ -139,6 +142,14 @@ public class SqlSelectOperator extends SqlOperator {
     final SqlWriter.Frame selectFrame =
         writer.startList(SqlWriter.FrameTypeEnum.SELECT);
     writer.sep("SELECT");
+
+    if (select.hasHints()) {
+      writer.sep("/*+");
+      select.hints.unparse(writer, leftPrec, rightPrec);
+      writer.print("*/");
+      writer.newlineAndIndent();
+    }
+
     for (int i = 0; i < select.keywordList.size(); i++) {
       final SqlNode keyword = select.keywordList.get(i);
       keyword.unparse(writer, 0, 0);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java b/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java
new file mode 100644
index 0000000..87a6f6c
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTableRef.java
@@ -0,0 +1,72 @@
+/*
+ * 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.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ *  A <code>SqlTableRef</code> is a node of a parse tree which represents
+ *  a table reference.
+ *
+ *  <p>It can be attached with a sql hint statement, see {@link SqlHint} for details.
+ */
+public class SqlTableRef extends SqlCall {
+
+  //~ Instance fields --------------------------------------------------------
+
+  private final SqlIdentifier tableName;
+  private final SqlNodeList hints;
+
+  //~ Static fields/initializers ---------------------------------------------
+
+  private static final SqlOperator OPERATOR =
+      new SqlSpecialOperator("TABLE_REF", SqlKind.TABLE_REF);
+
+  //~ Constructors -----------------------------------------------------------
+
+  public SqlTableRef(SqlParserPos pos, SqlIdentifier tableName, SqlNodeList hints) {
+    super(pos);
+    this.tableName = tableName;
+    this.hints = hints;
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  public SqlOperator getOperator() {
+    return OPERATOR;
+  }
+
+  public List<SqlNode> getOperandList() {
+    return ImmutableList.of(tableName, hints);
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+    tableName.unparse(writer, leftPrec, rightPrec);
+    if (this.hints != null && this.hints.size() > 0) {
+      writer.newlineAndIndent();
+      writer.keyword("/*+");
+      this.hints.unparse(writer, 0, 0);
+      writer.keyword("*/");
+    }
+  }
+}
+
+// End SqlTableRef.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
index bbd1a6a..5740fdf 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
@@ -19,6 +19,10 @@ package org.apache.calcite.sql;
 import org.apache.calcite.avatica.util.ByteString;
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.linq4j.function.Functions;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
@@ -973,6 +977,51 @@ public abstract class SqlUtil {
     }
   }
 
+  /**
+   * Returns an immutable list of {@link RelHint} from sql hints, with a given
+   * inherit path from the root node.
+   *
+   * <p>The inherit path would be empty [].
+   *
+   * @param hintStrategies The hint strategies to validate if the sql hints
+   * @param sqlHints       The sql hints nodes
+   * @return the {@code RelHint} list
+   */
+  public static List<RelHint> getRelHint(HintStrategyTable hintStrategies, SqlNodeList sqlHints) {
+    final List<RelHint> relHints = new ArrayList<>();
+    if (sqlHints == null || sqlHints.size() == 0) {
+      return relHints;
+    }
+    for (SqlNode node : sqlHints) {
+      assert node instanceof SqlHint;
+      final SqlHint sqlHint = (SqlHint) node;
+      hintStrategies.validateHint(sqlHint.getName());
+      final RelHint relHint = new RelHint(new ArrayList<>(),
+          sqlHint.getName(), sqlHint.getOptionList(), sqlHint.getOptionKVPairs());
+      relHints.add(relHint);
+    }
+    return ImmutableList.copyOf(relHints);
+  }
+
+  /**
+   * Attach the {@code hints} to {@code rel} with specified hint strategies.
+   *
+   * @param hintStrategies The strategies to filter the hints
+   * @param hints          The original hints to be attached
+   * @return A copy of {@code rel} if there are any hints can be attached given
+   * the hint strategies, or the original node if such hints don't exist
+   */
+  public static RelNode attachRelHint(
+      HintStrategyTable hintStrategies,
+      List<RelHint> hints,
+      Hintable rel) {
+    final List<RelHint> relHints = hintStrategies.apply(hints, (RelNode) rel);
+    if (relHints.size() > 0) {
+      return rel.attachHints(relHints);
+    }
+    return (RelNode) rel;
+  }
+
   //~ Inner Classes ----------------------------------------------------------
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
index 52df66c..7c2654c 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
@@ -143,7 +143,8 @@ public class MysqlSqlDialect extends SqlDialect {
     final SqlNode operand = ((SqlBasicCall) aggCall).operand(0);
     final SqlLiteral nullLiteral = SqlLiteral.createNull(SqlParserPos.ZERO);
     final SqlNode unionOperand = new SqlSelect(SqlParserPos.ZERO, SqlNodeList.EMPTY,
-        SqlNodeList.of(nullLiteral), null, null, null, null, SqlNodeList.EMPTY, null, null, null);
+        SqlNodeList.of(nullLiteral), null, null, null, null,
+        SqlNodeList.EMPTY, null, null, null, SqlNodeList.EMPTY);
     // For MySQL, generate
     //   CASE COUNT(*)
     //   WHEN 0 THEN NULL
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
index 8546742..060482c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/IdentifierNamespace.java
@@ -20,6 +20,7 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.parser.SqlParserPos;
@@ -89,8 +90,14 @@ public class IdentifierNamespace extends AbstractNamespace {
     switch (node.getKind()) {
     case EXTEND:
       final SqlCall call = (SqlCall) node;
-      return Pair.of((SqlIdentifier) call.getOperandList().get(0),
-          (SqlNodeList) call.getOperandList().get(1));
+      final SqlNode operand0 = call.operand(0);
+      final SqlIdentifier identifier = operand0.getKind() == SqlKind.TABLE_REF
+          ? ((SqlCall) operand0).operand(0)
+          : (SqlIdentifier) operand0;
+      return Pair.of(identifier, call.operand(1));
+    case TABLE_REF:
+      final SqlCall tableRef = (SqlCall) node;
+      return Pair.of(tableRef.operand(0), null);
     default:
       return Pair.of((SqlIdentifier) node, null);
     }
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 9478384..8855ea4 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -1077,13 +1077,19 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       // Handle extended identifiers.
       final SqlCall call = (SqlCall) node;
       switch (call.getOperator().getKind()) {
+      case TABLE_REF:
+        return getNamespace(call.operand(0), scope);
       case EXTEND:
-        final SqlIdentifier id = (SqlIdentifier) call.getOperandList().get(0);
+        final SqlNode operand0 = call.getOperandList().get(0);
+        final SqlIdentifier identifier = operand0.getKind() == SqlKind.TABLE_REF
+            ? ((SqlCall) operand0).operand(0)
+            : (SqlIdentifier) operand0;
         final DelegatingScope idScope = (DelegatingScope) scope;
-        return getNamespace(id, idScope);
+        return getNamespace(identifier, idScope);
       case AS:
         final SqlNode nested = call.getOperandList().get(0);
         switch (nested.getKind()) {
+        case TABLE_REF:
         case EXTEND:
           return getNamespace(nested, scope);
         }
@@ -1116,6 +1122,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
         return ns;
       }
       // fall through
+    case TABLE_REF:
     case SNAPSHOT:
     case OVER:
     case COLLECTION_TABLE:
@@ -1231,7 +1238,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
             new SqlNodeList(SqlParserPos.ZERO);
         selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
         return new SqlSelect(node.getParserPosition(), null, selectList, node,
-            null, null, null, null, null, null, null);
+            null, null, null, null, null, null, null, null);
       }
 
     case ORDER_BY: {
@@ -1288,7 +1295,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       }
       return new SqlSelect(SqlParserPos.ZERO, null, selectList, orderBy.query,
           null, null, null, null, orderList, orderBy.offset,
-          orderBy.fetch);
+          orderBy.fetch, null);
     }
 
     case EXPLICIT_TABLE: {
@@ -1297,7 +1304,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
       selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
       return new SqlSelect(SqlParserPos.ZERO, null, selectList, call.operand(0),
-          null, null, null, null, null, null, null);
+          null, null, null, null, null, null, null, null);
     }
 
     case DELETE: {
@@ -1391,7 +1398,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
             call.getCondition());
     SqlSelect select =
         new SqlSelect(SqlParserPos.ZERO, null, selectList, outerJoin, null,
-            null, null, null, null, null, null);
+            null, null, null, null, null, null, null);
     call.setSourceSelect(select);
 
     // Source for the insert call is a select of the source table
@@ -1409,7 +1416,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       final SqlNode insertSource = SqlNode.clone(sourceTableRef);
       select =
           new SqlSelect(SqlParserPos.ZERO, null, selectList, insertSource, null,
-              null, null, null, null, null, null);
+              null, null, null, null, null, null, null);
       insertCall.setSource(select);
     }
   }
@@ -1474,7 +1481,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     }
     source =
         new SqlSelect(SqlParserPos.ZERO, null, selectList, source, null, null,
-            null, null, null, null, null);
+            null, null, null, null, null, null);
     source = SqlValidatorUtil.addAlias(source, UPDATE_SRC_ALIAS);
     SqlMerge mergeCall =
         new SqlMerge(updateCall.getParserPosition(), target, condition, source,
@@ -1529,7 +1536,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
               call.getAlias().getSimple());
     }
     return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
-        call.getCondition(), null, null, null, null, null, null);
+        call.getCondition(), null, null, null, null, null, null, null);
   }
 
   /**
@@ -1550,7 +1557,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
               call.getAlias().getSimple());
     }
     return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
-        call.getCondition(), null, null, null, null, null, null);
+        call.getCondition(), null, null, null, null, null, null, null);
   }
 
   /**
@@ -2319,6 +2326,22 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
 
       return newNode;
 
+    case TABLE_REF:
+      call = (SqlCall) node;
+      registerFrom(parentScope,
+          usingScope,
+          register,
+          call.operand(0),
+          enclosingNode,
+          alias,
+          extendList,
+          forceNullable,
+          lateral);
+      if (extendList != null && extendList.size() != 0) {
+        return enclosingNode;
+      }
+      return newNode;
+
     case EXTEND:
       final SqlCall extend = (SqlCall) node;
       return registerFrom(parentScope,
@@ -3108,6 +3131,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     Objects.requireNonNull(targetRowType);
     switch (node.getKind()) {
     case AS:
+    case TABLE_REF:
       validateFrom(
           ((SqlCall) node).operand(0),
           targetRowType,
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
index fa96e3e..57e01a6 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
@@ -1150,7 +1150,7 @@ public class SqlValidatorUtil {
     final SqlSelect select0 = new SqlSelect(SqlParserPos.ZERO, null,
         new SqlNodeList(Collections.singletonList(expr), SqlParserPos.ZERO),
         new SqlIdentifier(tableName, SqlParserPos.ZERO),
-        null, null, null, null, null, null, null);
+        null, null, null, null, null, null, null, null);
     Prepare.CatalogReader catalogReader = createSingleTableCatalogReader(
         caseSensitive,
         tableName,
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
index 521edfa..8b9f499 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java
@@ -19,6 +19,7 @@ package org.apache.calcite.sql2rel;
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelFieldCollation;
@@ -437,6 +438,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
             getNewForOldRel(rel.getRight()),
             rel.getCondition().accept(new RewriteRexShuttle()),
             rel.getVariablesSet(), rel.getJoinType());
+    newRel = (LogicalJoin) RelOptUtil.copyRelHints(rel, newRel);
     setNewForOldRel(rel, newRel);
   }
 
@@ -508,6 +510,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
     RelNode newRel = relBuilder.push(newInput)
         .projectNamed(newProjects, newNames, true)
         .build();
+    newRel = RelOptUtil.copyRelHints(rel, newRel);
     setNewForOldRel(rel, newRel);
   }
 
@@ -729,6 +732,7 @@ public class RelStructuredTypeFlattener implements ReflectiveVisitor {
     if (!SqlTypeUtil.isFlat(rel.getRowType())) {
       newRel = coverNewRelByFlatteningProjection(rel, newRel);
     }
+    newRel = RelOptUtil.copyRelHints(rel, newRel);
     setNewForOldRel(rel, newRel);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 186b3f7..97a94ce 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -34,6 +34,7 @@ import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.RelShuttleImpl;
 import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Collect;
@@ -49,6 +50,9 @@ import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.TableScan;
 import org.apache.calcite.rel.core.Uncollect;
 import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.hint.RelHint;
 import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.rel.logical.LogicalCorrelate;
 import org.apache.calcite.rel.logical.LogicalFilter;
@@ -187,9 +191,11 @@ import java.math.BigDecimal;
 import java.util.AbstractList;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Deque;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -241,6 +247,7 @@ public class SqlToRelConverter {
   private final SqlOperatorTable opTab;
   protected final RelDataTypeFactory typeFactory;
   private final SqlNodeToRexConverter exprConverter;
+  private final HintStrategyTable hintStrategies;
   private int explainParamCount;
   public final SqlToRelConverter.Config config;
   private final RelBuilder relBuilder;
@@ -320,11 +327,13 @@ public class SqlToRelConverter {
     this.subQueryConverter = new NoOpSubQueryConverter();
     this.rexBuilder = cluster.getRexBuilder();
     this.typeFactory = rexBuilder.getTypeFactory();
-    this.cluster = Objects.requireNonNull(cluster);
     this.exprConverter = new SqlNodeToRexConverterImpl(convertletTable);
     this.explainParamCount = 0;
     this.config = new ConfigBuilder().withConfig(config).build();
     this.relBuilder = config.getRelBuilderFactory().create(cluster, null);
+    this.hintStrategies = config.getHintStrategyTable();
+    this.cluster = Objects.requireNonNull(cluster)
+        .withHintStrategies(this.hintStrategies);
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -581,8 +590,18 @@ public class SqlToRelConverter {
     }
 
     final RelDataType validatedRowType = validator.getValidatedNodeType(query);
+    List<RelHint> hints = new ArrayList<>();
+    if (query.getKind() == SqlKind.SELECT) {
+      final SqlSelect select = (SqlSelect) query;
+      if (select.hasHints()) {
+        hints = SqlUtil.getRelHint(hintStrategies, select.getHints());
+      }
+    }
+    // propagate the hints.
+    result = result.accept(new RelHintPropagateShuttle(this.hintStrategies));
     return RelRoot.of(result, validatedRowType, query.getKind())
-        .withCollation(collation);
+        .withCollation(collation)
+        .withHints(hints);
   }
 
   private static boolean isStream(SqlNode query) {
@@ -675,10 +694,30 @@ public class SqlToRelConverter {
     if (select.isDistinct()) {
       distinctify(bb, true);
     }
+
     convertOrder(
         select, bb, collation, orderExprList, select.getOffset(),
         select.getFetch());
-    bb.setRoot(bb.root, true);
+
+    if (select.hasHints()) {
+      final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies, select.getHints());
+      // Attach the hints to the first project we found from the root node.
+      bb.setRoot(bb.root
+          .accept(
+              new RelShuttleImpl() {
+                boolean attached = false;
+                @Override public RelNode visit(LogicalProject project) {
+                  if (!attached) {
+                    attached = true;
+                    return project.attachHints(hints);
+                  } else {
+                    return project;
+                  }
+                }
+              }), true);
+    } else {
+      bb.setRoot(bb.root, true);
+    }
   }
 
   /**
@@ -2046,15 +2085,23 @@ public class SqlToRelConverter {
       }
       return;
 
+    case TABLE_REF:
+      call = (SqlCall) from;
+      convertIdentifier(bb, call.operand(0), null, call.operand(1));
+      return;
+
     case IDENTIFIER:
-      convertIdentifier(bb, (SqlIdentifier) from, null);
+      convertIdentifier(bb, (SqlIdentifier) from, null, null);
       return;
 
     case EXTEND:
       call = (SqlCall) from;
-      SqlIdentifier id = (SqlIdentifier) call.getOperandList().get(0);
+      final SqlNode operand0 = call.getOperandList().get(0);
+      final SqlIdentifier id = operand0.getKind() == SqlKind.TABLE_REF
+          ? ((SqlCall) operand0).operand(0)
+          : (SqlIdentifier) operand0;
       SqlNodeList extendedColumns = (SqlNodeList) call.getOperandList().get(1);
-      convertIdentifier(bb, id, extendedColumns);
+      convertIdentifier(bb, id, extendedColumns, null);
       return;
 
     case SNAPSHOT:
@@ -2329,7 +2376,7 @@ public class SqlToRelConverter {
   }
 
   private void convertIdentifier(Blackboard bb, SqlIdentifier id,
-      SqlNodeList extendedColumns) {
+      SqlNodeList extendedColumns, SqlNodeList tableHints) {
     final SqlValidatorNamespace fromNamespace =
         validator.getNamespace(id).resolve();
     if (fromNamespace.getNode() != null) {
@@ -2352,10 +2399,12 @@ public class SqlToRelConverter {
       table = table.extend(extendedFields);
     }
     final RelNode tableRel;
+    final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies, tableHints);
     if (config.isConvertTableAccess()) {
-      tableRel = toRel(table);
+      tableRel = toRel(table, hints);
     } else {
-      tableRel = LogicalTableScan.create(cluster, table);
+      tableRel = SqlUtil.attachRelHint(hintStrategies,
+          hints, LogicalTableScan.create(cluster, table));
     }
     bb.setRoot(tableRel, true);
     if (usedDataset[0]) {
@@ -2392,7 +2441,7 @@ public class SqlToRelConverter {
       final RelDataType rowType = table.getRowType(typeFactory);
       RelOptTable relOptTable = RelOptTableImpl.create(null, rowType, table,
           udf.getNameAsId().names);
-      RelNode converted = toRel(relOptTable);
+      RelNode converted = toRel(relOptTable, null);
       bb.setRoot(converted, true);
       return;
     }
@@ -3323,8 +3372,11 @@ public class SqlToRelConverter {
     return ViewExpanders.toRelContext(viewExpander, cluster);
   }
 
-  public RelNode toRel(final RelOptTable table) {
-    final RelNode scan = table.toRel(createToRelContext());
+  public RelNode toRel(final RelOptTable table, final List<RelHint> hints) {
+    final RelNode rel = table.toRel(createToRelContext());
+    final RelNode scan = rel instanceof Hintable && hints != null && hints.size() > 0
+        ? SqlUtil.attachRelHint(hintStrategies, hints, (Hintable) rel)
+        : rel;
 
     final InitializerExpressionFactory ief =
         Util.first(table.unwrap(InitializerExpressionFactory.class),
@@ -5588,6 +5640,117 @@ public class SqlToRelConverter {
     }
   }
 
+  /**
+   * A {@code RelShuttle} which propagates all the hints of relational expression to
+   * their children nodes.
+   *
+   * <p>Given a plan:
+   *
+   * <blockquote><pre>
+   * Filter Hint1
+   * +- Join
+   *    +- Scan
+   *    +- Project Hint2
+   *       +- Scan
+   * </pre></blockquote>
+   *
+   * <p>Every hint has a {@code inheritPath} (integers list) which records its propagate path,
+   * number `0` represents the hint is propagated from the first(left) child, number `1`
+   * represents the hint is propagated from the second(right) child, so the plan would have
+   * hints path as follows:
+   *
+   * <ol>
+   *   <li>Filter would have hints {Hint1[]}</li>
+   *   <li>Join would have hints {Hint1[0]}</li>
+   *   <li>Scan would have hints {Hint1[0, 0]}</li>
+   *   <li>Project would have hints {Hint1[0,1], Hint2}</li>
+   *   <li>Scan2 would have hints {[Hint1[0, 1, 0], Hint2[0]}</li>
+   * </ol>
+   */
+  private static class RelHintPropagateShuttle extends RelShuttleImpl {
+    /**
+     * Stack recording the hints and its current inheritPath.
+     */
+    private final Deque<Pair<List<RelHint>, Deque<Integer>>> inheritPaths =
+        new ArrayDeque<>();
+
+    /**
+     * The hint strategies to decide if a hint should be attached to
+     * a relational expression.
+     */
+    private final HintStrategyTable hintStrategies;
+
+    RelHintPropagateShuttle(HintStrategyTable hintStrategies) {
+      this.hintStrategies = hintStrategies;
+    }
+
+    /**
+     * Visits a particular child of a parent.
+     */
+    protected RelNode visitChild(RelNode parent, int i, RelNode child) {
+      inheritPaths.forEach(inheritPath -> inheritPath.right.push(i));
+      try {
+        RelNode child2 = child.accept(this);
+        if (child2 != child) {
+          final List<RelNode> newInputs = new ArrayList<>(parent.getInputs());
+          newInputs.set(i, child2);
+          return parent.copy(parent.getTraitSet(), newInputs);
+        }
+        return parent;
+      } finally {
+        inheritPaths.forEach(inheritPath -> inheritPath.right.pop());
+      }
+    }
+
+    public RelNode visit(LogicalJoin join) {
+      final LogicalJoin join1 = (LogicalJoin) super.visit(join);
+      return attachHints(join1);
+    }
+
+    public RelNode visit(LogicalProject project) {
+      final List<RelHint> topHints = project.getHints();
+      final boolean hasHints = topHints != null && topHints.size() > 0;
+      if (hasHints) {
+        inheritPaths.push(Pair.of(topHints, new ArrayDeque<>()));
+      }
+      final RelNode project1 = super.visit(project);
+      if (hasHints) {
+        inheritPaths.pop();
+      }
+      return attachHints(project1);
+    }
+
+    public RelNode visit(TableScan scan) {
+      TableScan scan1 = (TableScan) super.visit(scan);
+      return attachHints(scan1);
+    }
+
+    private RelNode attachHints(RelNode original) {
+      assert original instanceof Hintable;
+      if (inheritPaths.size() > 0) {
+        final List<RelHint> hints = inheritPaths.stream()
+            .sorted(Comparator.comparingInt(o -> o.right.size()))
+            .map(path -> copyWithInheritPath(path.left, path.right))
+            .reduce(new ArrayList<>(), (acc, hints1) -> {
+              acc.addAll(hints1);
+              return acc;
+            });
+        final List<RelHint> filteredHints = hintStrategies.apply(hints, original);
+        if (filteredHints.size() > 0) {
+          return ((Hintable) original).attachHints(filteredHints);
+        }
+      }
+      return original;
+    }
+
+    private static List<RelHint> copyWithInheritPath(List<RelHint> hints,
+        Deque<Integer> inheritPath) {
+      return hints.stream()
+          .map(hint -> hint.copy(Arrays.asList(inheritPath.toArray(new Integer[]{}))))
+          .collect(Collectors.toList());
+    }
+  }
+
   /** Creates a builder for a {@link Config}. */
   public static ConfigBuilder configBuilder() {
     return new ConfigBuilder();
@@ -5648,6 +5811,11 @@ public class SqlToRelConverter {
     /** Returns the factory to create {@link RelBuilder}, never null. Default is
      * {@link RelFactories#LOGICAL_BUILDER}. */
     RelBuilderFactory getRelBuilderFactory();
+
+    /** Returns the hint strategies used to decide how the hints are propagated to
+     * the relational expressions. Default is
+     * {@link HintStrategyTable#EMPTY}. */
+    HintStrategyTable getHintStrategyTable();
   }
 
   /** Builder for a {@link Config}. */
@@ -5660,6 +5828,7 @@ public class SqlToRelConverter {
     private boolean expand = true;
     private int inSubQueryThreshold = DEFAULT_IN_SUB_QUERY_THRESHOLD;
     private RelBuilderFactory relBuilderFactory = RelFactories.LOGICAL_BUILDER;
+    private HintStrategyTable hintStrategyTable = HintStrategyTable.EMPTY;
 
     private ConfigBuilder() {}
 
@@ -5673,6 +5842,7 @@ public class SqlToRelConverter {
       this.expand = config.isExpand();
       this.inSubQueryThreshold = config.getInSubQueryThreshold();
       this.relBuilderFactory = config.getRelBuilderFactory();
+      this.hintStrategyTable = config.getHintStrategyTable();
       return this;
     }
 
@@ -5722,11 +5892,17 @@ public class SqlToRelConverter {
       return this;
     }
 
+    public ConfigBuilder withHintStrategyTable(
+        HintStrategyTable hintStrategyTable) {
+      this.hintStrategyTable = hintStrategyTable;
+      return this;
+    }
+
     /** Builds a {@link Config}. */
     public Config build() {
       return new ConfigImpl(convertTableAccess, decorrelationEnabled,
           trimUnusedFields, createValuesRel, explain, expand,
-          inSubQueryThreshold, relBuilderFactory);
+          inSubQueryThreshold, relBuilderFactory, hintStrategyTable);
     }
   }
 
@@ -5741,11 +5917,13 @@ public class SqlToRelConverter {
     private final boolean expand;
     private final int inSubQueryThreshold;
     private final RelBuilderFactory relBuilderFactory;
+    private final HintStrategyTable hintStrategyTable;
 
     private ConfigImpl(boolean convertTableAccess, boolean decorrelationEnabled,
         boolean trimUnusedFields, boolean createValuesRel, boolean explain,
         boolean expand, int inSubQueryThreshold,
-        RelBuilderFactory relBuilderFactory) {
+        RelBuilderFactory relBuilderFactory,
+        HintStrategyTable hintStrategyTable) {
       this.convertTableAccess = convertTableAccess;
       this.decorrelationEnabled = decorrelationEnabled;
       this.trimUnusedFields = trimUnusedFields;
@@ -5754,6 +5932,7 @@ public class SqlToRelConverter {
       this.expand = expand;
       this.inSubQueryThreshold = inSubQueryThreshold;
       this.relBuilderFactory = relBuilderFactory;
+      this.hintStrategyTable = hintStrategyTable;
     }
 
     public boolean isConvertTableAccess() {
@@ -5787,6 +5966,10 @@ public class SqlToRelConverter {
     public RelBuilderFactory getRelBuilderFactory() {
       return relBuilderFactory;
     }
+
+    public HintStrategyTable getHintStrategyTable() {
+      return hintStrategyTable;
+    }
   }
 }
 
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 13f986d..2739a60 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -8577,6 +8577,122 @@ public class SqlParserTest {
     sql(sql2).ok(expected);
   }
 
+  @Test public void testQueryHint() {
+    final String sql = "select "
+        + "/*+ properties(k1='v1', k2='v2'), no_hash_join, Index(idx1, idx2) */ "
+        + "empno, ename, deptno from emps";
+    final String expected = "SELECT\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `NO_HASH_JOIN`, `INDEX`(`IDX1`, `IDX2`) */\n"
+        + "`EMPNO`, `ENAME`, `DEPTNO`\n"
+        + "FROM `EMPS`";
+    sql(sql).ok(expected);
+  }
+
+  @Test public void testTableHintsInQuery() {
+    final String hint = "/*+ PROPERTIES(K1 ='v1', K2 ='v2'), INDEX(IDX0, IDX1) */";
+    final String sql1 = String.format(Locale.ROOT, "select * from t %s", hint);
+    final String expected1 = "SELECT *\n"
+        + "FROM `T`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */";
+    sql(sql1).ok(expected1);
+    final String sql2 = String.format(Locale.ROOT, "select * from\n"
+        + "(select * from t %s union all select * from t %s )", hint, hint);
+    final String expected2 = "SELECT *\n"
+        + "FROM (SELECT *\n"
+        + "FROM `T`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+        + "UNION ALL\n"
+        + "SELECT *\n"
+        + "FROM `T`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */)";
+    sql(sql2).ok(expected2);
+    final String sql3 = String.format(Locale.ROOT, "select * from t %s join t %s", hint, hint);
+    final String expected3 = "SELECT *\n"
+        + "FROM `T`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+        + "INNER JOIN `T`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */";
+    sql(sql3).ok(expected3);
+  }
+
+  @Test public void testTableHintsInInsert() {
+    final String sql = "insert into emps\n"
+        + "/*+ PROPERTIES(k1='v1', k2='v2'), INDEX(idx0, idx1) */\n"
+        + "select * from emps";
+    final String expected = "INSERT INTO `EMPS`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
+        + "(SELECT *\n"
+        + "FROM `EMPS`)";
+    sql(sql).ok(expected);
+  }
+
+  @Test public void testTableHintsInDelete() {
+    final String sql = "delete from emps\n"
+        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
+        + "where empno=12";
+    final String expected = "DELETE FROM `EMPS`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), `INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */\n"
+        + "WHERE (`EMPNO` = 12)";
+    sql(sql).ok(expected);
+  }
+
+  @Test public void testTableHintsInUpdate() {
+    final String sql = "update emps\n"
+        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
+        + "set empno = empno + 1, sal = sal - 1 \n"
+        + "where empno=12";
+    final String expected = "UPDATE `EMPS`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), "
+        + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
+        + "SET `EMPNO` = (`EMPNO` + 1)\n"
+        + ", `SAL` = (`SAL` - 1)\n"
+        + "WHERE (`EMPNO` = 12)";
+    sql(sql).ok(expected);
+  }
+
+  @Test public void testTableHintsInMerge() {
+    final String sql = "merge into emps\n"
+        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */ e\n"
+        + "using tempemps as t\n"
+        + "on e.empno = t.empno\n"
+        + "when matched then update\n"
+        + "set name = t.name, deptno = t.deptno, salary = t.salary * .1\n"
+        + "when not matched then insert (name, dept, salary)\n"
+        + "values(t.name, 10, t.salary * .15)";
+    final String expected = "MERGE INTO `EMPS`\n"
+        + "/*+ `PROPERTIES`(`K1` ='v1', `K2` ='v2'), "
+        + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
+        + "AS `E`\n"
+        + "USING `TEMPEMPS` AS `T`\n"
+        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
+        + "WHEN MATCHED THEN UPDATE SET `NAME` = `T`.`NAME`\n"
+        + ", `DEPTNO` = `T`.`DEPTNO`\n"
+        + ", `SALARY` = (`T`.`SALARY` * 0.1)\n"
+        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
+        + "(VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15))))";
+    sql(sql).ok(expected);
+  }
+
+  @Test public void testInvalidHintFormat() {
+    final String sql1 = "select "
+        + "/*+ properties(k1^=^123, k2='v2'), no_hash_join() */ "
+        + "empno, ename, deptno from emps";
+    sql(sql1).fails("(?s).*Encountered \"= 123\" at line 1, column 25.\n.*");
+    final String sql2 = "select "
+        + "/*+ properties(k1, k2^=^'v2'), no_hash_join */ "
+        + "empno, ename, deptno from emps";
+    sql(sql2).fails("(?s).*Encountered \"=\" at line 1, column 29.\n.*");
+    final String sql3 = "select "
+        + "/*+ no_hash_join() */ "
+        + "empno, ename, deptno from emps";
+    // allow empty options.
+    final String expected3 = "SELECT\n"
+        + "/*+ `NO_HASH_JOIN` */\n"
+        + "`EMPNO`, `ENAME`, `DEPTNO`\n"
+        + "FROM `EMPS`";
+    sql(sql3).ok(expected3);
+  }
+
   //~ Inner Interfaces -------------------------------------------------------
 
   /**
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index 88a949d..4019c2e 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -140,6 +140,7 @@ import org.junit.runners.Suite;
     SqlValidatorUtilTest.class,
     TypeFinderTest.class,
     RexShuttleTest.class,
+    SqlHintsConverterTest.class,
 
     // medium tests (above 0.1s)
     SqlParserTest.class,
diff --git a/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java b/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
index 77c8427..6f51176 100644
--- a/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
+++ b/core/src/test/java/org/apache/calcite/test/MockRelOptPlanner.java
@@ -35,6 +35,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 
 /**
  * MockRelOptPlanner is a mock implementation of the {@link RelOptPlanner}
@@ -233,7 +234,8 @@ public class MockRelOptPlanner extends AbstractRelOptPlanner {
     }
 
     // implement RelOptRuleCall
-    public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv) {
+    public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+        BiFunction<RelNode, RelNode, RelNode> handler) {
       transformationResult = rel;
     }
   }
diff --git a/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
new file mode 100644
index 0000000..9659fbc
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
@@ -0,0 +1,516 @@
+/*
+ * 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.test;
+
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.adapter.enumerable.EnumerableHashJoin;
+import org.apache.calcite.adapter.enumerable.EnumerableRules;
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
+import org.apache.calcite.rel.RelVisitor;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinInfo;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.hint.HintStrategies;
+import org.apache.calcite.rel.hint.HintStrategyTable;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.rules.FilterMergeRule;
+import org.apache.calcite.rel.rules.FilterProjectTransposeRule;
+import org.apache.calcite.rel.rules.ProjectMergeRule;
+import org.apache.calcite.sql.SqlDelete;
+import org.apache.calcite.sql.SqlInsert;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlTableRef;
+import org.apache.calcite.sql.SqlUpdate;
+import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql2rel.SqlToRelConverter;
+import org.apache.calcite.tools.Program;
+import org.apache.calcite.tools.Programs;
+import org.apache.calcite.tools.RuleSet;
+import org.apache.calcite.tools.RuleSets;
+import org.apache.calcite.util.Util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * Unit test for {@link org.apache.calcite.rel.hint.RelHint}.
+ */
+public class SqlHintsConverterTest extends SqlToRelTestBase {
+  //~ Static fields/initializers ---------------------------------------------
+
+  private static final String HINT = "properties(k1='v1', k2='v2'), index(ename), no_hash_join";
+
+  private static final RelHint PROPS_HINT = new RelHint(new ArrayList<>(),
+      "PROPERTIES", null,
+      ImmutableMap.of("K1", "v1", "K2", "v2"));
+
+  private static final RelHint IDX_HINT = new RelHint(new ArrayList<>(), "INDEX",
+      ImmutableList.of("ENAME"), null);
+
+  private static final RelHint JOIN_HINT = new RelHint(new ArrayList<>(), "NO_HASH_JOIN",
+      null, null);
+
+  private static final HintStrategyTable HINT_STRATEGY_TABLE = createHintStrategies();
+
+  @Rule public ExpectedException expectedEx = ExpectedException.none();
+
+  //~ Tests ------------------------------------------------------------------
+
+  @Test public void testQueryHint() {
+    final String sql = withHint("select /*+ %s */ *\n"
+        + "from emp e1\n"
+        + "inner join dept d1 on e1.deptno = d1.deptno\n"
+        + "inner join emp e2 on e1.ename = e2.job");
+    final List<String> expectedHints = Arrays.asList(
+        "Project:[[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}], "
+            + "[INDEX inheritPath:{} options:[ENAME]], "
+            + "[NO_HASH_JOIN inheritPath:{}]]",
+        "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]",
+        "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]",
+        "TableScan:[[PROPERTIES inheritPath:{0} options:{K1=v1, K2=v2}], "
+            + "[INDEX inheritPath:{0} options:[ENAME]]]",
+        "TableScan:[[PROPERTIES inheritPath:{0, 1} options:{K1=v1, K2=v2}], "
+            + "[INDEX inheritPath:{0, 1} options:[ENAME]]]",
+        "TableScan:[[PROPERTIES inheritPath:{0, 1} options:{K1=v1, K2=v2}], "
+            + "[INDEX inheritPath:{0, 1} options:[ENAME]]]");
+    sql(sql).ok(expectedHints);
+  }
+
+  @Test public void testNestedQueryHint() {
+    final String sql = "select /*+ resource(parallelism='3') */ empno\n"
+        + "from (select /*+ resource(mem='20Mb')*/ empno, ename from emp)";
+    sql(sql).ok(
+        Collections.singletonList("Project:[[RESOURCE inheritPath:{} options:{PARALLELISM=3}]]"));
+  }
+
+  @Test public void testTwoLevelNestedQueryHint() {
+    final String sql = "select /*+ resource(parallelism='3'), no_hash_join */ empno\n"
+        + "from (select /*+ resource(mem='20Mb')*/ empno, ename\n"
+        + "from emp left join dept on emp.deptno = dept.deptno)";
+    final List<String> expectedHints = Arrays.asList(
+        "Project:[[RESOURCE inheritPath:{} options:{PARALLELISM=3}]"
+            + ", [NO_HASH_JOIN inheritPath:{}]]",
+        "LogicalJoin:[[NO_HASH_JOIN inheritPath:{0}]]");
+    sql(sql).ok(expectedHints);
+  }
+
+  @Test public void testInvalidQueryHint() {
+    final String sql = "select /*+ weird_hint */ empno\n"
+        + "from (select /*+ resource(mem='20Mb')*/ empno, ename\n"
+        + "from emp left join dept on emp.deptno = dept.deptno)";
+    sql(sql).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+
+    final String sql1 = "select /*+ resource(mem='20Mb')*/ empno\n"
+        + "from (select /*+ weird_kv_hint(k1='v1') */ empno, ename\n"
+        + "from emp left join dept on emp.deptno = dept.deptno)";
+    sql(sql1).fails("Hint: WEIRD_KV_HINT should be registered in the HintStrategies.");
+  }
+
+  @Test public void testTableHintsInJoin() {
+    final String sql = "select\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp /*+ index(idx1, idx2) */\n"
+        + "join dept /*+ properties(k1='v1', k2='v2') */\n"
+        + "on emp.deptno = dept.deptno";
+    final List<String> expectedHints = Arrays.asList(
+        "TableScan:[[INDEX inheritPath:{} options:[IDX1, IDX2]]]",
+        "TableScan:[[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}]]");
+    sql(sql).ok(expectedHints);
+  }
+
+  @Test public void testTableHintsInSelect() {
+    final String sql = withHint("select * from emp /*+ %s */");
+    final List<String> expectedHints = Collections
+        .singletonList("TableScan:["
+            + "[PROPERTIES inheritPath:{} options:{K1=v1, K2=v2}], "
+            + "[INDEX inheritPath:{} options:[ENAME]]]");
+    sql(sql).ok(expectedHints);
+  }
+
+  @Test public void testTableHintsInInsert() throws Exception {
+    final String sql = withHint("insert into dept /*+ %s */ (deptno, name) "
+        + "select deptno, name from dept");
+    final SqlInsert insert = (SqlInsert) tester.parseQuery(sql);
+    assert insert.getTargetTable() instanceof SqlTableRef;
+    final SqlTableRef tableRef = (SqlTableRef) insert.getTargetTable();
+    List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+        (SqlNodeList) tableRef.getOperandList().get(1));
+    assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+  }
+
+  @Test public void testTableHintsInUpdate() throws Exception {
+    final String sql = withHint("update emp /*+ %s */ set name = 'test' where deptno = 1");
+    final SqlUpdate sqlUpdate = (SqlUpdate) tester.parseQuery(sql);
+    assert sqlUpdate.getTargetTable() instanceof SqlTableRef;
+    final SqlTableRef tableRef = (SqlTableRef) sqlUpdate.getTargetTable();
+    List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+        (SqlNodeList) tableRef.getOperandList().get(1));
+    assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+  }
+
+  @Test public void testTableHintsInDelete() throws Exception {
+    final String sql = withHint("delete from emp /*+ %s */ where deptno = 1");
+    final SqlDelete sqlDelete = (SqlDelete) tester.parseQuery(sql);
+    assert sqlDelete.getTargetTable() instanceof SqlTableRef;
+    final SqlTableRef tableRef = (SqlTableRef) sqlDelete.getTargetTable();
+    List<RelHint> hints = SqlUtil.getRelHint(HINT_STRATEGY_TABLE,
+        (SqlNodeList) tableRef.getOperandList().get(1));
+    assertHintsEquals(Arrays.asList(PROPS_HINT, IDX_HINT, JOIN_HINT), hints);
+  }
+
+  @Test public void testInvalidTableHints() {
+    final String sql = "select\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp /*+ weird_hint(idx1, idx2) */\n"
+        + "join dept /*+ properties(k1='v1', k2='v2') */\n"
+        + "on emp.deptno = dept.deptno";
+    sql(sql).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+
+    final String sql1 = "select\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp /*+ index(idx1, idx2) */\n"
+        + "join dept /*+ weird_kv_hint(k1='v1', k2='v2') */\n"
+        + "on emp.deptno = dept.deptno";
+    sql(sql1).fails("Hint: WEIRD_HINT should be registered in the HintStrategies.");
+  }
+
+  @Test public void testJoinHintRequiresSpecificInputs() {
+    final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp join dept on emp.deptno = dept.deptno";
+    final List<String> expectedHints = Arrays.asList(
+        "Project:[[USE_HASH_JOIN inheritPath:{} options:[R, S]], "
+            + "[USE_HASH_JOIN inheritPath:{} options:[EMP, DEPT]]]",
+        "LogicalJoin:[[USE_HASH_JOIN inheritPath:{0} options:[EMP, DEPT]]]");
+    // Hint use_hash_join(r, s) expect to be ignored by the join node.
+    sql(sql).ok(expectedHints);
+  }
+
+  @Test public void testHintsPropagationInHepPlannerRules() {
+    final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp join dept on emp.deptno = dept.deptno";
+    final RelNode rel = tester.convertSqlToRel(sql).rel;
+    final RelHint hint = new RelHint(
+        Collections.singletonList(0),
+        "USE_HASH_JOIN",
+        Arrays.asList("EMP", "DEPT"),
+        null);
+    // Validate Hep planner.
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(MockJoinRule.INSTANCE)
+        .build();
+    HepPlanner planner = new HepPlanner(program);
+    planner.setRoot(rel);
+    RelNode newRel = planner.findBestExp();
+    new ValidateHintVisitor(hint, Join.class).go(newRel);
+  }
+
+  @Test public void testHintsPropagationInVolcanoPlannerRules() {
+    final String sql = "select /*+ use_hash_join(r, s), use_hash_join(emp, dept) */\n"
+        + "ename, job, sal, dept.name\n"
+        + "from emp join dept on emp.deptno = dept.deptno";
+    RelOptPlanner planner = new VolcanoPlanner();
+    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+    Tester tester1 = tester.withDecorrelation(true)
+        .withClusterFactory(
+            relOptCluster -> RelOptCluster.create(planner, relOptCluster.getRexBuilder()));
+    final RelNode rel = tester1.convertSqlToRel(sql).rel;
+    final RelHint hint = new RelHint(
+        Collections.singletonList(0),
+        "USE_HASH_JOIN",
+        Arrays.asList("EMP", "DEPT"),
+        null);
+    // Validate Volcano planner.
+    RuleSet ruleSet = RuleSets.ofList(
+        new MockEnumerableJoinRule(hint), // Rule to validate the hint.
+        FilterProjectTransposeRule.INSTANCE,
+        FilterMergeRule.INSTANCE,
+        ProjectMergeRule.INSTANCE,
+        EnumerableRules.ENUMERABLE_JOIN_RULE,
+        EnumerableRules.ENUMERABLE_PROJECT_RULE,
+        EnumerableRules.ENUMERABLE_FILTER_RULE,
+        EnumerableRules.ENUMERABLE_SORT_RULE,
+        EnumerableRules.ENUMERABLE_LIMIT_RULE,
+        EnumerableRules.ENUMERABLE_TABLE_SCAN_RULE);
+    Program program = Programs.of(ruleSet);
+    RelTraitSet toTraits = rel
+        .getCluster()
+        .traitSet()
+        .replace(EnumerableConvention.INSTANCE);
+
+    program.run(planner, rel, toTraits,
+        Collections.emptyList(), Collections.emptyList());
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override protected Tester createTester() {
+    return super.createTester()
+        .withConfig(SqlToRelConverter
+            .configBuilder()
+            .withHintStrategyTable(HINT_STRATEGY_TABLE)
+            .build());
+  }
+
+  /** Format the query with hint {@link #HINT}. */
+  private static String withHint(String sql) {
+    return String.format(Locale.ROOT, sql, HINT);
+  }
+
+  /** Sets the SQL statement for a test. */
+  public final Sql sql(String sql) {
+    return new Sql(sql, tester, expectedEx);
+  }
+
+  /**
+   * Creates mock hint strategies.
+   *
+   * @return HintStrategyTable instance
+   */
+  private static HintStrategyTable createHintStrategies() {
+    return HintStrategyTable.builder()
+        .addHintStrategy("no_hash_join", HintStrategies.JOIN)
+        .addHintStrategy("time_zone", HintStrategies.SET_VAR)
+        .addHintStrategy("index", HintStrategies.TABLE_SCAN)
+        .addHintStrategy("properties", HintStrategies.TABLE_SCAN)
+        .addHintStrategy("resource", HintStrategies.PROJECT)
+        .addHintStrategy("use_hash_join",
+            HintStrategies.cascade(HintStrategies.JOIN,
+                HintStrategies.explicit((hint, rel) -> {
+                  if (!(rel instanceof LogicalJoin)) {
+                    return false;
+                  }
+                  LogicalJoin join = (LogicalJoin) rel;
+                  final List<String> tableNames = hint.listOptions;
+                  final List<String> inputTables = join.getInputs().stream()
+                      .filter(input -> input instanceof TableScan)
+                      .map(scan -> Util.last(scan.getTable().getQualifiedName()))
+                      .collect(Collectors.toList());
+                  return equalsStringList(tableNames, inputTables);
+                })))
+        .build();
+  }
+
+  private static boolean equalsStringList(List<String> l, List<String> r) {
+    if (l.size() != r.size()) {
+      return false;
+    }
+    for (String s : l) {
+      if (!r.contains(s)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static void assertHintsEquals(List<RelHint> expected, List<RelHint> actual) {
+    assertArrayEquals(expected.toArray(new RelHint[0]), actual.toArray(new RelHint[0]));
+  }
+
+  //~ Inner Class ------------------------------------------------------------
+
+  /** A Mock rule to validate the hint. */
+  private static class MockJoinRule extends RelOptRule {
+    public static final MockJoinRule INSTANCE = new MockJoinRule();
+
+    MockJoinRule() {
+      super(operand(LogicalJoin.class, any()), "MockJoinRule");
+    }
+
+    public void onMatch(RelOptRuleCall call) {
+      LogicalJoin join = call.rel(0);
+      assertThat(1, is(join.getHints().size()));
+      call.transformTo(
+          LogicalJoin.create(join.getLeft(),
+              join.getRight(),
+              join.getCondition(),
+              join.getVariablesSet(),
+              join.getJoinType()));
+    }
+  }
+
+  /** A Mock rule to validate the hint.
+   * This rule also converts the rel to EnumerableConvention. */
+  private static class MockEnumerableJoinRule extends ConverterRule {
+    private final RelHint expectedHint;
+
+    MockEnumerableJoinRule(RelHint hint) {
+      super(
+          LogicalJoin.class,
+          Convention.NONE,
+          EnumerableConvention.INSTANCE,
+          "MockEnumerableJoinRule");
+      this.expectedHint = hint;
+    }
+
+    @Override public RelNode convert(RelNode rel) {
+      LogicalJoin join = (LogicalJoin) rel;
+      assertThat(join.getHints().size(), is(1));
+      assertEquals(expectedHint, join.getHints().get(0));
+      List<RelNode> newInputs = new ArrayList<>();
+      for (RelNode input : join.getInputs()) {
+        if (!(input.getConvention() instanceof EnumerableConvention)) {
+          input =
+              convert(
+                  input,
+                  input.getTraitSet()
+                      .replace(EnumerableConvention.INSTANCE));
+        }
+        newInputs.add(input);
+      }
+      final RelOptCluster cluster = join.getCluster();
+      final RelNode left = newInputs.get(0);
+      final RelNode right = newInputs.get(1);
+      final JoinInfo info = join.analyzeCondition();
+      return EnumerableHashJoin.create(
+          left,
+          right,
+          info.getEquiCondition(left, right, cluster.getRexBuilder()),
+          join.getVariablesSet(),
+          join.getJoinType());
+    }
+  }
+
+  /** A visitor to validate the join node has specific hint. **/
+  private static class ValidateHintVisitor extends RelVisitor {
+    private RelHint expectedHint;
+    private Class clazz;
+
+    /**
+     * Creates the validate visitor.
+     *
+     * @param hint  the hint to validate
+     * @param clazz the node type to validate the hint with
+     */
+    ValidateHintVisitor(RelHint hint, Class clazz) {
+      this.expectedHint = hint;
+      this.clazz = clazz;
+    }
+
+    @Override public void visit(
+        RelNode node,
+        int ordinal,
+        RelNode parent) {
+      if (clazz.isInstance(node)) {
+        Join join = (Join) node;
+        assertThat(join.getHints().size(), is(1));
+        assertEquals(expectedHint, join.getHints().get(0));
+      }
+      super.visit(node, ordinal, parent);
+    }
+  }
+
+  /** Sql test tool. */
+  private static class Sql {
+    private String sql;
+    private Tester tester;
+    private List<String> hintsCollect;
+    private ExpectedException expectedEx;
+
+    Sql(String sql, Tester tester, ExpectedException expectedEx) {
+      this.sql = sql;
+      this.tester = tester;
+      this.hintsCollect = new ArrayList<>();
+      this.expectedEx = expectedEx;
+    }
+
+    Sql tester(Tester tester) {
+      this.tester = tester;
+      return this;
+    }
+
+    Sql ok(List<String> expectedHints) {
+      final HintCollector collector = new HintCollector(hintsCollect);
+      final RelNode rel = tester.convertSqlToRel(sql).rel;
+      rel.accept(collector);
+      assertArrayEquals(expectedHints.toArray(new String[0]), hintsCollect.toArray(new String[0]));
+      return this;
+    }
+
+    Sql fails(String failedMsg) {
+      expectedEx.expect(RuntimeException.class);
+      expectedEx.expectMessage(failedMsg);
+      tester.convertSqlToRel(sql);
+      return this;
+    }
+
+    /** A shuttle to collect all the hints within the relational expression into a collection. */
+    private static class HintCollector extends RelShuttleImpl {
+      private final List<String> hintsCollect;
+
+      HintCollector(List<String> hintsCollect) {
+        this.hintsCollect = hintsCollect;
+      }
+
+      @Override public RelNode visit(TableScan scan) {
+        if (scan.getHints().size() > 0) {
+          this.hintsCollect.add("TableScan:" + scan.getHints().toString());
+        }
+        return super.visit(scan);
+      }
+
+      @Override public RelNode visit(LogicalJoin join) {
+        if (join.getHints().size() > 0) {
+          this.hintsCollect.add("LogicalJoin:" + join.getHints().toString());
+        }
+        return super.visit(join);
+      }
+
+      @Override public RelNode visit(LogicalProject project) {
+        if (project.getHints().size() > 0) {
+          this.hintsCollect.add("Project:" + project.getHints().toString());
+        }
+        return super.visit(project);
+      }
+    }
+  }
+}
+
+// End SqlHintsConverterTest.java
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
index 5958371..db104c1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
@@ -886,8 +886,11 @@ public abstract class SqlToRelTestBase {
     private final RelOptCluster cluster;
     private final SqlToRelConverter.Config config;
 
-    MockViewExpander(SqlValidator validator, Prepare.CatalogReader catalogReader,
-        RelOptCluster cluster, SqlToRelConverter.Config config) {
+    MockViewExpander(
+        SqlValidator validator,
+        Prepare.CatalogReader catalogReader,
+        RelOptCluster cluster,
+        SqlToRelConverter.Config config) {
       this.validator = validator;
       this.catalogReader = catalogReader;
       this.cluster = cluster;
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
index bba9b2f..97179b0 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
+++ b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
@@ -25,6 +25,7 @@ import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.linq4j.QueryProvider;
 import org.apache.calcite.linq4j.Queryable;
 import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
 import org.apache.calcite.plan.RelOptSchema;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.prepare.CalciteCatalogReader;
@@ -551,7 +552,8 @@ public abstract class MockCatalogReader extends CalciteCatalogReader {
     }
 
     public Expression getExpression(Class clazz) {
-      throw new UnsupportedOperationException();
+      // Return a true constant just to pass the tests in EnumerableTableScanRule.
+      return Expressions.constant(true);
     }
 
     public void addColumn(String name, RelDataType type) {
diff --git a/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java b/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
index 0b8c11a..34b8ea1 100644
--- a/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
+++ b/server/src/main/java/org/apache/calcite/sql/ddl/SqlDdlNodes.java
@@ -224,7 +224,7 @@ public class SqlDdlNodes {
                 .addAll(columnList)
                 .build());
     return new SqlSelect(p, null, selectList, from, null, null, null, null,
-        null, null, null);
+        null, null, null, null);
   }
 
   /** Populates the table called {@code name} by executing {@code query}. */
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index b26674c..36566dd 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -199,7 +199,7 @@ orderItem:
       expression [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ]
 
 select:
-      SELECT [ STREAM ] [ ALL | DISTINCT ]
+      SELECT [ '/*+' hint [, hint]* '*/' ] [ STREAM ] [ ALL | DISTINCT ]
           { * | projectItem [, projectItem ]* }
       FROM tableExpression
       [ WHERE booleanExpression ]
@@ -234,7 +234,7 @@ tableReference:
 tablePrimary:
       [ [ catalogName . ] schemaName . ] tableName
       '(' TABLE [ [ catalogName . ] schemaName . ] tableName ')'
-  |   tablePrimary [ EXTEND ] '(' columnDecl [, columnDecl ]* ')'
+  |   tablePrimary [ '/*+' hint [, hint]* '*/' ] [ EXTEND ] '(' columnDecl [, columnDecl ]* ')'
   |   [ LATERAL ] '(' query ')'
   |   UNNEST '(' expression ')' [ WITH ORDINALITY ]
   |   [ LATERAL ] TABLE '(' [ SPECIFIC ] functionName '(' expression [, expression ]* ')' ')'
@@ -242,6 +242,17 @@ tablePrimary:
 columnDecl:
       column type [ NOT NULL ]
 
+hint:
+      hintName
+  |   hintName '(' hintOptions ')'
+
+hintOptions:
+      hintKVOption [, hintKVOption]*
+  |   optionName, [, optionName]*
+
+hintKVOption:
+      optionName '=' stringLiteral
+
 values:
       VALUES expression [, expression ]*