|
1 | 1 | ## Optimizer Overview
|
2 | 2 |
|
3 |
| -The optimizer follows the cascade framework, it takes an annotated parse tree as input, goes through multiple optimization phases, and output an execution plan. The major phases are: |
4 |
| - |
5 |
| -* Parse tree to logical operator tree transformation, which turns a parse tree into a logical operator tree. |
| 3 | +The optimizer follows the [Cascade](http://15721.courses.cs.cmu.edu/spring2018/papers/15-optimizer1/graefe-ieee1995.pdf) framework, it takes an annotated parse tree as input, goes through multiple optimization phases, and output an execution plan. The major phases are: |
| 4 | +* Parse tree to logical operator tree transformation. |
6 | 5 | * Predicate push-down, which pushes predicates to the lowest possible operator to evaluate.
|
7 |
| -* Unnesting, which turns sub-queries with corelation into regular join operator. |
8 |
| -* Logical transformation, which enumerates all possible join order. |
9 |
| -* Stats derivation, which derives stats needed to compute cost for each group. |
10 |
| -* Phyisical implementation, which enumerates all possible implementation for a logical operator, e.g. hash join v.s. nested-loop join |
11 |
| -* Property enforcing, which adds missing properties descirbing the output data's format, e.g. sort order. |
| 6 | +* Unnesting, which turns arbitary correlated subqeries into logical join operator. |
| 7 | +* Logical transformation, which enumerates all possible join orders. |
| 8 | +* Stats derivation, which derives stats needed to compute the cost for each group. |
| 9 | +* Phyisical implementation, which enumerates all possible implementation for a logical operator and cost them, e.g. hash join v.s. nested-loop join |
| 10 | +* Property enforcing, which adds missing properties descirbing the output format, e.g. sort order. |
12 | 11 | * Operator to plan transformation, which turns the best physical operator tree into an execution plan.
|
13 | 12 |
|
14 |
| -Predicate push-down and unnesting are rewrite phases, they will run independently. |
| 13 | +The rewrite phase consists of predicate push-down and unnesting, they will run once separately before later optimization phases. Transformation, stats derivation, physical implementation and property enforcing will not be separated, an new operator generated from a transformation rule is going to be implemented as a physical operator and cost immediately, this allows us to do pruning based on the cost to avoid enumerating inefficient plans. |
| 14 | + |
| 15 | +The entrance of the optimizer is `Optimizer::BuildPelotonPlanTree()` in [`optimizer.cpp`](https://github.com/chenboy/peloton/blob/optimizer_doc/src/optimizer/optimizer.cpp). We'll cover each phase in the following sections. |
| 16 | + |
| 17 | +## Parse Tree Transformation |
| 18 | + |
| 19 | +The first step in the optimizer is to transform a peloton parse tree into a logical operator tree which follows relational algreba. This is implemented in [`query_to_operator_transformer.cpp`](https://github.com/chenboy/peloton/blob/optimizer_doc/src/optimizer/query_to_operator_transformer.cpp). Most of the transformations are trivial since the parse tree is mostly structurally similar to a relational algreba tree. There are two things worth mentioning: |
| 20 | +* First, we extract conjunction expressions into a vector of expressions, annotate them with the table aliases occurred in the expression, and uses a separate predicate operators to store them. This will allow us to implement rule-based predicate push-down much easier. |
| 21 | +* Second, we transform correlated subqueries into `dependent join` and `mark join` intrudoced in [this paper](http://btw2017.informatik.uni-stuttgart.de/slidesandpapers/F1-10-37/paper_web.pdf) from Hyper, in the unnesting phase we'll transform dependent join into regular join operators. |
| 22 | + |
| 23 | +## Rewrite Phase |
| 24 | + |
| 25 | +The rewrite phase includes heuristic-based optimization passes that will be run once. For extensibility, we implement these passes as rule-based optimization. These passes assume the transformed tree is always better than the original tree so when a rule is applied, the new pattern will always replace the old one. For people who want to add new rewrite passes, they should specify a set of rules, pick a rewrite framework from top-down rewrite and bottom-up rewrite and push the rewrite task to the task queue. |
| 26 | + |
| 27 | +Predicate push-down is a top-down rewrite pass, what it does is to push predicate throgh operators and when the predicate cannot be further push-down, the predicate is combined with the underlying operator. |
| 28 | + |
| 29 | +Unnesting is a bottom-up pass that eliminates dependent join. It uses a bunch of techniques mentioned in Patrick's report. |
| 30 | + |
| 31 | +## Cascade Style Optimization |
| 32 | + |
| 33 | +After the rewrite phase we'll get a logical operator tree, the next step is to feed the logical operator tree into a Cascade style query optimizer to generate the lowest cost operator tree. The implementation basically follows the [Columbia Optimizer paper](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.54.1153&rep=rep1&type=pdf), we'll add more details in the documentation, for now please just take the paper as reference. The tasks are implemented in [`optimizer_task.cpp`](https://github.com/chenboy/peloton/blob/optimizer_doc/src/optimizer/optimizer_task.cpp). |
| 34 | + |
| 35 | +There's one task `DeriveStats` that is not mentioned in the Columnbia paper. We follow the [Orca paper](http://15721.courses.cs.cmu.edu/spring2018/papers/15-optimizer1/p337-soliman.pdf) to derive stats for each group on the fly. When a new group is generated, we'll recursively collect stats for the column used in the root operator's predicate (When a new group is generated, there's only one expression in the group, thus only one root operator), compute stats for the root group and cache those stats in the group so that we only derive stats for columns we need, which is efficient for both time and space. |
| 36 | + |
| 37 | +The design of property enforcing also follows Orca rather than Columbia. We'll add enforcer after applying physical rules and the enforcer will be stored separately from regular physical operators. |
| 38 | + |
| 39 | +## Operator to plan transformation |
| 40 | + |
| 41 | +When all the optimizations are done, we'll pick the lowest cost operator tree and use it to generate an execution plan. |
| 42 | + |
| 43 | +We'll first derive the output column, which are a vector of expressions, for the root group based on the query's select list, then we'll recursively derive output columns for each operator (implemented in [`input_column_deriver.cpp`](https://github.com/chenboy/peloton/blob/optimizer_doc/src/optimizer/input_column_deriver.cpp)) based on columns used in each operator. At last, we generate peloton plan node using operators in the best operator tree and the input/output column pairs for these operators. What we do here is basically setting column offset for the plan nodes, e.g. output column offset, sort column's offset and column offset in the predicates (implemented in [`plan_generator.cpp`](https://github.com/chenboy/peloton/blob/optimizer_doc/src/optimizer/plan_generator.cpp)). |
| 44 | + |
| 45 | +## WIP |
| 46 | + |
| 47 | +There are still a lot of interesting work needed to be implemented, including: |
| 48 | +* Join order enumeration. |
| 49 | +* Expression rewrite, my current thought is it should be done in the binder after annotating expressions or in the optimizer before predicate push-down. |
| 50 | +* Implement sampling-based stats derivation and cost calculation |
| 51 | +* Support unnesting arbitary queries so that we can support a wider range of queries in TPC-H. This would need the codegen engine to support `semi join`, `anti-semi join`, `mark join`, `single join`. |
| 52 | +* More documentations. |
0 commit comments