diff --git a/src/optimizer/CMakeLists.txt b/src/optimizer/CMakeLists.txt
index d186bd16ebccd9b328b4f3e30cce576abadc0905..c79e44f3621669bc469115b7bcb6fff9f4ac8318 100644
--- a/src/optimizer/CMakeLists.txt
+++ b/src/optimizer/CMakeLists.txt
@@ -12,6 +12,7 @@ nebula_add_library(
     OptRule.cpp
     OptContext.cpp
     rule/PushFilterDownGetNbrsRule.cpp
+    rule/RemoveNoopProjectRule.cpp
     rule/MergeGetVerticesAndDedupRule.cpp
     rule/MergeGetVerticesAndProjectRule.cpp
     rule/MergeGetNbrsAndDedupRule.cpp
diff --git a/src/optimizer/OptGroup.h b/src/optimizer/OptGroup.h
index bd48a4789cfdaa605c4cc05f5638ead85840118b..cb9f3ea5e53832775451edb256284d37c926934c 100644
--- a/src/optimizer/OptGroup.h
+++ b/src/optimizer/OptGroup.h
@@ -79,6 +79,10 @@ public:
         return dependencies_;
     }
 
+    void setDeps(std::vector<OptGroup *> deps) {
+        dependencies_ = deps;
+    }
+
     void addBody(OptGroup *body) {
         bodies_.emplace_back(body);
     }
diff --git a/src/optimizer/rule/RemoveNoopProjectRule.cpp b/src/optimizer/rule/RemoveNoopProjectRule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..85c9904c25329ea9956f34312568a48fad9b3af3
--- /dev/null
+++ b/src/optimizer/rule/RemoveNoopProjectRule.cpp
@@ -0,0 +1,108 @@
+/* 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/RemoveNoopProjectRule.h"
+
+#include "optimizer/OptContext.h"
+#include "optimizer/OptGroup.h"
+#include "planner/PlanNode.h"
+#include "planner/Query.h"
+
+using nebula::graph::PlanNode;
+using nebula::graph::QueryContext;
+
+namespace nebula {
+namespace opt {
+
+std::unique_ptr<OptRule> RemoveNoopProjectRule::kInstance =
+    std::unique_ptr<RemoveNoopProjectRule>(new RemoveNoopProjectRule());
+
+RemoveNoopProjectRule::RemoveNoopProjectRule() {
+    RuleSet::QueryRules().addRule(this);
+}
+
+const Pattern& RemoveNoopProjectRule::pattern() const {
+    static Pattern pattern = Pattern::create(graph::PlanNode::Kind::kProject);
+    return pattern;
+}
+
+StatusOr<OptRule::TransformResult> RemoveNoopProjectRule::transform(
+    OptContext* octx,
+    const MatchedResult& matched) const {
+    const auto* projGroupNode = matched.node;
+    const auto* oldProjNode = projGroupNode->node();
+    DCHECK_EQ(oldProjNode->kind(), PlanNode::Kind::kProject);
+
+    TransformResult result;
+    result.eraseAll = true;
+    const auto* projGroup = projGroupNode->group();
+    const auto depGroups = projGroupNode->dependencies();
+    DCHECK_EQ(depGroups.size(), 1);
+    const auto* depGroup = depGroups.front();
+    const auto& groupNodes = depGroup->groupNodes();
+    for (auto* groupNode : groupNodes) {
+        auto* newNode = groupNode->node()->clone();
+        const auto& newColNames = newNode->colNames();
+        const auto& oldColNames = oldProjNode->colNames();
+        auto colsNum = newColNames.size();
+        if (colsNum != oldColNames.size()) {
+            return TransformResult::noTransform();
+        }
+        for (size_t i = 0; i < colsNum; ++i) {
+            if (newColNames[i].compare(oldColNames[i])) {
+                return TransformResult::noTransform();
+            }
+        }
+        newNode->setOutputVar(oldProjNode->outputVar());
+        auto* newGroupNode = OptGroupNode::create(octx, newNode, projGroup);
+        newGroupNode->setDeps(groupNode->dependencies());
+        result.newGroupNodes.emplace_back(newGroupNode);
+    }
+
+    return result;
+}
+
+bool RemoveNoopProjectRule::match(OptContext* octx, const MatchedResult& matched) const {
+    if (!OptRule::match(octx, matched)) {
+        return false;
+    }
+
+    auto* projGroupNode = matched.node;
+    DCHECK_EQ(projGroupNode->node()->kind(), PlanNode::Kind::kProject);
+    auto depGroups = projGroupNode->dependencies();
+
+    // handled in Pattern::match
+    DCHECK_EQ(depGroups.size(), 1);
+    auto* node = depGroups.front()->groupNodes().front()->node();
+    auto kind = node->kind();
+    // disable BiInputNode/SetOp (multi input)
+    // disable IndexScan/PassThrough (multi output)
+    if (!node->isSingleInput() ||
+        kind == PlanNode::Kind::kUnion ||
+        kind == PlanNode::Kind::kMinus ||
+        kind == PlanNode::Kind::kIntersect ||
+        kind == PlanNode::Kind::kIndexScan ||
+        kind == PlanNode::Kind::kPassThrough) {
+        return false;
+    }
+
+    auto* projNode = static_cast<const graph::Project*>(projGroupNode->node());
+    std::vector<YieldColumn*> cols = projNode->columns()->columns();
+    for (auto* col : cols) {
+        if (col->alias() || col->expr()->kind() != Expression::Kind::kVarProperty) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+std::string RemoveNoopProjectRule::toString() const {
+    return "RemoveNoopProjectRule";
+}
+
+}   // namespace opt
+}   // namespace nebula
diff --git a/src/optimizer/rule/RemoveNoopProjectRule.h b/src/optimizer/rule/RemoveNoopProjectRule.h
new file mode 100644
index 0000000000000000000000000000000000000000..2031754667b0bc8d66941f82246e108b7892bf5e
--- /dev/null
+++ b/src/optimizer/rule/RemoveNoopProjectRule.h
@@ -0,0 +1,37 @@
+/* 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_REMOVENOOPPROJECTRULE_H_
+#define OPTIMIZER_RULE_REMOVENOOPPROJECTRULE_H_
+
+#include <memory>
+
+#include "optimizer/OptRule.h"
+
+namespace nebula {
+namespace opt {
+
+class RemoveNoopProjectRule final : public OptRule {
+public:
+    const Pattern &pattern() const override;
+
+    StatusOr<TransformResult> transform(OptContext *ctx,
+                                        const MatchedResult &matched) const override;
+
+    bool match(OptContext *ctx, const MatchedResult &matched) const override;
+
+    std::string toString() const override;
+
+private:
+    RemoveNoopProjectRule();
+
+    static std::unique_ptr<OptRule> kInstance;
+};
+
+}   // namespace opt
+}   // namespace nebula
+
+#endif   // OPTIMIZER_RULE_REMOVENOOPPROJECTRULE_H_
diff --git a/tests/tck/features/optimizer/PushFilterDownAggregateRule.feature b/tests/tck/features/optimizer/PushFilterDownAggregateRule.feature
index 9ca947eb36860770526c655b23fdebb44072ce6c..7ed720a0755c0fcbaac450d780a800b4bc0f4878 100644
--- a/tests/tck/features/optimizer/PushFilterDownAggregateRule.feature
+++ b/tests/tck/features/optimizer/PushFilterDownAggregateRule.feature
@@ -11,10 +11,12 @@ Feature: Push Filter down Aggregate rule
     When profiling query:
       """
       MATCH (v:player)
-      WITH v.age+1 AS age, COUNT(v.age) as count
-        WHERE age<30
-      RETURN age,count
-        ORDER BY age
+      WITH
+        v.age+1 AS age,
+        COUNT(v.age) as count
+      WHERE age<30
+      RETURN age, count
+      ORDER BY age
       """
     Then the result should be, in any order:
       | age | count |
@@ -34,14 +36,13 @@ Feature: Push Filter down Aggregate rule
     And the execution plan should be:
       | id | name        | dependencies | operator info |
       | 13 | DataCollect | 12           |               |
-      | 12 | Sort        | 11           |               |
-      | 11 | Project     | 18           |               |
-      | 18 | Aggregate   | 17           |               |
-      | 17 | Filter      | 8            |               |
+      | 12 | Sort        | 19           |               |
+      | 19 | Aggregate   | 18           |               |
+      | 18 | Filter      | 8            |               |
       | 8  | Filter      | 7            |               |
       | 7  | Project     | 6            |               |
       | 6  | Project     | 5            |               |
-      | 5  | Filter      | 16           |               |
-      | 16 | GetVertices | 14           |               |
+      | 5  | Filter      | 17           |               |
+      | 17 | GetVertices | 14           |               |
       | 14 | IndexScan   | 0            |               |
       | 0  | Start       |              |               |
diff --git a/tests/tck/features/optimizer/RemoveUselessProjectRule.feature b/tests/tck/features/optimizer/RemoveUselessProjectRule.feature
new file mode 100644
index 0000000000000000000000000000000000000000..bceb55c6ab8e8d468002cc7f56e26676145004fc
--- /dev/null
+++ b/tests/tck/features/optimizer/RemoveUselessProjectRule.feature
@@ -0,0 +1,80 @@
+# 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: Remove Useless Project Rule
+
+  Background:
+    Given a graph with space named "nba"
+
+  Scenario: Remove useless project
+    When profiling query:
+      """
+      MATCH (v:player)
+      WITH
+        v.age+1 AS age,
+        count(v.age) AS count
+      RETURN age, count
+      ORDER BY age, count
+      """
+    Then the result should be, in any order:
+      | age | count |
+      | -3  | 1     |
+      | -2  | 1     |
+      | -1  | 1     |
+      | 0   | 1     |
+      | 1   | 1     |
+      | 21  | 1     |
+      | 23  | 1     |
+      | 24  | 1     |
+      | 25  | 1     |
+      | 26  | 2     |
+      | 27  | 1     |
+      | 28  | 1     |
+      | 29  | 3     |
+      | 30  | 4     |
+      | 31  | 4     |
+      | 32  | 3     |
+      | 33  | 3     |
+      | 34  | 4     |
+      | 35  | 4     |
+      | 37  | 3     |
+      | 38  | 1     |
+      | 39  | 3     |
+      | 40  | 1     |
+      | 41  | 2     |
+      | 42  | 1     |
+      | 43  | 2     |
+      | 44  | 1     |
+      | 46  | 2     |
+      | 47  | 1     |
+      | 48  | 1     |
+    And the execution plan should be:
+      | id | name        | dependencies | operator info |
+      | 12 | DataCollect | 11           |               |
+      | 11 | Sort        | 14           |               |
+      | 14 | Aggregate   | 8            |               |
+      | 8  | Filter      | 7            |               |
+      | 7  | Project     | 6            |               |
+      | 6  | Project     | 5            |               |
+      | 5  | Filter      | 16           |               |
+      | 16 | GetVertices | 13           |               |
+      | 13 | IndexScan   | 0            |               |
+      | 0  | Start       |              |               |
+    When profiling query:
+      """
+      MATCH p = (n:player{name:"Tony Parker"})
+      RETURN n, p
+      """
+    Then the result should be, in any order:
+      | n                                                     | p                                                       |
+      | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | <("Tony Parker" :player{age: 36, name: "Tony Parker"})> |
+    And the execution plan should be:
+      | id | name        | dependencies | operator info |
+      | 11 | Filter      | 7            |               |
+      | 7  | Project     | 6            |               |
+      | 6  | Project     | 5            |               |
+      | 5  | Filter      | 13           |               |
+      | 13 | GetVertices | 10           |               |
+      | 10 | IndexScan   | 0            |               |
+      | 0  | Start       |              |               |