Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 24

Optimizing Queries with Logical Transformations

Recommended resources

Hello! Welcome back to our module on Query Optimization.

In our last lesson, you learned how a declarative SQL query is translated into a procedural logical plan based on relational algebra. We saw that the initial "canonical" translation often results in a highly inefficient plan, for example, by creating massive Cartesian products. This sets the stage for today's topic: how does the database systematically improve this initial plan?

Today's learning outcome is to apply logical query transformations, such as predicate pushdown and join reordering. We will explore a set of powerful, rule-based techniques that an optimizer uses to rewrite a logical plan into a logically equivalent—but far more efficient—form. These transformations are the first and most crucial step in turning a naive plan into a high-performance execution strategy.

The Philosophy of Rule-Based Optimization

Before diving into specific rules, let's establish the overall philosophy. The optimizer takes an initial logical plan (represented as a tree of relational algebra operators) and applies a series of equivalence rules to transform it. An equivalence rule allows us to replace one expression with another that produces the exact same result.

For example, σ_{p1 AND p2}(R) is equivalent to σ_{p1}(σ_{p2}(R)).

This process is often called heuristic optimization or rule-based optimization because the rules are applied based on heuristics that are almost always beneficial, without yet needing to calculate the precise "cost" of each alternative. The primary goal is to reduce the amount of data that flows between operators as early as possible.

To see how a database system approaches this, let's get an overview of the process.

Database Systems: Query Optimization (Part 1), rule- vs cost-based, join graph, interesting orders

Professor Jens Dittrich provides a clear walkthrough of how a SQL query is translated and then rewritten using a series of logical rules. This will frame our entire discussion for today.

Please watch the segment from 06:10 to 13:10. As you watch, note the key rewrite steps: Breaking up predicates: A conjunctive WHERE clause is split into separate select operators. Pushing down selections: The select operators are moved down the tree to apply filters as early as possible. Rewriting cross products: A cross product followed by a selection is transformed into a more efficient join operator.

As you saw, a few simple rewrites can dramatically change the structure of the query plan, making it look much more sensible. Now, let's explore the most important of these rules in more detail.


Predicate Pushdown: The Golden Rule

If there's one transformation to remember, it's predicate pushdown. The principle is simple: perform selections (filters) as early as possible. By filtering out rows you don't need before they enter expensive operations like joins, you can drastically reduce the amount of work the database has to do.

Query Rewriting: Predicate Pushdown
This diagram from a lecture at the University of Wisconsin-Madison visualizes predicate pushdown. The optimized plan on the right filters the `Reserves` and `Sailors` tables *before* the join, significantly reducing the size of the join inputs.

The performance impact of this single heuristic can be enormous.

#15 - Query Planning & Optimization (CMU Intro to Database Systems)

Let's watch a segment from the CMU lecture that quantifies the impact of these transformations. You'll see the I/O cost drop from millions to dozens just by applying these rules.

Watch the segment from 03:13 to 08:08 and then the key part from 09:34 to 11:28. Pay close attention to how the I/O cost estimate plummets when the optimizer stops doing a Cartesian product and pushes the filter on the 'department' table down below the join.

Predicate Pushdown in Practice

While the concept is simple, the implementation has important nuances, especially concerning different join types. Since you're interested in systems internals, let's look at how a real system like StarRocks applies these rules.

Inside StarRocks: Why Joins Are Faster Than You'd Expect

This technical blog from the developers of StarRocks provides excellent, practical detail on how predicate pushdown is implemented.

Please read Section 2.2, "Predicate Pushdown". Focus on the distinction it makes: For INNER JOINs: Predicates in both the WHERE clause and the ON clause can generally be pushed down to either side of the join. For OUTER JOINs: The rules are much stricter. For a LEFT OUTER JOIN, you cannot push down a predicate to the left (preserved) table, as that would incorrectly filter rows that should have been preserved in the output. You can only push predicates down to the right (non-preserved) table.

This distinction is a great example of an engineering trade-off. The optimizer applies a powerful general heuristic (push down predicates) but must do so carefully to preserve the exact semantics of the SQL query.


Join Reordering: Finding the Best Path

For queries that join multiple tables, the order in which the joins are performed is another huge factor in performance. The goal is to join tables in an order that keeps the intermediate results as small as possible. This typically means performing the most selective joins (those that produce the fewest rows) first.

Consider a join between tables A, B, and C. A logical plan can execute this as (A ⋈ B) ⋈ C or A ⋈ (B ⋈ C). These are logically equivalent due to the associativity of joins. Furthermore, A ⋈ B is equivalent to B ⋈ A (join commutativity). Together, these properties open up a large search space of possible join orders.

For an N-table join, the number of possible join orders can be very large (related to Catalan numbers). Exhaustively checking every single one is often infeasible. Therefore, optimizers use heuristics and algorithms to explore this space.

From a systems perspective, a common approach is to identify a sequence of INNER JOINs and treat them as a single "multi-join" block to be reordered.

Inside StarRocks: Why Joins Are Faster Than You'd Expect

Let's return to the StarRocks blog to see how join reordering is handled in a real-world system.

Please read the introduction to Section 3, "Join Reordering", and skim the sections on Exhaustive and Greedy algorithms (3.1 and 3.2). Notice the practical approach: the system groups consecutive Inner or Cross joins into a Multi Join Node and then applies reordering algorithms within that node.

A Better Approach: The Join Graph

In the last lesson, we discussed translating SQL into a "canonical" tree with cross products at the bottom. We then saw how rule-based optimization works hard to transform this tree into something better. This raises a design question: is starting with that canonical tree the best idea in the first place?

Modern optimizers often take a more direct approach. Instead of building an initial tree, they parse the SQL query directly into a join graph, where relations are nodes and join predicates are edges. This representation avoids creating artificial problems (like large cross products) that then need to be fixed.

Database Systems: Query Optimization (Part 1), rule- vs cost-based, join graph, interesting orders

Professor Dittrich makes a compelling case for using a join graph from the start. This is a key insight into modern optimizer design.

Please watch the segment from 59:03 to 1:07:30. This is a longer segment, but it's full of valuable insights. Focus on: How a SQL query is translated directly into a join graph. The argument that this avoids the "artificial problems" created by the canonical tree-based translation. How dynamic programming can then be used to traverse this graph and find an optimal join order by building up plans from size 1 to size N.

This shift from a rigid tree to a more flexible graph representation is a powerful design principle. It allows the optimizer to focus directly on the hardest part of the problem—finding the best join order—without being constrained by an arbitrary initial plan structure.


Other Important Logical Transformations

While predicate pushdown and join reordering are the heavy hitters, several other logical transformations are crucial for a complete optimizer.

  • Projection Pushdown: Similar to predicate pushdown, this rule pushes projection (π) operators down the tree. This makes tuples "narrower" by discarding unused columns early, which can save significant I/O and memory, especially in a row-oriented system.
  • Subquery Decorrelation: Correlated subqueries (where the inner query depends on the outer query) can be very inefficient if executed naively (i.e., re-running the subquery for every outer row). Decorrelation is a set of rewrite rules that transform a correlated subquery into an equivalent JOIN. This allows the subquery to be executed once and enables it to participate in join reordering with the other tables.

Rule-based Optimization - TiDB Development Guide

The TiDB development guide gives a great, code-level view of how decorrelation is implemented.

Skim Section 'Decorrelation'. You don't need to understand every detail, but appreciate that there are specific rules to transform an Apply operator (representing a subquery) into a standard Join operator, enabling further optimization.

Test your understanding!

Consider the following query, which finds the names of sailors who have reserved a red boat and are over 30 years old.

SELECT S.sname
FROM Sailors S, Reserves R, Boats B
WHERE S.sid = R.sid AND R.bid = B.bid
  AND B.color = 'red' AND S.age > 30;

Assume the tables have the following sizes:

  • Sailors: 100,000 rows
  • Reserves: 1,000,000 rows
  • Boats: 1,000 rows (and only 50 are red)

Describe a sequence of logical transformations you would apply to optimize this query, and explain your reasoning for the join order you choose.

Show answer

An optimal sequence of logical transformations would be:

  1. Start with the Canonical Plan: The initial (inefficient) plan is π_sname ( σ_conditions (Sailors × Reserves × Boats) ).

  2. Apply Predicate Pushdown: Push the filter conditions as low as possible in the tree.

    • σ_(B.color = 'red') is pushed down to the Boats table.
    • σ_(S.age > 30) is pushed down to the Sailors table.
  3. Convert Cross Products to Joins: The cross products followed by selections on join keys are converted into explicit join operators. The plan now looks something like: π_sname ( (σ_age(Sailors) ⋈ σ_color(Boats)) ⋈ Reserves ) with the join conditions applied correctly.

  4. Apply Join Reordering: Now we must choose the best join order. The goal is to minimize the size of intermediate results.

    • Applying σ_(B.color = 'red') to Boats reduces its size from 1,000 to just 50 rows. This is a highly selective predicate.
    • Let's assume S.age > 30 is moderately selective, maybe reducing Sailors from 100,000 to 50,000 rows.
    • The best strategy is to start with the smallest inputs. A good join order would be:
      1. Join the filtered Boats table (50 rows) with the Reserves table (1,000,000 rows) on R.bid = B.bid. This intermediate result will be much smaller than the full Reserves table, as it only contains reservations for red boats.
      2. Join that small intermediate result with the filtered Sailors table on S.sid = R.sid.

    The resulting optimized logical plan would be structured as π_sname ( σ_age(Sailors) ⋈ (Reserves ⋈ σ_color(Boats)) ). This order performs the most selective operations first, ensuring that the most expensive join operates on the smallest possible inputs.


Conclusion

In this lesson, you've learned how a query optimizer uses logical transformations to rewrite a query plan. These rule-based heuristics are the first line of defense against inefficient query execution.

Key Takeaways:

  • Logical optimization rewrites a query plan using equivalence rules to find a more efficient, but semantically identical, plan.
  • Predicate Pushdown is the most important heuristic: filter data as early as possible to reduce the size of intermediate results.
  • Join Reordering is critical for multi-table queries. The goal is to find an order that minimizes the size of the data being joined at each step.
  • Modern optimizers often bypass the classic "canonical plan" by translating SQL directly into a join graph, which provides a more flexible structure for optimization.
  • Other rules like projection pushdown and subquery decorrelation are also essential tools in the optimizer's toolkit.

Preview of the Next Lesson

So far, our rules have been based on heuristics (e.g., "pushing selections is good"). But to choose between two plausible plans (like A ⋈ B vs. B ⋈ A), the optimizer needs more information. It needs to estimate the cost of each alternative. In the next lesson, you will learn how to estimate the cardinality of query results using statistics and selectivity, which is the foundation of cost-based optimization.