分类: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 - 按分区独立处理 → PartitionExchange5. 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) ... 直到所有 VGroup6. 时序专属物理算子
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 |
代码示例
查看物理计划
EXPLAINVERBOSESELECTlocation,AVG(current)FROMmetersWHEREts>now-1hGROUPBYlocation;-- 输出会显示:-- - Subplan 0: AggregateMerge + ExchangeReceiver-- - Subplan 1..N: AggregatePartial + TableScan强制使用 QNode
-- 提示查询使用 QNode(如果集群配置了)SELECT/*+ USE_QNODE */COUNT(*)FROMbig_table;观察 Exchange 数据量
EXPLAINANALYZESELECTlocation,COUNT(*)FROMmetersGROUPBYlocation;-- 关注 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 提供实时分析、可视化、事件管理与报警等功能。