分类 :4.查询引擎 | 篇章:06 扫描算子

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-13
扫描算子是查询树的叶子,负责从存储层读取数据。TDengine 的扫描算子针对时序数据深度优化:时间裁剪、Tag 索引、SMA 预聚合、谓词下推、列裁剪一系列机制让 Scan 阶段就能过滤掉绝大部分无效数据。
核心概念速查表
| 概念 | 说明 |
|---|---|
| TableScan | 标准表扫描,读取数据列 |
| TagScan | 仅扫描 Tag(不读 TSDB 数据) |
| SystemTableScan | 系统表扫描(information_schema 等) |
| Time Range Filter | 时间范围裁剪(File Set 级) |
| Tag Index Filter | Tag 索引过滤(uid 列表) |
| Block Skip | 基于 SMA 的数据块跳过 |
| Column Pruning | 列裁剪(只读 SELECT 中的列) |
详细解析
1. TableScan 的核心职责
TableScan 输入与输出:
输入:
- 表名(超级表/子表)
- 时间范围 [tsMin, tsMax]
- Tag 过滤条件
- 数据列过滤条件
- 需要的列列表
- VGroup ID(分布式查询时)
输出:
- 列式 DataBlock(每块默认 4096 行)
- 包含请求的列 + ts 列
- 数据按 ts 升序(默认)或降序(ORDER BY ts DESC)
TableScan 内部流程:
① 根据时间范围确定相关 File Set
② 根据 Tag 过滤确定相关子表 uid 列表
③ 对每个子表:
a. 打开 MemTable 读取器
b. 打开 STT 文件读取器
c. 打开 .data 文件读取器
d. 多路归并合并(详见 04-file-format)
④ 应用数据列过滤
⑤ 投影出需要的列
⑥ 组装 DataBlock 输出
2. 时间裁剪
时间裁剪三个级别:
① File Set 级:
SQL: WHERE ts > '2024-06-01'
→ 跳过所有 fid_max < '2024-06-01' 的 File Set
→ 减少打开文件数
② Block 级:
File Set 内每个数据块有 [minTs, maxTs]
→ 跳过 maxTs < 查询条件的块
→ 减少解压数据量
③ Row 级:
块内逐行比较 ts
→ 跳过不符合的行
时间裁剪的失效场景:
✗ WHERE DATE(ts) = '2024-06-01'
→ ts 被函数包裹,无法下推
✗ WHERE ts + 3600 > now
→ 表达式复杂,可能不下推
✓ 正确写法:
WHERE ts BETWEEN '2024-06-01' AND '2024-06-02'
WHERE ts > now - 1h
3. Tag 过滤与索引
Tag 过滤的执行路径:
SELECT * FROM meters WHERE location='BJ' AND ts > now-1h
① Translator 阶段:
分离 Tag 条件:location='BJ'
② Catalog/META 查询:
┌─ 查 Tag 索引:
│ Key=(suid_meters, col_location, 'BJ')
│ → 返回 uid 列表 [101, 205, 308, ...]
│
└─ 索引未命中 → 全表扫描 Tag 表过滤
③ TableScan 仅针对这些 uid 扫描数据
Tag 索引的覆盖:
✓ 等值查询:location='BJ' → 索引精确命中
✓ IN 查询:location IN ('BJ','SH') → 多次索引查询
✓ 范围查询(数值 Tag):group_id BETWEEN 1 AND 10
✗ 不能用索引:
LIKE 'BJ%'(前缀查询有限支持,看版本)
NOT location='BJ'
函数包裹的 Tag
4. SMA 预聚合的利用
SMA 加速聚合查询:
SELECT COUNT(*) FROM meters WHERE ts > now-1d
无 SMA:
扫描所有数据块 → 累加每块的 nRows
有 SMA:
读取 .sma 文件 → 直接累加块级 COUNT
无需读取 .data → 大幅加速
SELECT AVG(current) FROM meters
AVG = SUM/COUNT
→ 读取 .sma 中每块的 SUM 和 COUNT
→ 直接计算结果
适用 SMA 的函数:
COUNT, SUM, MIN, MAX, AVG(= SUM/COUNT)
不适用 SMA 的:
- 含 WHERE 数据列条件(如 WHERE current > 5)
→ 块级 SUM 包含不符合条件的行
- DISTINCT 类聚合
- 自定义 UDF
5. 列裁剪
列裁剪的效果:
表有 100 列:ts, c1, c2, ..., c100
查询 1: SELECT * FROM meters WHERE ts > now-1h
→ Scan 读取所有 100 列
→ I/O:100 列字节流
查询 2: SELECT ts, c1 FROM meters WHERE ts > now-1h
→ Scan 只读 ts + c1(2 列)
→ I/O:减少 98%
→ 解压时间也减少 98%
列裁剪触发条件:
- SELECT 明确列出列名(非 *)
- 列在 WHERE/GROUP BY/ORDER BY 中也算被引用
- 列裁剪规则在 Logical Plan 优化阶段标记
6. 谓词下推到 Scan
WHERE 条件的执行位置:
SELECT * FROM meters
WHERE ts > now-1h
AND current > 10
AND voltage < 250
传统:Filter 算子在 Scan 上方
Filter
└── Scan (返回所有行)
谓词下推:条件传给 Scan 算子
Scan (filter = current>10 AND voltage<250)
下推后:
- 在解压时即过滤
- 不构造无用的 DataBlock
- 减少向上层传输的数据量
下推的条件:
✓ 简单列比较:col op constant
✓ AND 连接的简单谓词
✓ 列在比较的左侧(避免函数包裹)
部分下推(一部分进 Scan,一部分留在 Filter):
- WHERE simple_cond AND complex_expr
→ simple_cond 下推
→ complex_expr 仍在 Filter
7. TagScan(仅扫 Tag)
TagScan 的场景:
SELECT DISTINCT location FROM meters
→ 只需读 Tag,不读数据列
→ TagScan 直接读 META → 跳过 TSDB
SELECT TBNAME, location FROM meters
→ TagScan 输出子表名 + Tag 值
对比 TableScan:
- TableScan: 读 TSDB(.data 文件) + META
- TagScan: 仅读 META → 速度极快(毫秒级)
典型应用:
SHOW TABLES → TagScan
SHOW TABLES LIKE 'd1%' → TagScan + 字符串过滤
SELECT 子表元信息 → TagScan
8. 系统表扫描
SystemTableScan:
访问 information_schema 和 performance_schema 表:
SELECT * FROM information_schema.ins_tables;
SELECT * FROM information_schema.ins_columns;
SELECT * FROM performance_schema.perf_queries;
这类查询:
- 不走 TSDB,直接查 Mnode 内存元信息
- 数据量小(通常元数据 < 1MB)
- 适合监控、运维脚本
注意:
- ins_tables 在大集群可能返回百万行
- 建议带 LIMIT 或过滤条件
代码示例
触发各种 Scan 优化
sql
-- 优化 1:时间裁剪 + 列裁剪
SELECT ts, current
FROM meters
WHERE ts BETWEEN '2024-06-01' AND '2024-06-02';
-- 优化 2:Tag 索引
SELECT * FROM meters
WHERE location IN ('BJ','SH','GZ')
AND ts > now-1h;
-- 优化 3:SMA 加速(无数据过滤条件)
SELECT COUNT(*), AVG(current)
FROM meters
WHERE ts > now-1d;
-- 优化 4:TagScan(只查 Tag)
SELECT DISTINCT location FROM meters;
反优化案例
sql
-- 反例 1:函数包裹 ts → 失去时间裁剪
SELECT * FROM meters
WHERE TO_CHAR(ts, 'yyyy-mm') = '2024-06';
-- 改为:WHERE ts >= '2024-06-01' AND ts < '2024-07-01'
-- 反例 2:函数包裹 Tag → 失去索引
SELECT * FROM meters
WHERE LOWER(location) = 'bj';
-- 改为:保证写入时已规范化
-- 反例 3:SELECT * → 无法列裁剪
SELECT * FROM meters WHERE ts > now-1h;
-- 改为:SELECT ts, current, voltage FROM ...
性能考量
各种扫描的延迟参考
| 场景 | 数据规模 | 延迟 |
|---|---|---|
| 单子表 + 时间范围 | 1 小时数据 | 1~10ms |
| 全表 + SMA 聚合 | 万子表 × 1 天 | 50~200ms |
| Tag 索引命中 | 100/百万子表 | 5~20ms |
| TagScan | 万子表 | 10~50ms |
| 全表 + 数据过滤 | 万子表 × 1 天 | 500ms~5s |
| 全表无时间条件 | 数月数据 | 数分钟(不推荐) |
Scan 阶段的优化清单
- WHERE 包含时间范围
- Tag 条件等值或 IN
- SELECT 列明确
- 避免函数包裹列
- 聚合查询尽量不带数据列过滤(享受 SMA)
FAQ
Q1: 没有时间条件的 SELECT 危险吗?
非常危险。会触发全 File Set 扫描,可能涉及数百 GB 数据。生产环境应在客户端强制要求时间条件,或在表结构上加合理的 KEEP 限制总数据量。
Q2: SMA 文件占多大空间?
每个数据块对应一个 SMA 记录(约几十字节)。SMA 总大小通常是 .data 文件的 1%~5%。收益(查询加速)远大于空间代价。
Q3: ORDER BY ts DESC 如何执行?
TableScan 支持指定扫描方向。ORDER BY ts DESC 时,Scan 从最新数据反向读取,无需全量排序。但只有"按 ts 排序"才能这样优化,其他列的 ORDER BY 需要显式 Sort 算子。
Q4: PARTITION BY tbname 与逐表查询哪个快?
PARTITION BY tbname 由查询引擎统一调度,比应用层逐表查询发起 N 次请求要快得多。引擎内部会按 VGroup 并行处理。
参考
系统构架篇
- 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 逻辑计划生成》