DuckDB 源码分析 - Logical Plan 逻辑计划
版本
- v1.3-ossivalis
背景
DuckDB 是一个面向分析型工作负载的嵌入式数据库,引擎采用 C++ 编写,整体模块划分清晰,源码注释也非常到位,十分适合作为学习 AP 型数据库的入门项目。为了追踪逻辑计划在引擎中的完整生命周期,本文基于 v1.3-ossivalis 分支,编译 Debug 版本并配合 GDB 进行断点调试,便于观察每个阶段的内部状态。
git clone -b v1.3-ossivalis https://github.com/duckdb/duckdb
GEN=ninja make debug -j 8
./duckdb
v1.3.0-dev2068 022f826ddf
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
DSQL Parser
DuckDB 使用 PostgreSQL 的语法解析器来完成词法/语法分析阶段,随后借助 DuckDB 内部的 Transformer 将 PostgreSQL 的解析树转换成 DuckDB 的语法树(Parser::ParseQuery 返回的 ParserResult)。这一阶段的职责仅限于构造 SQL 的抽象语法树(AST),但不涉及语义校验。
在 Parser 段落调试时,可以配合 PRAGMA parser 将 SQL 转换成 JSON,或者直接在 Parser::ParseQuery 附近打断点,观察 statements 向量的增长过程。DuckDB 的 AST 会保留原始 SQL 的很多细节(例如关键字位置、原始字符串)。
Parser parser(db->con->context->GetParserOptions());
/* parser.ParseQuery() 会解析 SQL 语句, 将 PG 格式的 parse tree 转为 DuckDB 的 tree. */
parser.ParseQuery(query);
/* ... */
/* SQL 的 binder, 逻辑计划生成, 调用优化器进行查询计划的优化, 物理计划生成. */
auto pending = db->con->PendingQuery(std::move(statements.back()), false);Binder
DuckDB 选择将绑定(Binding)与逻辑计划的生成耦合在 Planner 模块中完成。与许多传统数据库类似,绑定阶段既是"语义审计员",也是逻辑计划构造的起点,主要负责以下几类工作:
- 校验 SELECT 列是否存在于作用域内;
- 确认 WHERE、GROUP BY、ORDER BY 等子句的字段或表达式是否合法;
- 为表、列、函数等对象分配
Binding信息,便于后续阶段定位到具体的 Catalog 条目。
Binder::Bind 会将 QueryNode 绑定为 BoundQueryNode,同时生成逻辑计划的根节点。调试时可以在此处打断点,观察 BoundStatement::plan 如何从空指针逐步填充:
Binder维护着当前作用域的BindContext,每当遇到子查询或 CTE 时都会临时切换上下文并在返回时合并结果;- 常量表达式会在绑定阶段尝试下折叠,从而避免在逻辑计划阶段重复构造相同的表达式树;
- 如果 SQL 含有参数(
$1、?等),Binder 也会在此阶段推导出参数类型,并写入PreparedStatementData供执行阶段使用。
BoundStatement Binder::Bind(QueryNode &node) {
BoundStatement result;
if (node.type != QueryNodeType::CTE_NODE && // Issue #13850 - Don't auto-materialize if users materialize (for now)
!Optimizer::OptimizerDisabled(context, OptimizerType::MATERIALIZED_CTE) && context.config.enable_optimizer &&
OptimizeCTEs(node)) {
switch (node.type) {
case QueryNodeType::SELECT_NODE:
result = BindWithCTE(node.Cast<SelectNode>());
break;
case QueryNodeType::RECURSIVE_CTE_NODE:
result = BindWithCTE(node.Cast<RecursiveCTENode>());
break;
case QueryNodeType::CTE_NODE:
result = BindWithCTE(node.Cast<CTENode>());
break;
default:
D_ASSERT(node.type == QueryNodeType::SET_OPERATION_NODE);
result = BindWithCTE(node.Cast<SetOperationNode>());
break;
}
} else {
/* 进行 binder 操作. */
auto bound_node = BindNode(node);
result.names = bound_node->names;
result.types = bound_node->types;
/* 逻辑计划生成. */
result.plan = CreatePlan(*bound_node);
}
return result;
}Logical Plan 逻辑计划
逻辑计划由一棵 LogicalOperator 树组成,每个算子节点描述一次逻辑运算(投影、过滤、JOIN、聚合等)。DuckDB 在算子层面贯彻了“结构即语义”的设计:算子类型、子节点列表与表达式集合共同定义了逻辑意图。LogicalOperator 的骨架如下:
class LogicalOperator {
public:
explicit LogicalOperator(LogicalOperatorType type);
LogicalOperator(LogicalOperatorType type, vector<unique_ptr<Expression>> expressions);
virtual ~LogicalOperator();
/* 当前逻辑算子类型. */
LogicalOperatorType type;
/* 子逻辑算子. */
vector<unique_ptr<LogicalOperator>> children;
/* 当前算子持有的表达式. */
vector<unique_ptr<Expression>> expressions;
/* ... */
};为了便于调试,我们构造一张测试表,并插入一些示例数据。这些数据既覆盖了 NULL、数组、唯一约束等场景,也方便在断点状态下观察列裁剪、谓词传播等细节:
在阅读 LogicalOperator 时可以先关注以下几个小技巧:
- 结合
LogicalOperator::ToString输出的树形结构与下文的 ASCII 图,可以快速确认算子嵌套是否符合预期; - 当逻辑计划出现循环依赖或缺失列时,
Verify会立刻报错,因此开发调试阶段推荐始终在 Debug 模式下运行。
D CREATE TABLE test_table (
id INTEGER PRIMARY KEY,
name VARCHAR,
age INTEGER,
is_active BOOLEAN,
created_at TIMESTAMP,
salary DECIMAL(10, 2),
tags TEXT[],
category VARCHAR,
UNIQUE(name, category)
);
D INSERT INTO test_table (id, name, age, is_active, created_at, salary, tags, category)
VALUES
(1, 'Alice', 30, TRUE, '2024-01-01 10:00:00', 50000.00, ARRAY['dev', 'manager'], 'A'),
(2, 'Bob', 25, FALSE, '2024-01-02 11:00:00', 45000.00, ARRAY['designer'], 'B'),
(3, 'Charlie', NULL, TRUE, '2024-01-03 12:00:00', 60000.00, NULL, 'A'),
(4, 'David', 40, TRUE, '2024-01-04 13:00:00', 70000.00, ARRAY['dev', 'lead'], 'C'),
(5, 'Eve', 35, FALSE, '2024-01-05 14:00:00', 55000.00, ARRAY['marketing'], 'B'),
(6, 'Frank', 28, TRUE, '2024-01-06 15:00:00', 48000.00, ARRAY['dev'], 'A'),
(7, 'Grace', 22, TRUE, '2024-01-07 16:00:00', 40000.00, NULL, 'C'),
(8, 'Hank', 33, FALSE, '2024-01-08 17:00:00', 49000.00, ARRAY['designer'], 'B'),
(9, 'Ivy', 27, TRUE, '2024-01-09 18:00:00', 52000.00, ARRAY['dev'], 'A'),
(10, 'Jack', 31, TRUE, '2024-01-10 19:00:00', 58000.00, ARRAY['lead'], 'C');开启 PRAGMA explain_output = 'all' 后可以同时查看逻辑计划与物理计划。如下查询的未优化逻辑计划包含三个算子,我们可以借助它来快速验证逻辑树结构与列绑定是否符合预期:
D PRAGMA explain_output = 'all';
D EXPLAIN SELECT * FROM test_table WHERE age > 30;┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Unoptimized Logical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ Expressions: │
│ id │
│ name │
│ age │
│ is_active │
│ created_at │
│ salary │
│ tags │
│ category │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ Expressions: │
│(age > CAST(30 AS INTEGER))│
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ SEQ_SCAN │
│ ──────────────────── │
│ Table: test_table │
│ Type: Sequential Scan │
└───────────────────────────┘LOGICAL_PROJECTION:对应 SELECT 列表,负责输出最终的列集;LOGICAL_FILTER:表达式为age > 30,对扫描结果做行级过滤;LOGICAL_GET:数据源算子,从test_table顺序扫描数据。
更多算子类型可以参考 logical_operator_type.hpp。
Logical Plan 生成
逻辑计划生成的核心入口是 Binder::CreatePlan(BoundSelectNode &statement)。函数内部按照 SQL 子句的语义顺序构建算子树,整体遵循“自下而上拼装子树、再逐层加壳”的思路:
- FROM 子句:
BoundSelectNode::from_table在绑定阶段已经转换成相应的LogicalGet或其他子查询算子,作为整个算子树的初始根节点。 - SAMPLE:若存在
USING SAMPLE语法,将LogicalSample包裹在当前根节点外层。 - WHERE:使用
PlanFilter将过滤谓词包装为LogicalFilter,并挂在现有根节点之上。 - GROUP BY / 聚合:当存在聚合或分组时,提前遍历子查询并生成
LogicalAggregate;Grouping Sets、Grouping Functions 等高级语法也在此处处理。 - HAVING:再次生成
LogicalFilter,与 WHERE 类似,只是输入来自聚合结果。 - 窗口函数与 QUALIFY:依次生成
LogicalWindow和LogicalFilter,并处理其中可能嵌套的子查询。 - UNNEST:针对 LATERAL 或 UNNEST 场景,构造
LogicalUnnest。 - SELECT 列表与投影:遍历
select_list处理子查询后,创建LogicalProjection作为新的根节点。 - ORDER BY / LIMIT / DISTINCT:调用
VisitQueryNode追加排序、去重、限制行数等算子。 - 列裁剪:若
need_prune标记为 True,插入额外的LogicalProjection以仅保留需要的列。
对应源码片段如下 (省略与主题无关的细节):
unique_ptr<LogicalOperator> Binder::CreatePlan(BoundSelectNode &statement) {
unique_ptr<LogicalOperator> root;
D_ASSERT(statement.from_table);
/* 在通用查询中,物理存储表的 LogicalOperator 是 LogicalOperatorType::LOGICAL_GET,
* LOGICAL_GET 会在 SELECT 语句 binder 阶段就被生成. */
root = CreatePlan(*statement.from_table);
D_ASSERT(root);
/* 处理 samples 语句: DuckDB 支持采样语法
* SELECT * FROM tbl USING SAMPLE 10% (选择表大约 10% 的样本). */
if (statement.sample_options) {
root = make_uniq<LogicalSample>(std::move(statement.sample_options), std::move(root));
}
/* 处理 where 条件. */
if (statement.where_clause) {
root = PlanFilter(std::move(statement.where_clause), std::move(root));
}
/* 聚合算子. */
if (!statement.aggregates.empty() || !statement.groups.group_expressions.empty()) {
if (!statement.groups.group_expressions.empty()) {
// visit the groups
for (auto &group : statement.groups.group_expressions) {
PlanSubqueries(group, root);
}
}
// now visit all aggregate expressions
for (auto &expr : statement.aggregates) {
PlanSubqueries(expr, root);
}
// finally create the aggregate node with the group_index and aggregate_index as obtained from the binder
auto aggregate = make_uniq<LogicalAggregate>(statement.group_index, statement.aggregate_index,
std::move(statement.aggregates));
aggregate->groups = std::move(statement.groups.group_expressions);
aggregate->groupings_index = statement.groupings_index;
aggregate->grouping_sets = std::move(statement.groups.grouping_sets);
aggregate->grouping_functions = std::move(statement.grouping_functions);
aggregate->AddChild(std::move(root));
root = std::move(aggregate);
} else if (!statement.groups.grouping_sets.empty()) {
// edge case: we have grouping sets but no groups or aggregates
// this can only happen if we have e.g. select 1 from tbl group by ();
// just output a dummy scan
root = make_uniq_base<LogicalOperator, LogicalDummyScan>(statement.group_index);
}
/* 处理 having 语句, 使用 LogicalFilter 过滤算子. */
if (statement.having) {
PlanSubqueries(statement.having, root);
auto having = make_uniq<LogicalFilter>(std::move(statement.having));
having->AddChild(std::move(root));
root = std::move(having);
}
/* 处理窗口函数语句. */
if (!statement.windows.empty()) {
auto win = make_uniq<LogicalWindow>(statement.window_index);
win->expressions = std::move(statement.windows);
// visit the window expressions
for (auto &expr : win->expressions) {
PlanSubqueries(expr, root);
}
D_ASSERT(!win->expressions.empty());
win->AddChild(std::move(root));
root = std::move(win);
}
/* 处理 QUALIFY 语句, QUALIFY 是针对窗口函数结果的过滤. */
if (statement.qualify) {
PlanSubqueries(statement.qualify, root);
auto qualify = make_uniq<LogicalFilter>(std::move(statement.qualify));
qualify->AddChild(std::move(root));
root = std::move(qualify);
}
/* Unnesting 算子. */
for (idx_t i = statement.unnests.size(); i > 0; i--) {
auto unnest_level = i - 1;
auto entry = statement.unnests.find(unnest_level);
if (entry == statement.unnests.end()) {
throw InternalException("unnests specified at level %d but none were found", unnest_level);
}
auto &unnest_node = entry->second;
auto unnest = make_uniq<LogicalUnnest>(unnest_node.index);
unnest->expressions = std::move(unnest_node.expressions);
// visit the unnest expressions
for (auto &expr : unnest->expressions) {
PlanSubqueries(expr, root);
}
D_ASSERT(!unnest->expressions.empty());
unnest->AddChild(std::move(root));
root = std::move(unnest);
}
/* 处理投影列. */
for (auto &expr : statement.select_list) {
/* 如果有子查询. */
PlanSubqueries(expr, root);
}
/* 逻辑计划最上层为 LogicalProjection 投影算子. */
auto proj = make_uniq<LogicalProjection>(statement.projection_index, std::move(statement.select_list)); auto &projection = *proj;
proj->AddChild(std::move(root));
root = std::move(proj);
/* 处理 LIMIT, ORDER BY, DISTINCT 算子. */
root = VisitQueryNode(statement, std::move(root));
/* 处理需要剪枝的情况. */
if (statement.need_prune) {
D_ASSERT(root);
vector<unique_ptr<Expression>> prune_expressions;
for (idx_t i = 0; i < statement.column_count; i++) {
prune_expressions.push_back(make_uniq<BoundColumnRefExpression>(
projection.expressions[i]->return_type, ColumnBinding(statement.projection_index, i)));
}
auto prune = make_uniq<LogicalProjection>(statement.prune_index, std::move(prune_expressions));
prune->AddChild(std::move(root));
root = std::move(prune);
}
return root;
}通过上述流程可以看到,逻辑计划的生成并不是单纯的"语法树到逻辑树"的转换,而是伴随着大量的语义分析和子查询规划,确保每个算子都具备足够的上下文信息供后续优化与执行阶段使用。
Logical Plan Optimizer 逻辑计划优化
逻辑计划完成生成后, 我们需要对逻辑计划展开优化:
/* src/main/client_context.cpp */
shared_ptr<PreparedStatementData>
ClientContext::CreatePreparedStatementInternal(ClientContextLock &lock, const string &query,
unique_ptr<SQLStatement> statement,
optional_ptr<case_insensitive_map_t<BoundParameterData>> values) {
/* ... */
if (config.enable_optimizer && logical_plan->RequireOptimizer()) {
profiler.StartPhase(MetricsType::ALL_OPTIMIZERS);
/* 进行逻辑计划的优化. */
Optimizer optimizer(*logical_planner.binder, *this);
logical_plan = optimizer.Optimize(std::move(logical_plan));
D_ASSERT(logical_plan);
profiler.EndPhase();
#ifdef DEBUG
logical_plan->Verify(*this);
#endif
}
/* ... */
}Optimizer::Optimize 的整体框架如下:
DuckDB 的逻辑优化器本质上是一个多阶段的规则引擎:首先应用表达式重写(常量折叠、谓词归并等),随后执行逻辑层的算子改写(谓词下推、JOIN 重排、列裁剪),最后再把控制权交给扩展点,让用户注入自定义的优化规则。每个阶段都通过 RunOptimizer 计时,配合 EXPLAIN ANALYZE 中的 Profile 信息可以精确定位耗时的规则。
unique_ptr<LogicalOperator> Optimizer::Optimize(unique_ptr<LogicalOperator> plan_p) {
Verify(*plan_p);
this->plan = std::move(plan_p);
/* 用户自定义的优化规则. */
for (auto &pre_optimizer_extension : DBConfig::GetConfig(context).optimizer_extensions) {
RunOptimizer(OptimizerType::EXTENSION, [&]() {
OptimizerExtensionInput input {GetContext(), *this, pre_optimizer_extension.optimizer_info.get()};
if (pre_optimizer_extension.pre_optimize_function) {
pre_optimizer_extension.pre_optimize_function(input, plan);
}
});
}
/* Built-in 优化规则. */
RunBuiltInOptimizers();
/* 用户自定义的优化规则. */
for (auto &optimizer_extension : DBConfig::GetConfig(context).optimizer_extensions) {
RunOptimizer(OptimizerType::EXTENSION, [&]() {
OptimizerExtensionInput input {GetContext(), *this, optimizer_extension.optimizer_info.get()};
if (optimizer_extension.optimize_function) {
optimizer_extension.optimize_function(input, plan);
}
});
}
Planner::VerifyPlan(context, plan);
return std::move(plan);
}DuckDB 内置了很多启发式规则, 其中内置的优化规则包括:
- EXPRESSION_REWRITER: 表达式重写.
表达式重写是
Optimizer初始化时构造函数里就填好的规则, 包括常量折叠, 分配律优化, CASE 语句简化等等.
- FILTER_PULLUP: 谓词上拉.
我们以下面这个 SQL 为例, FILTER_PULLUP 优化会将
vals1.i=5提至FILTER算子下方, 方便后续的谓词下推:
D explain SELECT * FROM (SELECT * FROM vals1, vals2 WHERE vals1.i=5) as tbl1,
(SELECT * FROM vals1, vals2) as tbl2
WHERE tbl1.i=tbl2.i;
┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Unoptimized Logical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ Expressions: │
│ i │
│ i_1 │
│ i │
│ i_1 │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ Expressions: (i = i) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ CROSS_PRODUCT │
│ ──────────────────── ├───────────────────────────────────────────┐
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐
│ PROJECTION │ │ PROJECTION │
│ ──────────────────── │ │ ──────────────────── │
│ Expressions: │ │ Expressions: │
│ i │ │ i │
│ i │ │ i │
└─────────────┬─────────────┘ └─────────────┬─────────────┘
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐
│ FILTER │ │ CROSS_PRODUCT │
│ ──────────────────── │ │ ──────────────────── │
│ Expressions: │ │ ├──────────────┐
│ (i = CAST(5 AS INTEGER)) │ │ │ │
└─────────────┬─────────────┘ └─────────────┬─────────────┘ │
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ CROSS_PRODUCT │ │ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── │ │ ──────────────────── ││ ──────────────────── │
│ ├──────────────┐ │ Table: vals1 ││ Table: vals2 │
│ │ │ │ Type: Sequential Scan ││ Type: Sequential Scan │
└─────────────┬─────────────┘ │ └───────────────────────────┘└───────────────────────────┘
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ Table: vals1 ││ Table: vals2 │
│ Type: Sequential Scan ││ Type: Sequential Scan │
└───────────────────────────┘└───────────────────────────┘FILTER_PULLUP 优化后的逻辑计划:
┌─────────────────────────────┐
│┌───────────────────────────┐│
││ Unoptimized Logical Plan ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ Expressions: │
│ i │
│ i_1 │
│ i │
│ i_1 │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ Expressions: (i = i) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ Expressions: (i = 5) │ /* 将谓词条件 vals1.i=5 提升至这里. */
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ CROSS_PRODUCT │
│ ──────────────────── ├───────────────────────────────────────────┐
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐
│ PROJECTION │ │ PROJECTION │
│ ──────────────────── │ │ ──────────────────── │
│ Expressions: │ │ Expressions: │
│ i │ │ i │
│ i │ │ i │
└─────────────┬─────────────┘ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ CROSS_PRODUCT │
│ │ ──────────────────── │
│ │ ├──────────────┐
│ │ │ │
│ └─────────────┬─────────────┘ │
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ CROSS_PRODUCT │ │ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── │ │ ──────────────────── ││ ──────────────────── │
│ ├──────────────┐ │ Table: vals1 ││ Table: vals2 │
│ │ │ │ Type: Sequential Scan ││ Type: Sequential Scan │
└─────────────┬─────────────┘ │ └───────────────────────────┘└───────────────────────────┘
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ Table: vals1 ││ Table: vals2 │
│ Type: Sequential Scan ││ Type: Sequential Scan │
└───────────────────────────┘└───────────────────────────┘- FILTER_PUSHDOWN: 谓词下推
FILTER_PULLUP 将过滤条件提至了上端, FILTER_PUSHDOWN 搜集这些过滤条件下推到底层投影列, 这样在 SQL 执行的过程中可以提早的过滤数据, 减少 CPU 和 IO 的开销.
-
EMPTY_RESULT_PULLUP,
-
CTE_FILTER_PUSHER,
-
REGEX_RANGE,
-
IN_CLAUSE,
-
JOIN_ORDER:
JOIN_ORDER 优化方法主要目标是重新排列多表 JOIN 的顺序,以获得最优的查询执行性能.
-
DELIMINATOR,
-
UNNEST_REWRITER,
-
UNUSED_COLUMNS,
-
STATISTICS_PROPAGATION,
-
COMMON_SUBEXPRESSIONS,
-
COMMON_AGGREGATE,
-
COLUMN_LIFETIME,
-
BUILD_SIDE_PROBE_SIDE,
-
LIMIT_PUSHDOWN:
LIMIT_PUSHDOWN 优化是将 LIMIT 操作符下推到 PROJECTION 操作符之下,以减少不必要的行处理和提高查询性能.
- TOP_N:
针对
ORDER BY ... LIMIT的查询,将排序限制在 Top-N 元素范围内执行,避免对全部数据排序。
- COMPRESSED_MATERIALIZATION:
在物化中保留压缩格式,仅在必要时解压以减少内存带宽和缓存占用。
- DUPLICATE_GROUPS:
在聚合之前检测并消除重复的分组键组合,降低
GROUP BY需要处理的行数。
- REORDER_FILTER:
调整同一节点下多个过滤条件的顺序,优先执行选择性更高的条件以尽早裁剪数据。
- SAMPLING_PUSHDOWN:
将采样操作下推到扫描算子,从数据源直接获取样本行,缩短上层管线等待时间。
- JOIN_FILTER_PUSHDOWN:
将连接产生的过滤条件继续向下推送到参与连接的输入上,减少参与 JOIN 的候选行。
- EXTENSION:
为用户自定义或外部扩展预留的规则集合,可在此阶段注入定制优化。
- MATERIALIZED_CTE:
判断 CTE 是否需要物化,若可复用则缓存一次结果供多处引用,若仅使用一次则改为内联避免额外开销。
- SUM_REWRITER: SUM() 聚合表达式的重写.
官方注释里标明了具体的优化规则即: SUM(x + C) -> SUM(x) + C * COUNT(x), x 为某列字段, C 为常量: SUM(x + C) 需要对每列的数据先多一次加法, 每列数据额外引入了一条加法指令. SUM(x) 对一大列连续内存做加法, 可以使用 SIMD,比如 AVX 指令一轮处理8或16个元素, 然后使用一条乘法指令 C * count(X) 即可.
- LATE_MATERIALIZATION:
在列式执行中尽量延迟宽列或大对象的加载,等到真正需要输出阶段再取回具体字段。
这些规则以流水线的方式运行:例如在 FILTER_PULLUP 将谓词统一收集到上层后,FILTER_PUSHDOWN 会立刻尝试把同一组谓词重新分发到 LogicalGet、LogicalJoin 等算子上,从而达到“上提 → 分发 → 裁剪”的闭环。
总结
本文梳理了 DuckDB 在逻辑层面的执行流程:从 PostgreSQL Parser 得到 AST,到 Binder 绑定语义,再到 LogicalOperator 树的逐步构建,最后交由启发式优化器对计划进行重写。逻辑计划优化器仍然是 DuckDB 中最值得深挖的部分,例如谓词上下推、JOIN 重排、统计信息传播等策略都蕴含着大量实现细节。后续文章将针对其中几类优化算法展开更深入的源码分析。