diff --git a/src/optimizer/CMakeLists.txt b/src/optimizer/CMakeLists.txt
index c79e44f3621669bc469115b7bcb6fff9f4ac8318..0c62d94c8172b72e3a94f3fc7cb4df471b08a19e 100644
--- a/src/optimizer/CMakeLists.txt
+++ b/src/optimizer/CMakeLists.txt
@@ -13,6 +13,7 @@ nebula_add_library(
     OptContext.cpp
     rule/PushFilterDownGetNbrsRule.cpp
     rule/RemoveNoopProjectRule.cpp
+    rule/CombineFilterRule.cpp
     rule/MergeGetVerticesAndDedupRule.cpp
     rule/MergeGetVerticesAndProjectRule.cpp
     rule/MergeGetNbrsAndDedupRule.cpp
diff --git a/src/optimizer/rule/CombineFilterRule.cpp b/src/optimizer/rule/CombineFilterRule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1fb69865164f05ab4338109838dfd6877e66cfc1
--- /dev/null
+++ b/src/optimizer/rule/CombineFilterRule.cpp
@@ -0,0 +1,75 @@
+/* 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/CombineFilterRule.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> CombineFilterRule::kInstance =
+    std::unique_ptr<CombineFilterRule>(new CombineFilterRule());
+
+CombineFilterRule::CombineFilterRule() {
+    RuleSet::QueryRules().addRule(this);
+}
+
+const Pattern& CombineFilterRule::pattern() const {
+    static Pattern pattern = Pattern::create(graph::PlanNode::Kind::kFilter,
+                                             {Pattern::create(graph::PlanNode::Kind::kFilter)});
+    return pattern;
+}
+
+StatusOr<OptRule::TransformResult> CombineFilterRule::transform(
+    OptContext* octx,
+    const MatchedResult& matched) const {
+    const auto* filterGroupNode = matched.node;
+    const auto* filterAbove = filterGroupNode->node();
+    DCHECK_EQ(filterAbove->kind(), PlanNode::Kind::kFilter);
+    const auto* conditionAbove = static_cast<const graph::Filter*>(filterAbove)->condition();
+    const auto& deps = matched.dependencies;
+    const auto* filterGroup = filterGroupNode->group();
+    auto* qctx = octx->qctx();
+
+    TransformResult result;
+    result.eraseAll = true;
+    for (auto& dep : deps) {
+        const auto* groupNode = dep.node;
+        const auto* filterBelow = groupNode->node();
+        DCHECK_EQ(filterBelow->kind(), PlanNode::Kind::kFilter);
+        auto* newFilter = static_cast<graph::Filter*>(filterBelow->clone());
+        const auto* conditionBelow = newFilter->condition();
+        auto* conditionCombine =
+            qctx->objPool()->add(new LogicalExpression(Expression::Kind::kLogicalAnd,
+                                                       conditionAbove->clone().release(),
+                                                       conditionBelow->clone().release()));
+        newFilter->setCondition(conditionCombine);
+        newFilter->setOutputVar(filterAbove->outputVar());
+        auto* newGroupNode = OptGroupNode::create(octx, newFilter, filterGroup);
+        newGroupNode->setDeps(groupNode->dependencies());
+        result.newGroupNodes.emplace_back(newGroupNode);
+    }
+
+    return result;
+}
+
+bool CombineFilterRule::match(OptContext* octx, const MatchedResult& matched) const {
+    return OptRule::match(octx, matched);
+}
+
+std::string CombineFilterRule::toString() const {
+    return "CombineFilterRule";
+}
+
+}   // namespace opt
+}   // namespace nebula
diff --git a/src/optimizer/rule/CombineFilterRule.h b/src/optimizer/rule/CombineFilterRule.h
new file mode 100644
index 0000000000000000000000000000000000000000..92ac49a1ba9fa499b92484f6daa622851505b772
--- /dev/null
+++ b/src/optimizer/rule/CombineFilterRule.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_COMBINEFILTERRULE_H_
+#define OPTIMIZER_RULE_COMBINEFILTERRULE_H_
+
+#include <memory>
+
+#include "optimizer/OptRule.h"
+
+namespace nebula {
+namespace opt {
+
+class CombineFilterRule 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:
+    CombineFilterRule();
+
+    static std::unique_ptr<OptRule> kInstance;
+};
+
+}   // namespace opt
+}   // namespace nebula
+
+#endif   // OPTIMIZER_RULE_COMBINEFILTERRULE_H_
diff --git a/tests/tck/features/expression/UnaryExpr.feature b/tests/tck/features/expression/UnaryExpr.feature
index ee3a47e274942a1264ef1a3682fd87a52824583f..111cb8be8b12766c5995b79695ada689a09b380c 100644
--- a/tests/tck/features/expression/UnaryExpr.feature
+++ b/tests/tck/features/expression/UnaryExpr.feature
@@ -97,12 +97,11 @@ Feature: UnaryExpression
       | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"})                                               |
     And the execution plan should be:
       | id | name        | dependencies | operator info                                      |
-      | 10 | Project     | 9            |                                                    |
-      | 9  | Filter      | 8            |                                                    |
-      | 8  | Filter      | 7            |                                                    |
+      | 10 | Project     | 12           |                                                    |
+      | 12 | Filter      | 7            |                                                    |
       | 7  | Project     | 6            |                                                    |
       | 6  | Project     | 5            |                                                    |
-      | 5  | Filter      | 13           |                                                    |
-      | 13 | GetVertices | 11           |                                                    |
+      | 5  | Filter      | 14           |                                                    |
+      | 14 | GetVertices | 11           |                                                    |
       | 11 | IndexScan   | 0            | {"indexCtx": {"columnHints":{"scanType":"RANGE"}}} |
       | 0  | Start       |              |                                                    |
diff --git a/tests/tck/features/optimizer/CombineFilterRule.feature b/tests/tck/features/optimizer/CombineFilterRule.feature
new file mode 100644
index 0000000000000000000000000000000000000000..331087783266e797d9c157208e6ef96fac8896f3
--- /dev/null
+++ b/tests/tck/features/optimizer/CombineFilterRule.feature
@@ -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.
+Feature: combine filters
+
+  Background:
+    Given a graph with space named "nba"
+
+  Scenario: combine filters
+    When profiling query:
+      """
+      MATCH (v:player)-[:like]->(n)
+      WHERE v.age>40 AND n.age>42
+      RETURN v, n
+      """
+    Then the result should be, in any order:
+      | v                                                       | n                                                   |
+      | ("Steve Nash" :player{age: 45, name: "Steve Nash"})     | ("Jason Kidd" :player{age: 45, name: "Jason Kidd"}) |
+      | ("Vince Carter" :player{age: 42, name: "Vince Carter"}) | ("Jason Kidd" :player{age: 45, name: "Jason Kidd"}) |
+      | ("Jason Kidd" :player{age: 45, name: "Jason Kidd"})     | ("Steve Nash" :player{age: 45, name: "Steve Nash"}) |
+    And the execution plan should be:
+      | id | name         | dependencies | operator info                                                                          |
+      | 16 | Project      | 18           |                                                                                        |
+      | 18 | Filter       | 13           | {"condition" : "((($v.age>40) AND ($n.age>42)) AND !(hasSameEdgeInPath($-.__COL_0)))"} |
+      | 13 | Project      | 12           |                                                                                        |
+      | 12 | InnerJoin    | 11           |                                                                                        |
+      | 11 | Project      | 20           |                                                                                        |
+      | 20 | GetVertices  | 7            |                                                                                        |
+      | 7  | Filter       | 6            |                                                                                        |
+      | 6  | Project      | 5            |                                                                                        |
+      | 5  | Filter       | 22           |                                                                                        |
+      | 22 | GetNeighbors | 17           |                                                                                        |
+      | 17 | IndexScan    | 0            |                                                                                        |
+      | 0  | Start        |              |                                                                                        |
diff --git a/tests/tck/features/optimizer/MergeGetVerticesDedupProjectRule.feature b/tests/tck/features/optimizer/MergeGetVerticesDedupProjectRule.feature
index 9009644343e24c3f555532d5776543e13f130c47..465fb0b5e16281a5d3fd049cd6d03af7c51f829b 100644
--- a/tests/tck/features/optimizer/MergeGetVerticesDedupProjectRule.feature
+++ b/tests/tck/features/optimizer/MergeGetVerticesDedupProjectRule.feature
@@ -19,11 +19,10 @@ Feature: merge get vertices, dedup and project rule
       | "Tim Duncan" |
     And the execution plan should be:
       | id | name        | dependencies | operator info     |
-      | 0  | Project     | 1            |                   |
-      | 1  | Filter      | 2            |                   |
-      | 2  | Filter      | 3            |                   |
-      | 3  | Project     | 4            |                   |
-      | 4  | Project     | 5            |                   |
-      | 5  | GetVertices | 6            | {"dedup": "true"} |
-      | 6  | PassThrough | 7            |                   |
-      | 7  | Start       |              |                   |
+      | 9  | Project     | 10           |                   |
+      | 10 | Filter      | 6            |                   |
+      | 6  | Project     | 5            |                   |
+      | 5  | Project     | 12           |                   |
+      | 12 | GetVertices | 1            | {"dedup": "true"} |
+      | 1  | PassThrough | 0            |                   |
+      | 0  | Start       |              |                   |