理解 DuckDB 的逻辑计划(Logical Plan)、优化器(Optimizer)和物理执行计划模块的工作流程
✅ 一、用于调试的 SQL 语句建议
✅ 1. 创建 orders 表的 SQL
CREATE TABLE orders (
order_id INTEGER,
customer_id INTEGER,
order_date DATE,
amount DOUBLE
);
✅ 2. 插入测试数据(模拟多个客户在不同时间下单)
INSERT INTO orders VALUES
-- 客户1:7个订单,部分在2022年之后
(1, 101, '2022-01-02', 120.0),
(2, 101, '2022-02-15', 80.0),
(3, 101, '2022-03-01', 90.0),
(4, 101, '2022-04-10', 60.0),
(5, 101, '2022-06-12', 70.0),
(6, 101, '2022-07-20', 30.0),
(7, 101, '2022-08-05', 40.0),
-- 客户2:4个订单,不满足 HAVING 条件
(8, 102, '2022-02-01', 200.0),
(9, 102, '2022-03-01', 180.0),
(10, 102, '2022-03-15', 150.0),
(11, 102, '2022-03-20', 220.0),
-- 客户3:6个订单,满足 HAVING 条件
(12, 103, '2022-01-05', 95.0),
(13, 103, '2022-01-15', 85.0),
(14, 103, '2022-02-10', 90.0),
(15, 103, '2022-02-20', 88.0),
(16, 103, '2022-03-01', 77.0),
(17, 103, '2022-03-10', 66.0),
-- 客户4:5个订单,刚好不满足 COUNT(*) > 5
(18, 104, '2022-04-01', 50.0),
(19, 104, '2022-04-05', 45.0),
(20, 104, '2022-04-10', 60.0),
(21, 104, '2022-04-15', 70.0),
(22, 104, '2022-04-20', 80.0);
✅ 3. 执行你的分析查询 SQL(统计订单数大于 5 的客户)
我们需要选择一个能覆盖典型逻辑运算的 SQL 语句,例如选择、投影、连接、聚合等。
sql
-- 推荐用于逻辑计划和优化器调试的 SQL
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE order_date >= DATE '2022-01-01'
GROUP BY customer_id
HAVING COUNT(*) > 5
ORDER BY order_count DESC
LIMIT 10;
这个 SQL 涵盖以下关键节点:
-
Filter(WHERE)
-
Group By / Aggregation
-
Having
-
Projection
-
Order By + Limit
✅ 二、推荐的 GDB 调试设置流程
(gdb) b Parser::ParseQuery
Breakpoint 1 at 0x589200
(gdb) b Binder::Bind
Breakpoint 2 at 0xa8ff40 (40 locations)
(gdb) b Planner::CreatePlan
Breakpoint 3 at 0xb1da90 (2 locations)
(gdb) b Optimizer::Optimize
Breakpoint 4 at 0xa2ae90
在上面的4处设置断点。
✅ 三、查看逻辑计划过程
主要的过程就是参考https://github.com/duckdb/duckdb/blob/main/src/main/client_context.cpp
文件里面的353行的ClientContext::CreatePreparedStatementInternal函数,里面记录了执行的过程,从1️⃣ Create Logical Plan->2️⃣ 优化逻辑计划->3️⃣ 构建物理计划->4️⃣ 初始化执行器->5️⃣ 开始执行。
逻辑计划的执行入口在/src/planner/planner.cpp的32行的Planner::CreatePlan函数内,首先查看duckdb逻辑计划构建阶段的入口。
Planner::CreatePlan函数
首先进入到同名的Planner::CreatePlan上面选择对应的statement->type类型,比如我这里选择的就是select类型。
Planner::CreatePlan函数地址:https://github.com/duckdb/duckdb/tree/main/src/planner 32行
在了解这个函数之前,先去了解逻辑计划的基类一下class LogicalOperator
类,简单图示:
class LogicalOperator
├── LogicalOperatorType type ← 节点类型(如 JOIN、GET、FILTER)
├── vector<LogicalOperator*> children← 子节点(树结构)
├── vector<Expression*> expressions ← 节点内表达式(如 WHERE 条件、SELECT 列)
├── vector<LogicalType> types ← 返回列类型
├── idx_t estimated_cardinality ← 行数估计
├── virtual ResolveTypes() = 0 ← 推断返回类型(由子类实现)
├── ToString()/Print() ← 输出逻辑计划树
└── Cast<T>() ← 类型安全的子类转换
- ①
LogicalOperatorType type
枚举类型,表示逻辑节点的类别:这个字段可以让优化器、渲染器、执行器知道当前是哪个操作。 - ② vector<unique_ptr> children
当前逻辑节点的子节点,构成逻辑计划树。
比如:一个 LogicalFilter 会有一个子节点是 LogicalGet。 - ③ vector<unique_ptr> expressions
表达式节点,比如:
WHERE x > 10 → x > 10 是一个表达式
SELECT x + y → x + y 是一个表达式
对于某些逻辑节点(如 LogicalProjection),表达式就是它的输出列表达式。 - ④ vector types
当前逻辑节点最终会输出哪些列的类型。
是在 ResolveOperatorTypes() 或 ResolveTypes() 中由具体逻辑算子设置。 - ⑤ estimated_cardinality 和 has_estimated_cardinality
用于估算输出行数,供优化器做选择(如连接顺序) - ⑥ ResolveOperatorTypes() + ResolveTypes()
比如:LogicalProjection 会根据表达式类型推导出列类型
LogicalAggregate 会根据聚合函数决定返回类型 - ⑦ ToString() 和 Print()
用于调试或 explain 时打印逻辑计划结构 - ⑧ Copy(), Serialize(), Cast() 等
提供计划节点的复制、序列化能力
Cast() 用于安全地将 LogicalOperator 转为具体类型(如 LogicalJoin)
🧠 总结一句话
LogicalOperator
是 DuckDB 中逻辑执行计划的树节点基类&#