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 优化

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

参考

系统构架篇

数据模型

存储引擎

查询引擎

相关推荐
放下华子我只抽RuiKe52 小时前
FastAPI 全栈后端(三):数据库与 ORM
前端·数据库·react.js·oracle·性能优化·前端框架·fastapi
ACP广源盛139246256732 小时前
IX6012 PCIe 交换芯片@ACP#RTX Spark 入门级 12 口存储外设扩展方案(对比 ASM1812)
大数据·人工智能·分布式·嵌入式硬件·gpt·spark·电脑
加速财经2 小时前
体育赛事如何与数字互动结合?世界杯期间用户参与模式的新尝试
大数据
BAGAE2 小时前
星链卫星数据获取:从太空安全到实时通信的技术革命
网络·数据结构·数据库·算法·云计算·hbase
zh_xuan2 小时前
Android导出并查看数据库
数据库·sqlite
小短腿的代码世界2 小时前
Qt定时器高精度架构:从QTimer源码到纳秒级定时调度
数据库·qt·架构
雨辰AI2 小时前
从零搭建大模型本地运行环境|Python+CUDA 基础配置避坑大全
大数据·开发语言·人工智能·python·ai·ai编程·ai写作
herinspace2 小时前
管家婆辉煌软件如何新增往来单位档案分类
服务器·数据库·电脑·管家婆软件
程序猿乐锅2 小时前
【MySQL | 第九篇】MySQL 存储过程
数据库·mysql