TDengine EXPLAIN 与查询优化 — 读懂计划、诊断慢查询、调优实战

分类 :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 中(提升并行) 重建表
流计算预聚合 极高(毫秒返回) 维护成本

优化清单(按顺序)

  1. WHERE 含明确时间范围
  2. WHERE 含 Tag 等值/IN 过滤
  3. SELECT 列名明确(非 *)
  4. ORDER BY 配合 LIMIT
  5. 聚合使用 SMA 友好函数
  6. 高频查询用 STMT 参数化
  7. 反复查询同一聚合 → 流计算
  8. 大集群部署 QNode

FAQ

Q1: EXPLAIN 显示的估算行数准吗?

是估算值,可能与实际差距较大(尤其无统计信息的列)。建议用 EXPLAIN ANALYZE 看实际数。

Q2: 优化生效后为什么还是慢?

可能原因:

  • 数据量本身就大(优化已是极限)
  • 网络或磁盘 I/O 是瓶颈
  • 并发查询挤占资源
  • 服务端资源不足

Q3: 同样 SQL 为什么有时快有时慢?

常见原因:

  • 缓存命中差异(块缓存/Catalog 缓存)
  • 并发查询争抢资源
  • 数据分布变化(如新增了大量子表)
  • VNode 在做 Compaction

Q4: 必要时如何强制使用某种执行方式?

可以使用 hint 影响调度:

  • /*+ USE_QNODE */ 强制 QNode
  • 其他高级 hint 见版本文档

参考

系统构架篇

数据模型

存储引擎

查询引擎

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。