欢迎光临
我们一直在努力

TDengine 扫描算子 — TableScan、TagScan 与下推优化

分类: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 优化

— 优化 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 > now1h;

— 优化 3:SMA 加速(无数据过滤条件)
SELECT COUNT(*), AVG(current)
FROM meters
WHERE ts > now1d;

— 优化 4:TagScan(只查 Tag)
SELECT DISTINCT location FROM meters;

反优化案例

— 反例 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 > now1h;
— 改为: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 逻辑计划生成》

查询引擎

  • 01-《TDengine 查询引擎概览》
  • 02-《TDengine SQL 解析与词法分析》
  • 03-《TDengine 语义分析与 AST 重写》
  • 04-《TDengine 逻辑计划生成》
  • 05-《TDengine 物理计划生成》
赞(0)
未经允许不得转载:171主机测评 » TDengine 扫描算子 — TableScan、TagScan 与下推优化
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址