diff --git a/src/optimizer/CMakeLists.txt b/src/optimizer/CMakeLists.txt
index 0c62d94c8172b72e3a94f3fc7cb4df471b08a19e..7cc6d6da8ba4515652553deb81bf8b3f9fbfadf3 100644
--- a/src/optimizer/CMakeLists.txt
+++ b/src/optimizer/CMakeLists.txt
@@ -22,6 +22,7 @@ nebula_add_library(
     rule/LimitPushDownRule.cpp
     rule/TopNRule.cpp
     rule/PushFilterDownAggregateRule.cpp
+    rule/PushFilterDownProjectRule.cpp
 )
 
 nebula_add_subdirectory(test)
diff --git a/src/optimizer/rule/PushFilterDownProjectRule.cpp b/src/optimizer/rule/PushFilterDownProjectRule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c5cbee47cceea256ee76864122929d9ac11e3f5b
--- /dev/null
+++ b/src/optimizer/rule/PushFilterDownProjectRule.cpp
@@ -0,0 +1,100 @@
+/* Copyright (c) 2021 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "optimizer/rule/PushFilterDownProjectRule.h"
+
+#include "optimizer/OptContext.h"
+#include "optimizer/OptGroup.h"
+#include "planner/plan/PlanNode.h"
+#include "planner/plan/Query.h"
+#include "util/ExpressionUtils.h"
+
+using nebula::graph::PlanNode;
+using nebula::graph::QueryContext;
+
+namespace nebula {
+namespace opt {
+
+std::unique_ptr<OptRule> PushFilterDownProjectRule::kInstance =
+    std::unique_ptr<PushFilterDownProjectRule>(new PushFilterDownProjectRule());
+
+PushFilterDownProjectRule::PushFilterDownProjectRule() {
+    RuleSet::QueryRules().addRule(this);
+}
+
+const Pattern& PushFilterDownProjectRule::pattern() const {
+    static Pattern pattern = Pattern::create(graph::PlanNode::Kind::kFilter,
+                                             {Pattern::create(graph::PlanNode::Kind::kProject)});
+    return pattern;
+}
+
+StatusOr<OptRule::TransformResult> PushFilterDownProjectRule::transform(
+    OptContext* octx,
+    const MatchedResult& matched) const {
+    auto filterGroupNode = matched.node;
+    auto oldFilterNode = filterGroupNode->node();
+    auto projGroupNode = matched.dependencies.front().node;
+    auto oldProjNode = projGroupNode->node();
+
+    auto newFilterNode = static_cast<graph::Filter*>(oldFilterNode->clone());
+    auto newProjNode = static_cast<graph::Project*>(oldProjNode->clone());
+    const auto condition = newFilterNode->condition();
+
+    auto varProps = graph::ExpressionUtils::collectAll(condition, {Expression::Kind::kVarProperty});
+    if (varProps.empty()) {
+        return TransformResult::noTransform();
+    }
+    std::vector<std::string> propNames;
+    for (auto expr : varProps) {
+        DCHECK(expr->kind() == Expression::Kind::kVarProperty);
+        propNames.emplace_back(*static_cast<const VariablePropertyExpression*>(expr)->prop());
+    }
+
+    auto projColNames = newProjNode->colNames();
+    auto projColumns = newProjNode->columns()->columns();
+    for (size_t i = 0; i < projColNames.size(); ++i) {
+        auto column = projColumns[i];
+        auto colName = projColNames[i];
+        auto iter = std::find_if(propNames.begin(), propNames.end(), [&colName](const auto& name) {
+            return !colName.compare(name);
+        });
+        if (iter == propNames.end()) continue;
+        if (!column->alias() && column->expr()->kind() == Expression::Kind::kVarProperty) {
+            continue;
+        } else {
+            // project column contains computing expression, need to rewrite
+            return TransformResult::noTransform();
+        }
+    }
+
+    // Exchange planNode
+    newProjNode->setOutputVar(oldFilterNode->outputVar());
+    newFilterNode->setInputVar(oldProjNode->inputVar());
+    newProjNode->setInputVar(oldProjNode->outputVar());
+    newFilterNode->setOutputVar(oldProjNode->outputVar());
+
+    // Push down filter's optGroup and embed newProjGroupNode into old filter's Group
+    auto newProjGroupNode = OptGroupNode::create(octx, newProjNode, filterGroupNode->group());
+    auto newFilterGroup = OptGroup::create(octx);
+    auto newFilterGroupNode = newFilterGroup->makeGroupNode(newFilterNode);
+    newProjGroupNode->dependsOn(newFilterGroup);
+    for (auto dep : projGroupNode->dependencies()) {
+        newFilterGroupNode->dependsOn(dep);
+    }
+
+    TransformResult result;
+    result.eraseAll = true;
+    result.newGroupNodes.emplace_back(newProjGroupNode);
+    return result;
+}
+
+std::string PushFilterDownProjectRule::toString() const {
+    return "PushFilterDownProjectRule";
+}
+
+}   // namespace opt
+}   // namespace nebula
+
diff --git a/src/optimizer/rule/PushFilterDownProjectRule.h b/src/optimizer/rule/PushFilterDownProjectRule.h
new file mode 100644
index 0000000000000000000000000000000000000000..14be2df8baf8eb6b9ee2298b124b6fbf556e4cf6
--- /dev/null
+++ b/src/optimizer/rule/PushFilterDownProjectRule.h
@@ -0,0 +1,35 @@
+/* Copyright (c) 2021 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_
+#define OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_
+
+#include <memory>
+#include "optimizer/OptRule.h"
+
+namespace nebula {
+namespace opt {
+
+class PushFilterDownProjectRule final : public OptRule {
+public:
+    const Pattern &pattern() const override;
+
+    StatusOr<OptRule::TransformResult> transform(OptContext *qctx,
+                                                 const MatchedResult &matched) const override;
+
+    std::string toString() const override;
+
+private:
+    PushFilterDownProjectRule();
+
+    static std::unique_ptr<OptRule> kInstance;
+};
+
+}   // namespace opt
+}   // namespace nebula
+
+#endif   // OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_
+
diff --git a/tests/tck/features/optimizer/PushFilterDownProjectRule.feature b/tests/tck/features/optimizer/PushFilterDownProjectRule.feature
new file mode 100644
index 0000000000000000000000000000000000000000..4593f4e7aafc8de8969bb5087f39f4672fa3ae40
--- /dev/null
+++ b/tests/tck/features/optimizer/PushFilterDownProjectRule.feature
@@ -0,0 +1,82 @@
+# Copyright (c) 2021 vesoft inc. All rights reserved.
+#
+# This source code is licensed under Apache 2.0 License,
+# attached with Common Clause Condition 1.0, found in the LICENSES directory.
+Feature: Push Filter down Project rule
+
+  Background:
+    Given a graph with space named "nba"
+
+  Scenario: push filter down Project
+    When profiling query:
+      """
+      MATCH (a:player)--(b)--(c)
+      WITH a AS a, b AS b, c AS c
+      WHERE a.age < 25 AND b.age < 25 AND c.age < 25
+      RETURN a
+      """
+    Then the result should be, in any order:
+      | a                                                                   |
+      | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) |
+      | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) |
+      | ("Luka Doncic" :player{age: 20, name: "Luka Doncic"})               |
+      | ("Luka Doncic" :player{age: 20, name: "Luka Doncic"})               |
+    And the execution plan should be:
+      | id | name         | dependencies | operator info |
+      | 23 | Project      | 40           |               |
+      | 40 | Project      | 39           |               |
+      | 39 | Filter       | 20           |               |
+      | 20 | Filter       | 19           |               |
+      | 19 | Project      | 18           |               |
+      | 18 | InnerJoin    | 17           |               |
+      | 17 | Project      | 28           |               |
+      | 28 | GetVertices  | 13           |               |
+      | 13 | InnerJoin    | 12           |               |
+      | 12 | Filter       | 11           |               |
+      | 11 | Project      | 32           |               |
+      | 32 | GetNeighbors | 7            |               |
+      | 7  | Filter       | 6            |               |
+      | 6  | Project      | 5            |               |
+      | 5  | Filter       | 31           |               |
+      | 31 | GetNeighbors | 24           |               |
+      | 24 | IndexScan    | 0            |               |
+      | 0  | Start        |              |               |
+    When profiling query:
+      """
+      MATCH (a:player)--(b)--(c)
+      WITH a, b, c.age+1 AS cage
+      WHERE a.name == 'Tim Duncan' AND b.age > 40
+      RETURN DISTINCT a, b, cage
+      """
+    Then the result should be, in any order:
+      | a                                                                                                           | b                                                             | cage |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | 39   |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | 32   |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | NULL |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})     | NULL |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})     | 43   |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})     | 37   |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})     | 35   |
+      | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})     | 30   |
+    And the execution plan should be:
+      | id | name         | dependencies | operator info |
+      | 25 | DataCollect  | 24           |               |
+      | 24 | Dedup        | 41           |               |
+      | 41 | Project      | 40           |               |
+      | 40 | Filter       | 20           |               |
+      | 20 | Filter       | 19           |               |
+      | 19 | Project      | 18           |               |
+      | 18 | InnerJoin    | 17           |               |
+      | 17 | Project      | 29           |               |
+      | 29 | GetVertices  | 13           |               |
+      | 13 | InnerJoin    | 12           |               |
+      | 12 | Filter       | 11           |               |
+      | 11 | Project      | 33           |               |
+      | 33 | GetNeighbors | 7            |               |
+      | 7  | Filter       | 6            |               |
+      | 6  | Project      | 5            |               |
+      | 5  | Filter       | 32           |               |
+      | 32 | GetNeighbors | 26           |               |
+      | 26 | IndexScan    | 0            |               |
+      | 0  | Start        |              |               |
+