分类 :4.查询引擎 | 篇章:12 EXPLAIN 与优化

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-20
EXPLAIN 是诊断查询性能的关键工具。本文系统讲解 EXPLAIN 输出的算子树结构、关键指标含义、常见慢查询模式的识别与优化方法,让"看到计划就知道哪里慢"成为可能。
核心概念速查表
| 概念 | 说明 |
|---|---|
| EXPLAIN | 显示查询的物理执行计划 |
| EXPLAIN VERBOSE | 显示更详细的计划(含估算行数) |
| EXPLAIN ANALYZE | 实际执行并显示运行时统计 |
| Cardinality | 估算的行数 |
| Actual Rows | 实际处理行数 |
| Cost | 估算的代价(IO+CPU 综合) |
| Slow Query Log | 慢查询日志 |
详细解析
1. EXPLAIN 输出结构
EXPLAIN 基本输出:
EXPLAIN SELECT location, AVG(current) FROM meters
WHERE ts > now-1h GROUP BY location;
输出(树形,从底向上读):
-> Project (location, avg)
-> Aggregate Final (group=location, agg=avg(current))
-> Exchange
-> Aggregate Partial (group=location, agg=sum,count)
-> Filter (ts > T1)
-> TableScan (meters, tagFilter=NULL)
阅读方式:
- 缩进越深 = 越底层
- 数据从底层流向上层
- 每行是一个算子
2. EXPLAIN VERBOSE 增强信息
EXPLAIN VERBOSE 提供:
- 每个算子的估算行数(estimated rows)
- 输出列信息
- 时间裁剪范围
- Tag 过滤详情
- Exchange 类型(Merge/Shuffle/...)
示例:
-> TableScan (meters)
rows=1000000
timeRange=[T1, T2]
tagFilter=(location='BJ')
cols=[ts, current, voltage]
order=asc
3. EXPLAIN ANALYZE 实际执行
EXPLAIN ANALYZE:实际跑一遍并显示运行时数据
EXPLAIN ANALYZE SELECT * FROM meters WHERE ts > now-1h;
输出关键指标:
-> TableScan
actual_rows=850000
actual_time=120ms
exec_count=8 ← 8 个 VNode 并行
bytes_read=45MB
blocks_read=200
blocks_skipped=180 ← 跳过了 180 个块(时间裁剪)
关键指标解读:
- actual_rows: 实际产出的行数
- actual_time: 实际耗时
- blocks_skipped: 表示优化生效程度
- bytes_read: I/O 量
4. 常见慢查询模式识别
模式 1:无时间裁剪
-> TableScan (meters)
timeRange=ALL ← 警告!全时段扫描
blocks_read=10000
诊断:缺少 WHERE ts 条件
优化:增加时间范围限制
模式 2:Tag 过滤未生效
-> TableScan (meters)
tagFilter=NULL ← Tag 条件未下推
...
诊断:可能 Tag 列被函数包裹
优化:检查 WHERE 写法
模式 3:列裁剪未生效
-> TableScan (meters)
cols=[ALL] ← 读取了所有列
诊断:SELECT * 或表达式引用了多列
优化:显式列出需要的列
模式 4:SMA 未使用
-> Aggregate
use_sma=false ← 未走预聚合
诊断:可能有数据列过滤条件
优化:拆分查询或调整聚合方式
模式 5:单 VGroup 瓶颈
-> TableScan (meters)
exec_count=1 ← 只有 1 个 VNode 在跑
诊断:数据集中在单 VGroup(vgroups 数太少)
优化:增加 VGROUPS 数量
5. 慢查询日志
开启慢查询日志:
taos.cfg 配置:
monitor 1
monitorInterval 30
自动记录的查询:
- 执行时间超过阈值
- 写入 log/taosSlowLog.<date>
查询慢查询:
SELECT * FROM log.taosd_slow_log
WHERE start_ts > today()
ORDER BY exec_time DESC
LIMIT 20;
关键字段:
- sql: SQL 文本
- exec_time: 执行时间(ms)
- read_rows: 读取行数
- peak_memory: 峰值内存
6. 查询优化方法论
优化步骤:
① 复现慢查询
- 确认输入数据规模
- 确认运行环境(集群配置)
② EXPLAIN ANALYZE 看实际执行
- 找出耗时最多的算子
- 找出处理行数最多的算子
③ 检查优化是否生效
- 时间裁剪 ✓
- Tag 索引 ✓
- 列裁剪 ✓
- SMA ✓
④ 改写 SQL 或调整 Schema
- 加 WHERE 限制
- 改用 SMA 友好函数
- 重新设计 Tag 结构
⑤ 必要时调整集群配置
- 增加 VGROUPS
- 启用 QNode
- 增大查询资源
⑥ 验证效果
7. 常见反模式
反模式 1:函数包裹列
WHERE TO_CHAR(ts, 'yyyy-mm-dd') = '2024-06-04'
→ ts 无法用于时间裁剪
改写:
WHERE ts >= '2024-06-04' AND ts < '2024-06-05'
反模式 2:OR 跨 Tag 列
WHERE (location='BJ' AND ts > T1) OR (location='SH' AND ts > T2)
→ Tag 索引可能放弃
改写:
WHERE location IN ('BJ','SH') AND ts > MIN(T1, T2)
反模式 3:超长 IN 列表
WHERE tbname IN ('d001','d002',...,'d10000')
→ Parser 慢,Catalog 拉取多
改写:用 Tag 等条件代替
反模式 4:大 OFFSET
ORDER BY ts LIMIT 10 OFFSET 1000000
→ 实际处理 100 万行
改写:用时间游标
WHERE ts > <last_seen_ts> ORDER BY ts LIMIT 10
反模式 5:高基数 GROUP BY 跨 VGroup
GROUP BY user_id -- 1 亿用户
→ Shuffle 数据量巨大
改写:
- 增加 PARTITION BY 让局部聚合
- 用近似算法(HLL)
8. 物化中间结果
针对反复查询的优化:用流计算预聚合
原始查询(慢):
SELECT _wstart, AVG(current)
FROM meters_billion
INTERVAL(1h);
优化:
-- 创建流持续维护小时聚合
CREATE STREAM s_hourly
INTO meters_hourly
AS SELECT _wstart, tbname, AVG(current)
FROM meters_billion
PARTITION BY tbname
INTERVAL(1h);
-- 查询时直接读预聚合表(快)
SELECT _wstart, AVG(avg_current_partial)
FROM meters_hourly
WHERE ts > now-1d;
代码示例
完整优化案例
sql
-- 慢查询:扫描太多
SELECT * FROM meters WHERE location='BJ';
-- EXPLAIN 显示 timeRange=ALL,扫描数年数据
-- 优化 1:加时间范围
SELECT * FROM meters WHERE location='BJ' AND ts > now-1h;
-- 优化 2:只选需要的列
SELECT ts, current FROM meters WHERE location='BJ' AND ts > now-1h;
-- 优化 3:限制返回
SELECT ts, current FROM meters
WHERE location='BJ' AND ts > now-1h
ORDER BY ts DESC LIMIT 1000;
监控查询性能
sql
-- 查看当前查询
SELECT
sql,
exec_usec/1000 AS ms,
sub_num
FROM performance_schema.perf_queries
ORDER BY exec_usec DESC LIMIT 10;
-- 查看历史慢查询(如开启日志)
SELECT * FROM log.taosd_slow_log
WHERE start_ts > today() - 1d
ORDER BY exec_time DESC LIMIT 20;
性能考量
优化优先级
| 优化项 | 收益 | 成本 |
|---|---|---|
| 加时间范围 | 高(避免全表) | 零 |
| Tag 过滤 | 高(跳过子表) | 零 |
| 列裁剪 | 中(减少 I/O) | 零 |
| 用 SMA 函数 | 高(用预聚合) | 零 |
| 部署 QNode | 中(隔离负载) | 节点资源 |
| 增加 VGROUPS | 中(提升并行) | 重建表 |
| 流计算预聚合 | 极高(毫秒返回) | 维护成本 |
优化清单(按顺序)
- WHERE 含明确时间范围
- WHERE 含 Tag 等值/IN 过滤
- SELECT 列名明确(非 *)
- ORDER BY 配合 LIMIT
- 聚合使用 SMA 友好函数
- 高频查询用 STMT 参数化
- 反复查询同一聚合 → 流计算
- 大集群部署 QNode
FAQ
Q1: EXPLAIN 显示的估算行数准吗?
是估算值,可能与实际差距较大(尤其无统计信息的列)。建议用 EXPLAIN ANALYZE 看实际数。
Q2: 优化生效后为什么还是慢?
可能原因:
- 数据量本身就大(优化已是极限)
- 网络或磁盘 I/O 是瓶颈
- 并发查询挤占资源
- 服务端资源不足
Q3: 同样 SQL 为什么有时快有时慢?
常见原因:
- 缓存命中差异(块缓存/Catalog 缓存)
- 并发查询争抢资源
- 数据分布变化(如新增了大量子表)
- VNode 在做 Compaction
Q4: 必要时如何强制使用某种执行方式?
可以使用 hint 影响调度:
/*+ USE_QNODE */强制 QNode- 其他高级 hint 见版本文档
参考
系统构架篇
- 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 逻辑计划生成》
- 05-《TDengine 物理计划生成》
- 06-《TDengine 扫描算子》
- 07-《TDengine 聚合算子》
- 08-《TDengine 聚合算子》
- 09-《TDengine 连接算子》
- 10-《TDengine 排序、填充与投影》
- 11-《TDengine 分布式查询执行》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。