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 FilterTag 索引过滤(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 > now-1h;

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

-- 优化 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 > 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 并行处理。

参考

系统构架篇

数据模型

存储引擎

查询引擎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TDengine (老段)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值