分类 :4.查询引擎 | 篇章:04 逻辑计划

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-11
逻辑计划(Logical Plan)将语义分析后的 AST 转换为与执行无关的关系代数算子树。这一层抽象屏蔽了具体的存储格式和执行方式,便于做规则化的查询优化(谓词下推、列裁剪、连接重排序等)。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Logical Plan | 关系代数算子树 |
| Logical Operator | 算子节点:Scan/Filter/Project/Agg/Join/Sort 等 |
| Predicate Pushdown | 谓词下推(过滤尽早执行) |
| Column Pruning | 列裁剪(只投影需要的列) |
| Subplan | 子计划(用于分布式执行的逻辑单元) |
| Rewrite Rule | 基于规则的重写优化 |
详细解析
1. 逻辑算子类型
| 算子 | 作用 | 输入/输出 |
|---|---|---|
| LogicScan | 表扫描 | 输入:表 / 输出:行 |
| LogicFilter | 过滤 | WHERE 条件 |
| LogicProject | 投影 | SELECT 列表 |
| LogicAgg | 聚合 | GROUP BY / 聚合函数 |
| LogicWindow | 时间窗口 | INTERVAL/SESSION/STATE |
| LogicSort | 排序 | ORDER BY |
| LogicJoin | 连接 | INNER/LEFT/...JOIN |
| LogicExchange | 节点间数据交换 | 分布式专用 |
| LogicMerge | 多源合并 | 跨 VGroup 结果合并 |
| LogicFill | 填充缺失值 | FILL 子句 |
| LogicPartition | 分区(按子表) | PARTITION BY |
2. AST 到 Logical Plan 的转换
SQL: SELECT location, AVG(current)
FROM meters
WHERE ts > now-1h AND voltage > 200
GROUP BY location
AST:
SelectStmt
├── select_list: [location, AVG(current)]
├── from: meters
├── where: ts > now-1h AND voltage > 200
└── group_by: [location]
转换为 Logical Plan(自底向上构建):
LogicProject (location, AVG(current))
│
▼
LogicAgg (group_by=[location], aggs=[AVG(current)])
│
▼
LogicFilter (ts > now-1h AND voltage > 200)
│
▼
LogicScan (meters)
3. 基础优化规则
规则一:谓词下推(Predicate Pushdown)
原始计划:
Filter (ts > T1 AND voltage > 200)
└── Join (t1 ON t2)
├── Scan (t1)
└── Scan (t2)
下推后:
Join
├── Filter (ts > T1) ← 下推到 t1 侧
│ └── Scan (t1)
└── Filter (voltage > 200) ← 下推到 t2 侧
└── Scan (t2)
效果:减少 Join 输入行数
规则二:列裁剪(Column Pruning)
原始计划:
Project (a, b)
└── Scan (a, b, c, d, e, f) ← 读取所有列
优化后:
Project (a, b)
└── Scan (a, b) ← 只读需要的列
效果:减少 I/O 和反序列化
规则三:常量传播
WHERE a = 5 AND b = a + 3
→ WHERE a = 5 AND b = 8
4. 时序专属优化
规则四:时间裁剪(Time Range Cut)
WHERE ts BETWEEN '2024-01-01' AND '2024-01-02'
→ 标注 LogicScan 的 timeRange = [T1, T2]
→ 后续物理计划阶段用此范围跳过无关 File Set
规则五:Tag 过滤前置(Tag Filter)
WHERE location = 'BJ' AND ts > now-1h
→ 分离 Tag 条件:location='BJ' → tagFilter
→ 数据条件:ts > now-1h → dataFilter
→ tagFilter 用于子表过滤
→ dataFilter 用于行过滤
规则六:聚合下推到子表
SELECT location, AVG(current)
FROM meters
GROUP BY location
优化为两阶段聚合:
阶段 1:每个子表内部 → 计算 SUM(current), COUNT(*)
阶段 2:跨子表合并 → 求总 AVG = SUM/COUNT
优势:
- 阶段 1 可在 Scan 算子内完成(无需中间物化)
- 阶段 2 输入数据量大幅减少
5. 子查询展开
能展开的子查询:
SELECT * FROM (
SELECT ts, current FROM meters WHERE voltage > 200
) WHERE ts > now-1h
↓ 展开
SELECT ts, current FROM meters
WHERE voltage > 200 AND ts > now-1h
不能展开的子查询:
SELECT * FROM (
SELECT location, AVG(current) AS avg_c
FROM meters
GROUP BY location
) WHERE avg_c > 100
→ 子查询有聚合,必须先完成聚合
→ 保留为两层计划:
Filter (avg_c > 100)
└── Agg (GROUP BY location)
└── Scan (meters)
6. JOIN 重排序
连接重排序(小表先 Join):
SQL: SELECT * FROM big_t
JOIN small_t1 ON ...
JOIN small_t2 ON ...
优化前:
Join (big_t, Join(small_t1, small_t2))
→ 大表参与所有 Join,中间结果大
优化后:
Join (Join(small_t1, small_t2), big_t)
→ 小表先 Join 缩小中间结果
→ 再与大表 Join
TDengine 主要用于时序数据,JOIN 重排序主要应用于:
- 维度表 JOIN
- 多个 Tag 表关联
7. 窗口算子的逻辑表达
INTERVAL 窗口的逻辑表示:
SELECT _wstart, _wend, COUNT(*)
FROM meters
WHERE ts > now-1d
INTERVAL(1h) SLIDING(30m) FILL(NULL)
Logical Plan:
LogicProject (_wstart, _wend, count)
│
▼
LogicFill (NULL)
│
▼
LogicWindow (
type = INTERVAL,
interval = 1h,
sliding = 30m,
aggregations = [COUNT(*)]
)
│
▼
LogicFilter (ts > now-1d)
│
▼
LogicScan (meters)
8. 子计划(Subplan)划分
分布式查询的子计划划分:
单 VGroup 计划(简单情况):
└── 全部算子在一个 Subplan 中
跨 VGroup 计划:
Subplan 0(Client/QNode 端):
Project → Sort → Exchange ← 接收下层数据
Subplan 1(VNode 1):
Partial Agg → Filter → Scan
Subplan 2(VNode 2):
Partial Agg → Filter → Scan
Subplan 3(VNode N):
...
每个 Subplan 独立调度到对应节点执行
通过 Exchange 算子连接父子 Subplan
代码示例
查看逻辑计划
sql
EXPLAIN VERBOSE
SELECT location, AVG(current)
FROM meters
WHERE ts > now-1h
GROUP BY location;
-- 输出示例:
-- LogicProject
-- LogicAgg [group=location, agg=AVG(current)]
-- LogicFilter [ts > T1]
-- LogicScan [meters, tagFilter=NULL]
优化前后对比
sql
-- 优化前(程序生成的低效 SQL)
SELECT * FROM (
SELECT * FROM meters
) WHERE location='BJ' AND ts > now-1h;
-- 经过子查询展开 + Tag 过滤前置后
-- 实际执行效果等同于:
SELECT * FROM meters WHERE location='BJ' AND ts > now-1h;
性能考量
优化规则的影响
| 规则 | 性能影响 |
|---|---|
| 谓词下推 | 减少中间数据量(关键) |
| 列裁剪 | 减少 I/O 和反序列化 |
| 时间裁剪 | 减少扫描 File Set 数 |
| Tag 过滤前置 | 减少参与子表数 |
| 聚合下推 | 减少节点间传输 |
| 子查询展开 | 减少物化中间结果 |
编写 SQL 的最佳实践
| 建议 | 原因 |
|---|---|
| WHERE 列尽量"裸用" | 让谓词能下推到 Scan |
| 优先选具体列而非 SELECT * | 触发列裁剪 |
| Tag 过滤 + 时间过滤 | 同时享受 Tag 索引和时间裁剪 |
| 避免 ORDER BY 全表 | 排序可能阻止流式输出 |
FAQ
Q1: 如何确认优化规则被应用了?
使用 EXPLAIN VERBOSE 查看最终的算子树。如果看到 Filter 出现在 Scan 上方而不是 Scan 内部,说明谓词未下推到 Scan 算子(可能因表达式过于复杂)。
Q2: 复杂表达式会被下推吗?
简单的列比较常量(如 a > 5)能下推。涉及函数(如 UPPER(name) = 'A')或多列运算(如 a + b > 10)的下推有限制,部分会保留在 Filter 算子中。
Q3: 为什么我的 WHERE Tag 条件没有用索引?
检查:
- 表达式是否裸用 Tag(不是
UPPER(location) = 'BJ') - 是否使用了
OR跨 Tag 列(可能放弃索引) - 是否有数据列条件混在其中(应该正常分离,但复杂情况下可能失效)
Q4: GROUP BY tbname 与 PARTITION BY tbname 有什么区别?
GROUP BY tbname 是标准 SQL 聚合分组。PARTITION BY tbname 是时序专属语法,按子表分区独立计算,常与窗口函数组合(每个子表独立时间窗口)。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
- 01-《TDengine 存储引擎概览》
- 02-《TDengine MemTable 深度解析》
- 03-《TDengine WAL 预写日志机制》
- 04-《TDengine 数据文件格式》
- 05-《TDengine Commit 与 Flush 机制 》
- 06-《TDengine Compaction 合并策略 》
- 07-《TDengine 数据保留与 TTL》
- 08-《TDengine 压缩编码机制》
- 09-《TDengine Cache 与 Last 查询加速》
- 10-《TDengine 数据修复与迁移》
查询引擎
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。