Skip to content

Commit e366aac

Browse files
committed
feat(optimizer): Add RBO to the cascade framework
1 parent 3ed9f5e commit e366aac

File tree

20 files changed

+440
-163
lines changed

20 files changed

+440
-163
lines changed

.github/workflows/build-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ permissions:
2424

2525
jobs:
2626
init-ubuntu:
27+
if: github.event.pull_request.draft == false
2728
runs-on: ubuntu-latest
2829
name: init-ubuntu
2930
outputs:
@@ -359,6 +360,7 @@ jobs:
359360
bash ./miniob_test_docker_entry.sh
360361
python3 ./libminiob_test.py -c conf.ini
361362
build-on-mac:
363+
if: github.event.pull_request.draft == false
362364
runs-on: macos-latest
363365
name: build-macos
364366

.github/workflows/clang-format.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- 'CONTRIBUTING.md'
1313
jobs:
1414
clang-format-checking:
15+
if: github.event.pull_request.draft == false
1516
runs-on: ubuntu-latest
1617
steps:
1718
- uses: actions/checkout@v4

docs/docs/design/miniob-cascade.md

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Cascade Optimizer(查询优化器)
66

77
优化器(Optimizer)是数据库中用于把逻辑计划转换成物理执行计划的核心组件,很大程度上决定了一个系统的性能。查询优化的本质就是对于给定的查询,找到该查询对应的正确物理执行计划并且是最低的"代价(cost)"。
88

9-
在 MiniOB 中,查询优化器目前将优化分为两个阶段(基于规则的优化和基于代价的优化)。本文主要介绍基于代价的查询优化器实现(Cascade Optimizer),其代码实现主要位于 `src/observer/optimizer/cascade`
9+
在 MiniOB 中,查询优化器目前支持两种优化模式:基于规则的优化(RBO)和基于代价的优化(CBO)。两者都统一在 Cascade Optimizer 框架中实现,通过 `use_cascade` 配置项来选择使用哪种模式。本文主要介绍 Cascade Optimizer 的实现,其代码实现主要位于 `src/observer/sql/optimizer/cascade`
1010

1111
## 基本概念
1212

@@ -18,20 +18,31 @@ title: Cascade Optimizer(查询优化器)
1818
这里的Expression(表达式)表示一个带有零或多个Input Expression(输入表达式)的Operator,同样可以分为Logical Expression(逻辑表达式)和 Physical Expression(物理表达式)。
1919
例如对于 TableScan 算子来说,TableScan 算子本身就可以理解为是一个 Expression(不带输入表达式);对于 Join 算子来说,Join 算子以及其子节点一起构成一个 Expression。
2020

21-
MiniOB 当前的实现中为每个 `OperatorNode` 增加了一个成员变量 `vector<OperatorNode*> general_children_;`,可以认为这里的 Expression 也对应到 MiniOB 中的 `OperatorNode`
21+
MiniOB 的 Cascade Optimizer 中,Expression 通过 `GroupExpr`(M_EXPR)来表示。`GroupExpr` 包含一个 `OperatorNode`(算子)和一组子 Group 的 ID(`child_group_ids`),而不是直接存储子 OperatorNode。这种设计使得多个等价的 Expression 可以共享相同的子 Group,从而避免重复计算
2222

2323
### Group
2424
Group 是一组逻辑等效的逻辑和物理表达式,产生相同的输出。
2525
例如对于 `SELECT * FROM A JOIN B ON A.id = B.id JOIN C ON C.id = A.id;` 来说,其输出结果为 A,B,C 三个表根据 Join 条件进行 Join 的结果。逻辑表达式就可能包括 `(A Join B) Join C``A Join (B Join C)`;物理表达式就可能包括`(A Hash Join B) Nested Loop Join C``(A Hash Join B) Hash Join C`等。
2626
在 MiniOB 中,Group 对应的实现位于 `src/observer/sql/optimizer/cascade/group.h`
2727

28-
### M_EXPR
29-
M_EXPR 是 Expression(表达式) 的一种紧凑形式。 输入是 Group 而不是 Expression。因此可以理解为, M_EXPR 包含多个 Expression。在 Cascade 中,所有搜索都是通过 M_EXPR 完成的。
30-
在 MiniOB 中,M_EXPR 对应的实现位于 `src/observer/sql/optimizer/cascade/group_expr.h`
28+
### M_EXPR (GroupExpr)
29+
M_EXPR(在 MiniOB 中实现为 `GroupExpr`)是 Expression(表达式)的一种紧凑形式。M_EXPR 包含一个 Operator(算子)和一组子 Group 的 ID,而不是直接包含子 Expression。这种设计的优势在于:
30+
- **共享子表达式**:多个等价的 Expression 可以共享相同的子 Group,避免重复计算
31+
- **紧凑表示**:通过 Group ID 引用子节点,而不是直接存储子节点,节省内存
32+
- **等价性检测**:通过 Group 机制,可以自动识别等价的表达式
33+
34+
在 Cascade 中,所有搜索都是通过 M_EXPR 完成的。在 MiniOB 中,M_EXPR 对应的实现位于 `src/observer/sql/optimizer/cascade/group_expr.h`
3135

3236
### Rule(规则)
3337
规则是将 Expression 转换为逻辑等价表达式。包含了Transformation Rule 和 Implementation Rule。其中,Transformation Rule 是将逻辑表达式转换为逻辑表达式;Implementation Rule 是将一个逻辑表达式转换为物理表达式。
34-
在 MiniOB 中,Rule 对应的实现位于 `src/observer/sql/optimizer/cascade/rules.h`,目前 MiniOB 只实现了 Implementation Rule。
38+
39+
**规则应用策略**:在 MiniOB 的 Cascade Optimizer 中,规则应用采用**全量应用**策略:
40+
- **CBO 模式**:收集所有匹配的规则,为每个规则创建 `APPLY_RULE` task,所有匹配的规则都会被应用
41+
- **RBO 模式**:遍历所有规则,对每个匹配的规则都进行应用,直到没有新的逻辑表达式生成
42+
43+
这种全量应用策略确保了所有可能的优化机会都被探索,从而生成更多的候选计划(CBO)或应用更多的优化规则(RBO)。
44+
45+
在 MiniOB 中,Rule 对应的实现位于 `src/observer/sql/optimizer/cascade/rules.h`。目前 MiniOB 已经实现了 Transformation Rule(如谓词下推、谓词重写、表达式简化)和 Implementation Rule(如逻辑算子到物理算子的转换)。
3546

3647
### Memo(在 Columbia 中也叫做 Search Space)
3748
Memo 用于存放查询优化过程中所有枚举的计划的数据结构,利用 Memo 来避免生成重复的计划生成。
@@ -49,7 +60,10 @@ while task_list is not empty
4960
perform task
5061
```
5162

52-
在cascade(columbia)中,task共分为5种, 其整体调度的流程图如下:
63+
在 MiniOB 的 Cascade Optimizer 中,task 共分为 6 种,根据优化模式(RBO 或 CBO)使用不同的 task:
64+
65+
**CBO 模式(Cost-Based Optimization)**
66+
使用异步任务调度,task 调度的流程图如下:
5367
```
5468
optimize()
5569
@@ -75,27 +89,55 @@ while task_list is not empty
7589
│ │
7690
└─────────────┘
7791
```
78-
* O_GROUP:根据优化目标,对一个 Group 进行优化,即遍历 Group 中的每个 M_EXPR,为logical M_EXPR 生成一个 O_EXPR task,为 physical M_EXPR 生成一个 O_INPUTS task。
7992

80-
* O_EXPR:对一个logical M_EXPR 进行优化,对于该 logical M_EXPR,如果存在可以应用的规则,则生成一个 APPLY_RULE task。
93+
**RBO 模式(Rule-Based Optimization)**
94+
使用同步递归,通过 `OPTIMIZE_RBO_GROUP` task 直接应用规则。
95+
96+
### Task 类型说明
97+
98+
* **O_GROUP**(CBO):根据优化目标,对一个 Group 进行优化,即遍历 Group 中的每个 M_EXPR,为 logical M_EXPR 生成一个 O_EXPR task,为 physical M_EXPR 生成一个 O_INPUTS task。
99+
100+
* **O_EXPR**(CBO):对一个 logical M_EXPR 进行优化,对于该 logical M_EXPR,如果存在可以应用的规则,则生成一个 APPLY_RULE task。
81101

82-
* E_GROUP:为 Group 中的每一个 logical M_EXPR 生成一个 O_EXPR task。
102+
* **E_GROUP**(CBO):为 Group 中的每一个 logical M_EXPR 生成一个 O_EXPR task。
83103

84-
* APPLY_RULE:应用具体的优化规则,从逻辑表达式转换到逻辑表达式,或者从逻辑表达式转换为物理表达式。如果产生逻辑表达式,则生成一个 O_EXPR task;如果产生物理表达式,则生成一个 O_INPUTS task。
104+
* **APPLY_RULE**(CBO):应用具体的优化规则,从逻辑表达式转换到逻辑表达式,或者从逻辑表达式转换为物理表达式。如果产生逻辑表达式,则生成一个 O_EXPR task;如果产生物理表达式,则生成一个 O_INPUTS task。
85105

86-
* O_INPUTS:对物理表达式的代价进行计算,在此过程中需要递归地计算子节点的代价。
106+
* **O_INPUTS**(CBO):对物理表达式的代价进行计算,在此过程中需要递归地计算子节点的代价。
87107

88-
在 MiniOB 中,5 种类型的 task 位于 `src/observer/sql/optimizer/cascade/tasks` 目录下,
89-
Cascade Optimizer 的入口函数为 `src/observer/sql/optimizer/cascade/optimizer.h::Optimizer::optimize`
108+
* **OPTIMIZE_RBO_GROUP**(RBO):使用同步递归的方式,对 Group 应用 transformation rules 和 implementation rules。先应用 transformation rules 生成逻辑表达式,然后应用 implementation rules 生成物理表达式。不进行代价计算,只保留最后一个(最变换的)物理表达式。未来会跳过需要代价估算的规则(如 IndexScan、HashJoin 等)。
109+
110+
在 MiniOB 中,所有 task 类型位于 `src/observer/sql/optimizer/cascade/tasks` 目录下,Cascade Optimizer 的入口函数为 `src/observer/sql/optimizer/cascade/optimizer.h::Optimizer::optimize`
111+
112+
### RBO 与 CBO 的区别
113+
114+
| 特性 | RBO | CBO |
115+
|------|-----|-----|
116+
| 任务调度 | 同步递归 | 异步任务调度 |
117+
| 执行模式 | 先应用规则,再按需优化新节点 | 先应用规则,再按需优化新节点 |
118+
| 候选探索 | 只保留最后一个(最变换的)逻辑/物理表达式 | 探索多个候选计划 |
119+
| 代价计算 | 不计算代价 | 计算代价,选择最优 |
120+
| 适用场景 | 简单查询,快速优化 | 复杂查询,需要精确代价估算 |
121+
122+
两者都使用相同的规则集(Transformation Rules 和 Implementation Rules)。目前 RBO 还没有区分哪些规则需要代价计算,但未来可能跳过依赖代价估算的规则(如 IndexScan、HashJoin 等),因为 RBO 不进行代价计算,无法判断这些规则生成的计划是否更优。
90123

91124
## 如何为 Cascade 添加新的算子转换规则
92125

93126
1. 添加逻辑算子和物理算子的定义,可参考`src/observer/sql/operator/table_get_logical_operator.h``src/observer/sql/operator/table_scan_physical_operator.h`
94127
2. 添加逻辑算子到物理算子的转换规则,可参考`src/observer/sql/optimizer/implementation_rules.h::LogicalGetToPhysicalSeqScan`
95128
3.`src/observer/sql/optimizer/rules.h` 中的 `RuleSet` 中注册相应的转换规则。
96129

130+
## 优化模式选择
131+
132+
MiniOB 通过 `use_cascade` 配置项来选择优化模式:
133+
- `use_cascade = true`:使用 CBO 模式,进行代价估算并选择最优计划
134+
- `use_cascade = false`:使用 RBO 模式,按规则顺序应用,不进行代价估算
135+
136+
两种模式都统一在 Cascade Optimizer 框架中,使用相同的规则集和 Memo 结构,只是执行方式和候选选择策略不同。
137+
97138
## WIP
98-
1. 将现有的基于规则的逻辑计划到逻辑计划的转换加入到 cascade optimizer 中。
99-
2. 实现 Apply Rule 中的 Expr binding。
100-
3. 实现 property enforce。
101-
4. 统计信息收集更多信息。
139+
1. 实现 Apply Rule 中的 Expr binding。
140+
2. 实现 property enforce。
141+
3. 统计信息收集更多信息。
142+
4. 优化 RBO 模式的规则应用顺序。
143+
5. 在 RBO 模式中区分并跳过需要代价计算的规则(如 IndexScan、HashJoin 等)。

src/observer/net/packet_buffer.h

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ class PacketBuffer
4040
}
4141

4242
// 禁止拷贝
43-
PacketBuffer(const PacketBuffer &) = delete;
43+
PacketBuffer(const PacketBuffer &) = delete;
4444
PacketBuffer &operator=(const PacketBuffer &) = delete;
4545

4646
// 允许移动
4747
PacketBuffer(PacketBuffer &&other) noexcept : capacity_(other.capacity_), data_(other.data_)
4848
{
4949
other.capacity_ = 0;
50-
other.data_ = nullptr;
50+
other.data_ = nullptr;
5151
}
5252

5353
PacketBuffer &operator=(PacketBuffer &&other) noexcept
@@ -56,10 +56,10 @@ class PacketBuffer
5656
if (data_) {
5757
free(data_);
5858
}
59-
capacity_ = other.capacity_;
60-
data_ = other.data_;
59+
capacity_ = other.capacity_;
60+
data_ = other.data_;
6161
other.capacity_ = 0;
62-
other.data_ = nullptr;
62+
other.data_ = nullptr;
6363
}
6464
return *this;
6565
}
@@ -101,4 +101,3 @@ class PacketBuffer
101101
size_t capacity_ = 0;
102102
char *data_ = nullptr;
103103
};
104-

src/observer/sql/executor/help_executor.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ class HelpExecutor
3434
{
3535
const char *strings[] = {"show tables;",
3636
"desc `table name`;",
37-
"create table `table name` (`column name` `column type`, ... [, primary key (`columns`)]) [storage format=`format`];",
37+
"create table `table name` (`column name` `column type`, ... [, primary key (`columns`)]) [storage "
38+
"format=`format`];",
3839
"create index `index name` on `table` (`column`);",
3940
"insert into `table` values(`value1`,`value2`);",
4041
"update `table` set `column`=`value` [where `column`=`value`];",
4142
"delete from `table` [where `column`=`value`];",
42-
"select [ * | `columns` ] from `table` [where `column`=`value`] [group by `columns`];",
43+
"select [ * | `columns` ] from `table` [where `column`=`value`] [group by `columns`];",
4344
"drop table `table name`;",
4445
"drop index `index name` on `table`;",
4546
"set `variable`=`value`;",

src/observer/sql/optimizer/cascade/group.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,17 @@ bool Group::set_expr_cost(GroupExpr *expr, double cost) {
6565
return false;
6666
}
6767

68-
GroupExpr *Group::get_winner() {
68+
GroupExpr *Group::get_winner(bool use_cbo) {
69+
if (!use_cbo) {
70+
// CBO disabled: return the last physical operator
71+
// (the one that has been transformed by the most transformation rules)
72+
if (!physical_expressions_.empty()) {
73+
return physical_expressions_.back();
74+
}
75+
return nullptr;
76+
}
77+
78+
// CBO enabled: return the expression with the lowest cost
6979
return std::get<1>(winner_);
7080
}
7181

src/observer/sql/optimizer/cascade/group.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ class Group
5858
bool set_expr_cost(GroupExpr *expr, double cost);
5959

6060
/**
61-
* @return The expression with the lowest cost.
61+
* @return The expression with the lowest cost (when CBO enabled),
62+
* or the last physical expression (when CBO disabled).
6263
*/
63-
GroupExpr *get_winner();
64+
GroupExpr *get_winner(bool use_cbo = false);
6465

6566
/**
6667
* @brief Gets the logical expressions in the group.

src/observer/sql/optimizer/cascade/optimizer.cpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ See the Mulan PSL v2 for more details. */
1010

1111
#include "sql/optimizer/cascade/optimizer.h"
1212
#include "sql/optimizer/cascade/tasks/o_group_task.h"
13+
#include "sql/optimizer/cascade/tasks/o_rbo_group_task.h"
1314
#include "sql/optimizer/cascade/memo.h"
1415
#include "sql/operator/physical_operator.h"
1516
#include "common/log/log.h"
@@ -41,7 +42,7 @@ RC Optimizer::choose_best_plan(int root_group_id, std::unique_ptr<PhysicalOperat
4142
}
4243

4344
// Choose the best physical plan
44-
auto winner = root_group->get_winner();
45+
auto winner = root_group->get_winner(context_->use_cbo());
4546
if (winner == nullptr) {
4647
LOG_WARN("No winner found in group %d", root_group_id);
4748
return RC::OPTIMIZER_MEMO_INSERT_FAILED;
@@ -67,19 +68,21 @@ RC Optimizer::optimize_loop(int root_group_id)
6768
context_->set_task_pool(task_stack);
6869

6970
Memo &memo = context_->get_memo();
70-
task_stack->push(new OptimizeGroup(memo.get_group_by_id(root_group_id), context_.get()));
71+
if (context_->use_cbo()) {
72+
task_stack->push(new OptimizeGroup(memo.get_group_by_id(root_group_id), context_.get()));
73+
} else {
74+
task_stack->push(new OptimizeRBOGroup(memo.get_group_by_id(root_group_id), context_.get()));
75+
}
7176

7277
return execute_task_stack(task_stack, root_group_id, context_.get());
7378
}
7479

7580
RC Optimizer::execute_task_stack(PendingTasks *task_stack, int root_group_id, OptimizerContext *root_context)
7681
{
7782
RC rc = RC::SUCCESS;
78-
while (!task_stack->empty()) {
83+
while (OB_SUCC(rc) && !task_stack->empty()) {
7984
auto task = task_stack->pop();
80-
if(OB_SUCC(rc)) {
81-
rc = task->perform();
82-
}
85+
rc = task->perform();
8386
delete task;
8487
}
8588
return rc;

src/observer/sql/optimizer/cascade/optimizer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class GroupExpr;
2323
class Optimizer
2424
{
2525
public:
26-
Optimizer() : context_(std::make_unique<OptimizerContext>()) {}
26+
Optimizer(bool enable_cbo) : context_(std::make_unique<OptimizerContext>(enable_cbo)) {}
2727

2828
RC optimize(GroupExpr *root_gexpr, std::unique_ptr<PhysicalOperator> &physical_operator);
2929

src/observer/sql/optimizer/cascade/optimizer_context.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ See the Mulan PSL v2 for more details. */
1212
#include "sql/optimizer/cascade/memo.h"
1313
#include "sql/optimizer/cascade/rules.h"
1414

15-
OptimizerContext::OptimizerContext()
15+
OptimizerContext::OptimizerContext(bool enable_cbo)
1616
: memo_(new Memo()), rule_set_(new RuleSet()), cost_model_(), task_pool_(nullptr),
17-
cost_upper_bound_(std::numeric_limits<double>::max()) {}
17+
cost_upper_bound_(std::numeric_limits<double>::max()), enable_cbo_(enable_cbo) {}
1818

1919
OptimizerContext::~OptimizerContext() {
2020
if (task_pool_ != nullptr) {

0 commit comments

Comments
 (0)