
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 查询引擎核心优势
- ✅ 标签与数据分离:内存标签过滤,减少 90%+ 数据扫描
- ✅ 时间分区:O(1) 时间定位数据文件,无需索引
- ✅ 预计算优化:BRIN + SMA,聚合查询不读原始数据
- ✅ 多级缓存:元数据、时序数据、最新数据多级缓存
- ✅ 分布式并行:多 vnode 并行计算,线性性能提升
- ✅ 列式存储:按列压缩和读取,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 提供实时分析、可视化、事件管理与报警等功能。