Simplified ExtractCommonSubexpression rule as a BottomUp rule.
diff --git a/query_optimizer/rules/CMakeLists.txt b/query_optimizer/rules/CMakeLists.txt
index 36e5959..70302be 100644
--- a/query_optimizer/rules/CMakeLists.txt
+++ b/query_optimizer/rules/CMakeLists.txt
@@ -109,7 +109,7 @@
                       quickstep_queryoptimizer_physical_Physical
                       quickstep_queryoptimizer_physical_PhysicalType
                       quickstep_queryoptimizer_physical_Selection
-                      quickstep_queryoptimizer_rules_Rule
+                      quickstep_queryoptimizer_rules_BottomUpRule
                       quickstep_utility_HashError
                       quickstep_utility_Macros)
 target_link_libraries(quickstep_queryoptimizer_rules_FuseAggregateJoin
diff --git a/query_optimizer/rules/ExtractCommonSubexpression.cpp b/query_optimizer/rules/ExtractCommonSubexpression.cpp
index 63b6b17..8b68bd2 100644
--- a/query_optimizer/rules/ExtractCommonSubexpression.cpp
+++ b/query_optimizer/rules/ExtractCommonSubexpression.cpp
@@ -66,30 +66,12 @@
   }
 }
 
-P::PhysicalPtr ExtractCommonSubexpression::apply(const P::PhysicalPtr &input) {
-  DCHECK(input->getPhysicalType() == P::PhysicalType::kTopLevelPlan);
-
-  return applyInternal(input);
-}
-
-P::PhysicalPtr ExtractCommonSubexpression::applyInternal(
+P::PhysicalPtr ExtractCommonSubexpression::applyToNode(
     const P::PhysicalPtr &input) {
-  // First process all child nodes.
-  std::vector<P::PhysicalPtr> new_children;
-  for (const auto &child : input->children()) {
-    new_children.emplace_back(applyInternal(child));
-  }
-
-  const P::PhysicalPtr node =
-      new_children == input->children()
-          ? input
-          : input->copyWithNewChildren(new_children);
-
-  // Process expressions of the current node.
-  switch (node->getPhysicalType()) {
+  switch (input->getPhysicalType()) {
     case P::PhysicalType::kAggregate: {
       const P::AggregatePtr aggregate =
-          std::static_pointer_cast<const P::Aggregate>(node);
+          std::static_pointer_cast<const P::Aggregate>(input);
 
       std::vector<E::ExpressionPtr> expressions;
       // Gather grouping expressions and aggregate functions' argument expressions.
@@ -159,7 +141,7 @@
     }
     case P::PhysicalType::kSelection: {
       const P::SelectionPtr selection =
-          std::static_pointer_cast<const P::Selection>(node);
+          std::static_pointer_cast<const P::Selection>(input);
 
       // Transform Selection's project expressions.
       const std::vector<E::NamedExpressionPtr> new_expressions =
@@ -175,7 +157,7 @@
     }
     case P::PhysicalType::kHashJoin: {
       const P::HashJoinPtr hash_join =
-          std::static_pointer_cast<const P::HashJoin>(node);
+          std::static_pointer_cast<const P::HashJoin>(input);
 
       // Transform HashJoin's project expressions.
       const std::vector<E::NamedExpressionPtr> new_expressions =
@@ -195,7 +177,7 @@
     }
     case P::PhysicalType::kNestedLoopsJoin: {
       const P::NestedLoopsJoinPtr nested_loops_join =
-          std::static_pointer_cast<const P::NestedLoopsJoin>(node);
+          std::static_pointer_cast<const P::NestedLoopsJoin>(input);
 
       // Transform NestedLoopsJoin's project expressions.
       const std::vector<E::NamedExpressionPtr> new_expressions =
@@ -214,7 +196,7 @@
       break;
   }
 
-  return node;
+  return input;
 }
 
 std::vector<E::ExpressionPtr> ExtractCommonSubexpression::transformExpressions(
diff --git a/query_optimizer/rules/ExtractCommonSubexpression.hpp b/query_optimizer/rules/ExtractCommonSubexpression.hpp
index 26b09cc..7d69e00 100644
--- a/query_optimizer/rules/ExtractCommonSubexpression.hpp
+++ b/query_optimizer/rules/ExtractCommonSubexpression.hpp
@@ -34,7 +34,7 @@
 #include "query_optimizer/expressions/ExpressionType.hpp"
 #include "query_optimizer/expressions/Scalar.hpp"
 #include "query_optimizer/physical/Physical.hpp"
-#include "query_optimizer/rules/Rule.hpp"
+#include "query_optimizer/rules/BottomUpRule.hpp"
 #include "utility/Macros.hpp"
 
 namespace quickstep {
@@ -54,7 +54,7 @@
  *       of the physical passes (e.g. ReuseAggregateExpressions) to be finalized
  *       before this one to maximize optimization opportunities.
  */
-class ExtractCommonSubexpression : public Rule<physical::Physical> {
+class ExtractCommonSubexpression : public BottomUpRule<physical::Physical> {
  public:
   /**
    * @brief Constructor.
@@ -69,11 +69,10 @@
     return "ExtractCommonSubexpression";
   }
 
-  physical::PhysicalPtr apply(const physical::PhysicalPtr &input) override;
+ protected:
+  physical::PhysicalPtr applyToNode(const physical::PhysicalPtr &input) override;
 
  private:
-  physical::PhysicalPtr applyInternal(const physical::PhysicalPtr &input);
-
   struct ScalarHash {
     inline std::size_t operator()(const expressions::ScalarPtr &scalar) const {
       return scalar->hash();