分类 :4.查询引擎 | 篇章:05 物理计划

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-12
物理计划(Physical Plan)将逻辑算子映射为具体的物理算子实现,确定算子的执行节点(VNode/QNode/Client),插入 Exchange 节点完成数据流转,最终输出可被 Scheduler 直接调度的 Subplan 集合。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Physical Operator | 物理算子(Table Scan、Hash Agg、Merge Join 等) |
| Subplan | 一个执行单元,对应一个节点(VNode/QNode)的任务 |
| Exchange Operator | 跨节点数据传输算子 |
| DataSink | Subplan 的输出端,将数据发给上层 |
| DAG | Subplan 之间形成的有向无环图 |
| Two-Phase Aggregation | 两阶段聚合(Partial + Final) |
详细解析
1. 物理算子分类
TDengine 主要物理算子:
扫描类:
- TableScan / TagScan / SystemTableScan
- StreamScan
过滤投影:
- Filter / Project
聚合类:
- Hash Aggregate(GROUP BY)
- Stream Aggregate(已排序数据流式聚合)
- Partial / Final Aggregate(两阶段)
窗口类:
- Interval Window
- Session Window
- State Window
- Event Window
- Count Window
排序合并:
- Sort / Merge / SortMerge
连接类:
- Hash Join / Merge Join / Nested Loop Join
数据流:
- Exchange(接收)
- DataDispatch(发送)
2. 逻辑算子到物理算子的映射
| 逻辑算子 | 候选物理算子 | 选择依据 |
|---|---|---|
| LogicScan | TableScan / TagScan | 是否只查 Tag |
| LogicFilter | Filter | 通用 |
| LogicAgg | HashAgg / StreamAgg | 输入是否已排序 |
| LogicWindow | IntervalWindow / SessionWindow | 窗口类型 |
| LogicSort | Sort / MergeSort | 输入是否多路 |
| LogicJoin | HashJoin / MergeJoin | 数据量与排序性 |
3. 两阶段聚合下沉
SELECT location, AVG(current) FROM meters GROUP BY location
逻辑计划:
LogicAgg (GROUP BY location, AVG(current))
└── LogicScan (meters)
物理计划(两阶段):
┌─ Subplan 0 (Client/QNode) ─────────────────┐
│ Final Aggregate │
│ AVG = SUM(partial_sum) / SUM(partial_cnt)│
│ ↑ │
│ Exchange ← 接收 Subplan 1/2/N 的结果 │
└────────────────────────────────────────────┘
▲
┌───────────┼───────────┬─────────────┐
│ │ │ │
┌─ Subplan 1 ─┐ ┌─ Subplan 2 ─┐ ┌─ Subplan N ─┐
│ DataSink │ │ DataSink │ │ DataSink │
│ Partial Agg │ │ Partial Agg │ │ Partial Agg │
│ SUM(c), │ │ SUM(c), │ │ SUM(c), │
│ COUNT(*) │ │ COUNT(*) │ │ COUNT(*) │
│ TableScan │ │ TableScan │ │ TableScan │
└─────────────┘ └─────────────┘ └─────────────┘
(VNode 1) (VNode 2) (VNode N)
优势:
- VNode 内本地聚合,输出从万行减少到几行
- Exchange 传输量极小
- Final 聚合开销低
4. Exchange 算子的工作模式
Exchange 的三种数据传输模式:
① ShuffleExchange(按 Key 重分发):
场景:跨节点 GROUP BY 大量分组键
行为:发送端按 Key Hash 决定接收方
接收端按 Key 收集到对应桶
② PartitionExchange(保持分区):
场景:PARTITION BY tbname
行为:每个子表的数据完整发到同一接收端
接收端独立计算每个分区
③ MergeExchange(按序合并):
场景:跨节点 ORDER BY ts
行为:每个 VNode 内已按 ts 排序
接收端做 K-way Merge
选择策略:
- 数据已有序 + 需要全局有序 → MergeExchange
- 按 Key 分组 → ShuffleExchange
- 按分区独立处理 → PartitionExchange
5. Subplan 切分规则
Subplan 边界的判定:
规则:算子需要跨节点数据 → 插入 Exchange → 切分 Subplan
示例:
SELECT location, AVG(current)
FROM meters
WHERE ts > now-1h
GROUP BY location
Subplan 切分:
Subplan 0 (Client)
Final Aggregate
Exchange (接收 Subplan 1~N)
Subplan 1 (VNode 1)
DataSink (发到 Subplan 0)
Partial Aggregate
Filter (ts > now-1h)
TableScan (meters, vgId=1)
Subplan 2 (VNode 2)
DataSink (发到 Subplan 0)
Partial Aggregate
Filter (ts > now-1h)
TableScan (meters, vgId=2)
... 直到所有 VGroup
6. 时序专属物理算子
INTERVAL 窗口物理算子选择:
SELECT _wstart, COUNT(*) FROM meters INTERVAL(1h)
数据已按 ts 排序的优势:
① StreamIntervalWindow(流式):
- 输入按 ts 有序
- 维护当前窗口状态
- ts 跨入新窗口 → 输出当前窗口结果
- 内存占用 O(1)(只保留当前窗口)
vs HashIntervalWindow:
- 输入无序时使用
- 用哈希表保存所有未关闭窗口
- 内存占用 O(N)(N = 窗口数)
TDengine 优先选择 StreamIntervalWindow(数据天然有序)
SESSION/STATE 窗口:
- 必须按 ts 顺序扫描
- 边界由数据内容决定(不是固定时间)
- 算子维护当前会话/状态
7. 限制性子句的物理实现
LIMIT 与 OFFSET:
LIMIT N 下推:
SELECT * FROM meters LIMIT 10
→ 每个 VNode 各取 10 行(Partial LIMIT)
→ Exchange 接收最多 10*N 行
→ Final 阶段 LIMIT 10
ORDER BY + LIMIT 优化:
SELECT * FROM meters ORDER BY ts DESC LIMIT 10
→ 每个 VNode 内有序 → 取最后 10 行
→ Exchange 阶段 K-way Merge 取前 10
→ 无需全量排序
OFFSET 的代价:
OFFSET 1000000 → 需要先读取并丢弃前 100 万行
→ 大 OFFSET 性能极差
→ 推荐用时间范围或 Tag 过滤代替分页
8. 不同查询类型的物理计划差异
| 查询类型 | 物理特点 |
|---|---|
| 单子表点查 | 单 Subplan,无 Exchange |
| 单超级表无聚合 | 跨 VGroup Scan + Merge |
| 单超级表聚合 | 两阶段 Agg + Exchange |
| 跨库 JOIN | Hash Join + 多 Subplan |
| 窗口聚合 | StreamWindow + 两阶段 |
| 嵌套子查询 | 多层 Exchange |
代码示例
查看物理计划
sql
EXPLAIN VERBOSE
SELECT location, AVG(current)
FROM meters
WHERE ts > now-1h
GROUP BY location;
-- 输出会显示:
-- - Subplan 0: AggregateMerge + ExchangeReceiver
-- - Subplan 1..N: AggregatePartial + TableScan
强制使用 QNode
sql
-- 提示查询使用 QNode(如果集群配置了)
SELECT /*+ USE_QNODE */ COUNT(*) FROM big_table;
观察 Exchange 数据量
sql
EXPLAIN ANALYZE
SELECT location, COUNT(*)
FROM meters
GROUP BY location;
-- 关注 Exchange 节点的:
-- - rows_input: 接收的行数
-- - bytes_input: 接收的字节数
-- → 越小说明 Partial Agg 越有效
性能考量
Subplan 数量对性能的影响
| Subplan 数 | 适用场景 | 注意 |
|---|---|---|
| 1 | 单子表点查 | 最简单,无网络开销 |
| 数十 | 中等规模聚合 | 平衡并行度与协调开销 |
| 数百 | 大规模分析 | 调度开销可能成为瓶颈 |
| 数千 | 不推荐 | 应考虑预聚合或分批查询 |
Exchange 优化要点
| 要点 | 说明 |
|---|---|
| 尽量在叶子节点 Partial Agg | 减少 Exchange 数据量 |
| 利用数据有序性 | 用 MergeExchange 替代 Sort |
| 避免无谓的全量传输 | LIMIT/Tag 过滤下推 |
FAQ
Q1: 为什么我的查询只有一个 Subplan?
可能原因:
- 只查一个子表(命中单一 VGroup)
- 时间范围内只有一个 VGroup 有数据
- 使用了不分区的 META 表查询
这是高效的------无 Exchange 开销。
Q2: 两阶段聚合一定更快吗?
大部分情况是。但极端场景(如分组键基数巨大且无重复,Partial Agg 无收益)可能反而增加开销。规划器会根据估算决定是否启用。
Q3: 物理计划缓存吗?
参数化查询(STMT)的物理计划会被复用。普通文本 SQL 每次都重新生成。如需高频执行相同结构的查询,强烈推荐使用 STMT。
Q4: 能手动控制 Subplan 切分吗?
目前不支持直接干预。可以通过 hint(如 USE_QNODE)影响算子放置,或通过改写 SQL(如 CTE/子查询)间接影响。
参考
系统构架篇
- 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 逻辑计划生成》
查询引擎
- 01-《TDengine 查询引擎概览》
- 02-《TDengine SQL 解析与词法分析》
- 03-《TDengine 语义分析与 AST 重写》
- 04-《TDengine 语义分析与 AST 重写》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。