TDengine 查询引擎设计与最佳实践

TDengine 查询引擎设计与最佳实践

一、设计理念

TDengine 查询引擎专为时序数据场景优化,核心目标是:充分利用时序数据特征,通过智能优化和分布式计算,实现毫秒级查询响应

核心设计原则

  • 标签先行:先过滤标签,再扫描数据,大幅减少数据扫描量
  • 预计算优化:利用 SMA 统计信息,避免读取原始数据
  • 分布式并行:多 vnode 并行计算,充分利用集群资源
  • 智能缓存:多级缓存机制,加速热点数据访问

二、查询架构总览

复制代码
┌────────────────────────────────────────────────────┐
│                应用程序 (Application)           	 │
└────────────────┬───────────────────────────────────┘
                 ↓
┌────────────────────────────────────────────────────┐
│              taosc (客户端驱动)             	     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │ SQL解析  │→│ 查询优化 │→│ 任务调度 │	             │
│  │  (AST)   │  │  (优化器) │  │ (调度器) │		     │
│  └──────────┘  └──────────┘  └──────────┘          │
└────────┬───────────────────────────┬───────────────┘
         ↓                           ↓
┌────────────────┐          ┌────────────────┐
│  mnode (元数据) │          │  vnode (数据) 	 │
│  - 表元数据   	 │          │  - 数据扫描   	 │
│  - vgroup信息 	 │          │  - 标签过滤 	 │
└────────────────┘          │  - 聚合计算	 │
                            └────────────────┘
                                    ↓
                            ┌────────────────┐
                            │  qnode (计算) 	 │
                            │  - 多表聚合   	 │
                            │  - 复杂计算   	 │
                            └────────────────┘

三、查询处理流程

3.1 完整查询流程

复制代码
┌─────────────────────────────────────────────────────┐
│ 第1步:SQL 解析与元数据获取            	      	      │
├─────────────────────────────────────────────────────┤
│ taosc 解析 SQL → 生成 AST                     	      │
│         ↓                               	   	  	  │
│ Catalog 向 mnode/vnode 请求元数据    	              │
│         ↓                                           │
│ 权限检查、语法校验、合法性校验                           │
└─────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────┐
│ 第2步:生成逻辑计划并优化                               │
├─────────────────────────────────────────────────────┤
│ 生成逻辑查询计划                                       │
│         ↓                                           │
│ 应用优化策略(谓词下推、投影下推等)                      │
│         ↓                                           │
│ 根据 vgroup 和 qnode 信息生成物理计划                   │
└─────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────┐
│ 第3步:任务调度                                       │
├─────────────────────────────────────────────────────┤
│ 调度器将子任务分发到 vnode/qnode                       │
│ 考虑数据亲缘性和负载均衡                                │
└─────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────┐
│ 第4-5步:执行查询                           	      │
├─────────────────────────────────────────────────────┤
│ vnode/qnode 接收任务 → 执行队列           	          │
│         ↓                                           │
│ 建立查询执行环境 → 执行查询 → 通知结果就绪                │
└─────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────┐
│ 第6-8步:结果返回                            	      │
├─────────────────────────────────────────────────────┤
│ taosc 按执行计划依次完成任务调度               	      │
│         ↓                                     	  │
│ 向查询节点发送数据获取请求        	                  │
│         ↓                                           │
│ 返回查询结果给应用程序   	    	                      │
└─────────────────────────────────────────────────────┘

3.2 超级表聚合查询流程(核心优势)

sql 复制代码
-- 典型的超级表聚合查询
SELECT location, AVG(temperature), MAX(humidity)
FROM sensors
WHERE ts >= '2024-01-01' AND ts < '2024-01-02'
  AND location IN ('北京', '上海')
GROUP BY location;

执行流程

复制代码
第1步:taosc → mnode 获取 sensors 超级表元数据
       返回:vgroup 分布信息、表结构信息

第2步:标签过滤(在元数据中完成,极快)
       location IN ('北京', '上海')
       ↓ 内存中过滤,确定需要扫描的子表集合
       找到:sensor_bj_001, sensor_bj_002, ..., sensor_sh_001, ...

第3步:时间过滤 + 数据分区定位
       ts >= '2024-01-01' AND ts < '2024-01-02'
       ↓ 根据时间范围计算文件组编号
       直接定位到对应的数据文件(无需扫描其他时间段)

第4步:并行计算(多 vnode 同时执行)
       vnode1: 扫描北京区域传感器数据 → 局部聚合
       vnode2: 扫描上海区域传感器数据 → 局部聚合
       vnode3: 扫描其他区域传感器数据 → 局部聚合

第5步:全局聚合(qnode 执行)
       qnode 收集各 vnode 的局部结果 → 最终聚合
       返回:location='北京', avg_temp=25.5, max_hum=60
              location='上海', avg_temp=26.3, max_hum=65

性能优势:
✓ 标签过滤在内存完成,毫秒级
✓ 时间分区直接定位文件,无需全表扫描
✓ 多 vnode 并行计算,速度线性提升
✓ 数据扫描量减少 90%+

四、核心查询优化技术

4.1 标签与数据分离存储

设计理念:标签数据与时序数据完全分离

复制代码
元数据层(META - B+Tree 存储):
├─ 超级表 sensors
│  ├─ schema: (ts TIMESTAMP, temperature FLOAT, humidity INT)
│  └─ tags: (location BINARY(64), device_id INT)
│
├─ 子表 sensor_001
│  └─ tags: {location: '北京', device_id: 1}
│
├─ 子表 sensor_002
│  └─ tags: {location: '上海', device_id: 2}
└─ ...

时序数据层(TSDB - LSM 存储):
├─ sensor_001 数据块
│  ├─ [时间列] [温度列] [湿度列]
│  └─ 压缩存储
│
├─ sensor_002 数据块
└─ ...

查询优势

sql 复制代码
-- 1. 标签过滤查询(极快)
SELECT COUNT(*) FROM sensors WHERE location = '北京';

执行:
1. 在元数据层扫描标签 → 找到 100 张北京的表
2. 只扫描这 100 张表的数据
   vs 传统方式:扫描全部 10000 张表
   
速度提升:100x+

-- 2. 标签更新(无需重写数据)
ALTER TABLE sensor_001 SET TAG location = '天津';

执行:
只更新元数据层的标签值,不涉及时序数据
速度:毫秒级

4.2 预计算(SMA)优化

BRIN 索引 + SMA 统计信息

c 复制代码
// 数据块统计信息(head 文件中)
struct DataBlockMeta {
    // 时间范围
    int64_t  minTimestamp;     // 2024-01-01 00:00:00
    int64_t  maxTimestamp;     // 2024-01-01 01:00:00
    
    // 预计算统计
    double   minValue[3];      // [20.5, 18.2, 50]  (温度、湿度、压力)
    double   maxValue[3];      // [30.8, 28.6, 70]
    double   sum[3];           // [25500, 23000, 60000]
    int32_t  count;            // 1000 条记录
    
    // 数据位置
    uint64_t dataOffset;       // 数据在 data 文件中的偏移
    uint32_t dataLength;       // 数据块大小
};

查询加速示例

sql 复制代码
-- 查询最大值(无需读取原始数据)
SELECT MAX(temperature), MIN(humidity), AVG(pressure)
FROM sensor_001
WHERE ts >= '2024-01-01' AND ts < '2024-01-02';

执行过程:
1. 根据时间范围定位数据文件组
2. 读取 head 文件中的 BRIN 索引(几 KB)
3. 扫描匹配时间范围的数据块元数据
4. 直接使用预计算值:
   - MAX(temperature) = max(所有块的maxValue[0])
   - MIN(humidity) = min(所有块的minValue[1])
   - AVG(pressure) = sum(所有块的sum[2]) / sum(所有块的count)
5. 返回结果

I/O 节省:99%+(只读索引,不读数据文件)
查询速度:毫秒级 vs 秒级

适用查询类型

  • ✅ COUNT、SUM、AVG、MAX、MIN
  • ✅ 大时间范围的聚合查询
  • ✅ 多表聚合统计

4.3 多级缓存机制

复制代码
┌───────────────────────────────────────────┐
│         缓存层次结构                        │
├───────────────────────────────────────────┤
│ L1: 元数据缓存(taosc)        	            │
│     - 表 schema                          	│
│     - vgroup 路由信息                    	│
│     - leader vnode 位置                 	│
│     策略:LRU,固定大小             	  	    │
│     命中率:90%+                          	│
├───────────────────────────────────────────┤
│ L2: 时序数据缓存(vnode 内存)       	    │
│     - 最新写入的数据                     	│
│     - SkipList 组织             	        │
│     策略:写驱动缓存              	        │
│     命中率(最新数据):99%+   	            │
├───────────────────────────────────────────┤
│ L3: last/last_row 缓存(vnode)       	    │
│     - 每张表的最后一条记录                	│
│     - 每列的最后一个非 NULL 值              	│
│     策略:LRU,延迟加载                     	│
│     命中率:95%+                      	    │
└───────────────────────────────────────────┘

缓存优化查询示例

sql 复制代码
-- 1. 最新数据查询(L2 缓存)
SELECT * FROM sensor_001 
WHERE ts >= NOW - 1h;

执行:直接从 vnode 内存缓存返回
响应时间:< 1ms

-- 2. Last Row 查询(L3 缓存)
SELECT LAST_ROW(*) FROM sensor_001;

执行:从 last_row 缓存返回
响应时间:< 1ms

-- 3. Last 查询(L3 缓存)
SELECT LAST(temperature), LAST(humidity) 
FROM sensors 
GROUP BY tbname;

执行:
1. 标签过滤找到所有子表
2. 从 last 缓存读取每张表的最后非 NULL 值
3. 组装返回

响应时间:毫秒级(vs 秒级)

4.4 查询策略选择

TDengine 提供 4 种查询策略(queryPolicy 配置):

sql 复制代码
-- 配置文件 taos.cfg
queryPolicy 1  -- 默认:仅使用 vnode
queryPolicy 2  -- 混合模式:vnode + qnode
queryPolicy 3  -- 存算分离:扫描用 vnode,计算用 qnode
queryPolicy 4  -- 客户端聚合:taosc 聚合

策略对比

策略 适用场景 优势 劣势
1 (vnode only) 简单查询、小数据量 延迟低、简单 vnode 负载高
2 (混合模式) 一般场景 平衡性能和资源 配置复杂
3 (存算分离) 复杂聚合、大数据量 充分利用 qnode 计算能力 网络传输开销
4 (客户端聚合) 小规模集群 减轻服务端压力 客户端性能要求高

五、SQL 扩展特性

5.1 PARTITION BY(分组扩展)

sql 复制代码
-- 按设备分组,计算每个设备的统计信息
SELECT location, device_id, 
       AVG(temperature) as avg_temp,
       STDDEV(temperature) as std_temp
FROM sensors
WHERE ts >= '2024-01-01'
PARTITION BY location, device_id;

执行优势:
- 数据按分组键切分
- 每组内独立计算,可并行
- 支持窗口函数和复杂运算

5.2 SLIMIT 与 SOFFSET(分组限制)

sql 复制代码
-- 限制返回的分组数量(不是记录数)
SELECT AVG(temperature)
FROM sensors
PARTITION BY device_id
SLIMIT 10 OFFSET 20;

说明:
- SLIMIT: 返回 10 个分组
- SOFFSET: 跳过前 20 个分组
- LIMIT: 限制每组内的记录数

5.3 窗口查询

sql 复制代码
-- 1. 时间窗口
SELECT _wstart, _wend, AVG(temperature)
FROM sensor_001
WHERE ts >= '2024-01-01'
INTERVAL(10m) SLIDING(5m);

-- 2. 会话窗口
SELECT _wstart, _wend, COUNT(*)
FROM sensor_001
SESSION(ts, 1h);

-- 3. 状态窗口
SELECT _wstart, _wend, COUNT(*)
FROM sensor_001
STATE_WINDOW(status);

-- 4. 事件窗口
SELECT _wstart, _wend, SUM(value)
FROM sensor_001
EVENT_WINDOW START WITH status=1 END WITH status=0;

5.4 时序特有的 JOIN

sql 复制代码
-- ASOF JOIN(时间匹配)
SELECT a.ts, a.temperature, b.humidity
FROM sensor_temp a
ASOF JOIN sensor_hum b
ON a.ts >= b.ts;

-- WINDOW JOIN(窗口关联)
SELECT a.location, AVG(a.temperature), AVG(b.humidity)
FROM sensor_temp a
WINDOW JOIN sensor_hum b
ON a.device_id = b.device_id
INTERVAL(1h);

六、查询最佳实践

6.1 ✅ 推荐的查询方法

1. 充分利用超级表 + 标签过滤
sql 复制代码
-- ✅ 好:先过滤标签,再扫描数据
SELECT AVG(temperature)
FROM sensors
WHERE location = '北京'      -- 标签过滤
  AND ts >= '2024-01-01'     -- 时间过滤
GROUP BY device_id;

性能:毫秒级
原因:
1. 标签过滤在元数据层完成,内存操作
2. 只扫描北京地区的表,数据量减少 90%+
3. 时间过滤直接定位文件组
2. 使用预计算函数
sql 复制代码
-- ✅ 好:使用 MAX/MIN/AVG 等预计算
SELECT MAX(temperature), MIN(temperature), AVG(humidity)
FROM sensors
WHERE ts >= '2024-01-01' AND ts < '2024-02-01';

性能:毫秒级
原因:直接使用 BRIN 索引中的统计信息,不读原始数据
3. 查询最新数据
sql 复制代码
-- ✅ 好:查询最新数据,命中缓存
SELECT * FROM sensor_001
WHERE ts >= NOW - 1h;

-- ✅ 好:使用 LAST_ROW
SELECT LAST_ROW(*) FROM sensor_001;

-- ✅ 好:使用 LAST
SELECT LAST(temperature) FROM sensor_001;

性能:< 1ms
原因:数据在内存缓存或 last/last_row 缓存中
4. 合理使用时间范围
sql 复制代码
-- ✅ 好:精确的时间范围
SELECT * FROM sensor_001
WHERE ts >= '2024-01-01 00:00:00'
  AND ts < '2024-01-01 01:00:00';

性能:快
原因:直接定位 1 小时的数据文件,扫描量最小
5. 利用窗口聚合
sql 复制代码
-- ✅ 好:使用时间窗口
SELECT _wstart, AVG(temperature)
FROM sensor_001
WHERE ts >= '2024-01-01'
INTERVAL(1h);

性能:快
原因:一次扫描完成分组聚合,高效
6. 批量查询
sql 复制代码
-- ✅ 好:一次查询多个指标
SELECT 
    AVG(temperature) as avg_temp,
    MAX(temperature) as max_temp,
    STDDEV(temperature) as std_temp,
    AVG(humidity) as avg_hum
FROM sensor_001
WHERE ts >= '2024-01-01';

性能:快
原因:一次数据扫描完成多个计算

6.2 ❌ 要避免的查询方法

1. 避免全表扫描
sql 复制代码
-- ❌ 差:无时间条件的全表扫描
SELECT * FROM sensor_001;

-- ❌ 差:无标签过滤的超级表查询
SELECT * FROM sensors;

问题:扫描全部数据,极慢
优化:
-- ✅ 好:加上时间条件
SELECT * FROM sensor_001
WHERE ts >= NOW - 7d;
2. 避免在数据列上过滤
sql 复制代码
-- ❌ 差:用数据列而非标签过滤
SELECT * FROM sensors
WHERE temperature > 25;    -- 需扫描所有数据

-- ✅ 好:用标签过滤
SELECT * FROM sensors
WHERE location = '北京'    -- 标签过滤,内存完成
  AND temperature > 25;    -- 数据过滤,扫描量已减少
3. 避免复杂子查询
sql 复制代码
-- ❌ 差:嵌套子查询
SELECT * FROM sensors
WHERE device_id IN (
    SELECT device_id FROM sensors
    WHERE temperature > 30
);

问题:多次扫描,性能差

-- ✅ 好:用 JOIN 或合并条件
SELECT DISTINCT device_id, AVG(temperature)
FROM sensors
WHERE temperature > 30
GROUP BY device_id;
4. 避免不必要的 DISTINCT
sql 复制代码
-- ❌ 差:对大数据集使用 DISTINCT
SELECT DISTINCT * FROM sensors
WHERE ts >= '2024-01-01';

问题:需要排序去重,内存占用大

-- ✅ 好:明确需要去重的列
SELECT DISTINCT device_id FROM sensors;
5. 避免 SELECT *
sql 复制代码
-- ❌ 差:查询所有列
SELECT * FROM sensors
WHERE ts >= '2024-01-01'
LIMIT 10;

-- ✅ 好:只查询需要的列
SELECT ts, temperature, humidity FROM sensors
WHERE ts >= '2024-01-01'
LIMIT 10;

原因:列式存储,只读取需要的列可节省 I/O
6. 避免跨大时间范围的计算
sql 复制代码
-- ❌ 差:跨年的 DIFF 计算
SELECT DIFF(value) FROM sensor_001
WHERE ts >= '2023-01-01' AND ts < '2024-12-31';

问题:DIFF 需要前后数据关联,跨度大性能差

-- ✅ 好:限制时间范围
SELECT DIFF(value) FROM sensor_001
WHERE ts >= NOW - 7d;
7. 避免在 WHERE 中使用函数
sql 复制代码
-- ❌ 差:WHERE 中使用函数
SELECT * FROM sensors
WHERE TO_TIMESTAMP(ts) >= '2024-01-01';

问题:无法利用索引,全表扫描

-- ✅ 好:直接比较
SELECT * FROM sensors
WHERE ts >= '2024-01-01 00:00:00';

七、性能优化建议

7.1 表设计优化

sql 复制代码
-- ✅ 好:合理设计标签
CREATE STABLE sensors (
    ts TIMESTAMP,
    temperature FLOAT,
    humidity INT
) TAGS (
    location BINARY(64),      -- 常用于过滤的字段
    building BINARY(64),      -- 常用于分组的字段
    floor INT,                -- 常用于统计的字段
    device_type BINARY(32)    -- 常用于分类的字段
);

设计原则:
1. 常用过滤条件作为标签
2. 标签数量适中(5-10 个)
3. 标签值不宜过多变化

7.2 索引优化

sql 复制代码
-- ✅ 好:创建标签索引
CREATE SMA INDEX temp_idx ON sensors FUNCTION(MAX(temperature), MIN(temperature))
INTERVAL(1h);

-- ✅ 好:创建时间分区
CREATE DATABASE sensor_db
    DURATION 10d           -- 每个文件组 10 天数据
    KEEP 365d;            -- 保留 365 天

7.3 查询优化配置

sql 复制代码
-- 1. 启用 last/last_row 缓存
CREATE DATABASE sensor_db
    CACHEMODEL 'both';    -- 启用 last 和 last_row 缓存

-- 2. 配置查询策略
-- 在 taos.cfg 中
queryPolicy 3            -- 存算分离,适合复杂查询

-- 3. 增加 vnode 内存
-- 在 taos.cfg 中
buffer 256               -- 每个 vnode 的写缓存(MB)

八、查询性能对比

案例:1000 万条记录的聚合查询

sql 复制代码
-- 查询:计算 24 小时平均温度
SELECT AVG(temperature) FROM sensor_001
WHERE ts >= '2024-01-01' AND ts < '2024-01-02';
优化方法 查询时间 优化效果
无优化(全表扫描) 5000ms 基准
+ 时间分区 500ms 10x
+ 列式存储 200ms 25x
+ BRIN 索引 + SMA 10ms 500x
+ 内存缓存(最新数据) < 1ms 5000x

案例:超级表多表聚合

sql 复制代码
-- 查询:10000 张表的温度统计
SELECT location, AVG(temperature)
FROM sensors
WHERE ts >= '2024-01-01'
GROUP BY location;
优化方法 查询时间 数据扫描量
传统数据库 60000ms 100%
TDengine(标签过滤) 2000ms 10%
TDengine(标签 + 并行) 200ms 10%
TDengine(全优化) 50ms < 1%

提升:1200x

九、总结

TDengine 查询引擎核心优势

  1. 标签与数据分离:内存标签过滤,减少 90%+ 数据扫描
  2. 时间分区:O(1) 时间定位数据文件,无需索引
  3. 预计算优化:BRIN + SMA,聚合查询不读原始数据
  4. 多级缓存:元数据、时序数据、最新数据多级缓存
  5. 分布式并行:多 vnode 并行计算,线性性能提升
  6. 列式存储:按列压缩和读取,I/O 节省 90%+

最佳实践核心要点

推荐做法

  • ✅ 使用超级表 + 标签过滤
  • ✅ 指定明确的时间范围
  • ✅ 查询最新数据(命中缓存)
  • ✅ 使用预计算函数(MAX/MIN/AVG)
  • ✅ 合理使用窗口查询
  • ✅ 只查询需要的列

避免做法

  • ❌ 全表扫描
  • ❌ 在数据列上过滤
  • ❌ 复杂嵌套子查询
  • ❌ 不必要的 DISTINCT
  • ❌ SELECT *
  • ❌ WHERE 中使用函数

性能提升数据

  • 标签过滤查询:100x ~ 1000x 提升
  • 预计算查询:100x ~ 500x 提升
  • 最新数据查询:1000x ~ 5000x 提升
  • 超级表聚合:100x ~ 1200x 提升

TDengine 查询引擎通过深度优化时序数据场景,实现了传统数据库难以企及的查询性能,特别是在多表聚合、时间范围查询和统计分析场景下,性能优势极其显著。


关于 TDengine

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

相关推荐
我命由我123452 小时前
Java 开发使用 MyBatis PostgreSQL 问题:使用了特殊字符而没有正确转义
java·开发语言·数据库·postgresql·java-ee·mybatis·学习方法
共享家95272 小时前
MySQL 数据类型
android·数据库·mysql
深蓝易网2 小时前
MES系统如何帮助企业实现产品质量的全过程追溯
大数据·人工智能
思成不止于此2 小时前
【MySQL 零基础入门】DQL 核心语法(三):学生表排序查询与分页查询篇
数据库·笔记·学习·mysql
听风吟丶2 小时前
微服务调用链追踪实战:用 SkyWalking 实现全链路可视化与故障溯源
数据库
云山工作室2 小时前
基于物联网的体温心率监测系统设计(论文+源码)
stm32·单片机·嵌入式硬件·物联网·课程设计
乐迪信息2 小时前
乐迪信息:AI摄像机+反光衣佩戴检测,保障智慧煤矿人员作业安全
大数据·运维·人工智能·物联网·安全
moluzhui2 小时前
mysql 修复插入零日期报错
数据库·mysql
不会c嘎嘎2 小时前
MySQL -- 深入剖析事务底层原理
数据库·mysql