分类 :4.查询引擎 | 篇章:07 聚合算子

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-14
聚合是 OLAP 查询的核心。TDengine 针对时序数据特征提供 Hash Aggregate(无序输入)、Stream Aggregate(已排序输入)、Partial/Final Aggregate(分布式两阶段)三种核心实现,配合 SMA 预聚合机制实现极致性能。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Hash Aggregate | 用哈希表分组的通用聚合 |
| Stream Aggregate | 输入已按分组键排序的流式聚合 |
| Partial Aggregate | 本地预聚合(保留中间状态) |
| Final Aggregate | 合并 Partial 结果的最终聚合 |
| Aggregation State | 聚合中间状态(如 AVG 需要 SUM 和 COUNT) |
| TBNAME 分组 | 按子表分组(时序常用) |
详细解析
1. Hash Aggregate 工作原理
Hash Aggregate 算法:
输入数据流(无序):
(BJ, 25.3), (SH, 30.1), (BJ, 26.0), (GZ, 28.5), (SH, 31.2), ...
执行:
① 维护哈希表:Key=分组键, Value=聚合状态
② 对每行:
a. 计算分组键的 hash
b. 在哈希表中查找/创建条目
c. 用当前行更新聚合状态
③ 输入结束:输出哈希表所有条目
哈希表内容(GROUP BY location, AVG(current)):
┌──────────┬─────────────────────────┐
│ location │ state (sum, count) │
├──────────┼─────────────────────────┤
│ BJ │ (51.3, 2) │
│ SH │ (61.3, 2) │
│ GZ │ (28.5, 1) │
└──────────┴─────────────────────────┘
输出:
BJ → AVG = 51.3/2 = 25.65
SH → AVG = 61.3/2 = 30.65
GZ → AVG = 28.5/1 = 28.50
特点:
✓ 支持任意输入顺序
✗ 内存随分组键基数增长
✗ 必须读完所有输入才能输出
2. Stream Aggregate 工作原理
Stream Aggregate 算法(输入已排序):
输入数据(按分组键排序):
(BJ, 25.3), (BJ, 26.0), (GZ, 28.5), (SH, 30.1), (SH, 31.2), ...
执行:
① 维护当前分组键 + 当前聚合状态
② 对每行:
a. 如果分组键 = 当前键 → 更新状态
b. 如果分组键 ≠ 当前键 → 输出当前组结果 → 开新组
③ 输入结束:输出最后一组
执行轨迹:
BJ: state=(25.3, 1)
BJ: state=(51.3, 2)
切组 → 输出 BJ → AVG=25.65
GZ: state=(28.5, 1)
切组 → 输出 GZ → AVG=28.50
SH: state=(30.1, 1)
SH: state=(61.3, 2)
切组 → 输出 SH → AVG=30.65
特点:
✓ 内存占用 O(1)
✓ 流式输出(边算边出)
✗ 输入必须已按分组键排序
3. 两阶段聚合(Two-Phase Aggregation)
两阶段聚合的分布式执行:
阶段 1:Partial Aggregate(本地)
在每个 VNode 内独立执行
输出聚合的"中间状态",而非最终结果
AVG → 输出 (SUM, COUNT)
SUM → 输出 SUM
COUNT → 输出 COUNT
MIN → 输出 MIN
MAX → 输出 MAX
阶段 2:Final Aggregate(汇总)
在 QNode 或 Client 端执行
合并各 VNode 的中间状态
输出最终结果
AVG: SUM(partial_sum) / SUM(partial_count)
SUM: SUM(partial_sum)
COUNT: SUM(partial_count)
MIN: MIN(partial_min)
MAX: MAX(partial_max)
示例:
SELECT location, AVG(current), COUNT(*)
FROM meters
GROUP BY location;
VNode 1 (Partial):
BJ: (sum=100, cnt=4)
SH: (sum=60, cnt=2)
VNode 2 (Partial):
BJ: (sum=78, cnt=3)
GZ: (sum=85, cnt=3)
QNode (Final 合并):
BJ: AVG=(100+78)/(4+3)=25.43, CNT=7
SH: AVG=60/2=30, CNT=2
GZ: AVG=85/3=28.33, CNT=3
4. 常用聚合函数的状态
| 函数 | Partial 状态 | Final 合并 |
|---|---|---|
| COUNT | count | SUM(counts) |
| SUM | sum | SUM(sums) |
| MIN | min | MIN(mins) |
| MAX | max | MAX(maxs) |
| AVG | (sum, count) | SUM(sum)/SUM(count) |
| STDDEV | (sum, sum², count) | 数学合并 |
| FIRST | (first_value, ts_min) | 取 ts_min 最小者 |
| LAST | (last_value, ts_max) | 取 ts_max 最大者 |
| COUNT(DISTINCT col) | 不可两阶段 | 用 HyperLogLog 近似 |
5. 时序专属聚合函数
时序特色聚合:
① LAST_ROW(col1, col2, ...)
返回最后一行(按 ts)
不忽略 NULL(最后一行如果为 NULL 就返回 NULL)
② LAST(col1, col2, ...)
返回每列最后一个非 NULL 值
不同列可能来自不同行
③ FIRST(col)
返回最早一行的非 NULL 值
④ DIFF(col)
相邻行的差值
输入 [1, 3, 6, 10] → 输出 [_, 2, 3, 4]
⑤ DERIVATIVE(col, unit, ignore_negative)
瞬时变化率(适合累计计数器)
⑥ MAVG(col, k)
滑动平均(k 行窗口)
⑦ TWA(col)
时间加权平均(按时间间隔加权)
⑧ INTERP(col)
插值(按指定时间点)
⑨ PERCENTILE(col, p)
分位数
⑩ APERCENTILE(col, p)
近似分位数(更快)
6. SMA 加速聚合的判定
SMA 加速的判定流程:
查询:SELECT COUNT(*), AVG(current) FROM meters WHERE ts > T1
Scan 算子检查:
① 是否有数据列过滤?
- WHERE 只含 ts 和 Tag → 可用 SMA
- WHERE 含数据列条件 → 不可用 SMA
② 聚合函数是否支持?
- COUNT/SUM/MIN/MAX/AVG → 支持
- LAST/FIRST/PERCENTILE → 不支持
- DIFF/DERIVATIVE → 不支持
③ 都满足 → 直接读 .sma 文件
SMA 加速效果:
- 1 亿行 COUNT(*) → 读 .data 需 1s+
- 同样查询 + SMA → 读 .sma 仅 10ms
- 加速 100 倍
7. GROUP BY 与 PARTITION BY
两种分组的区别:
GROUP BY tbname:
聚合所有数据后按子表汇总
输出:每个子表一行
PARTITION BY tbname:
按子表分区,每分区独立处理
通常与窗口/排序结合使用
对比示例:
-- GROUP BY: 每个设备一行 LAST 值
SELECT tbname, LAST(current)
FROM meters
GROUP BY tbname;
-- PARTITION BY: 每个设备的最近 10 行
SELECT * FROM meters
PARTITION BY tbname
ORDER BY ts DESC
LIMIT 10;
-- PARTITION BY + 窗口: 每个设备的每小时 AVG
SELECT tbname, _wstart, AVG(current)
FROM meters
PARTITION BY tbname
INTERVAL(1h);
8. HAVING 子句的处理
HAVING 过滤聚合结果:
SELECT location, AVG(current) AS avg_c
FROM meters
GROUP BY location
HAVING avg_c > 20;
执行计划:
Filter (avg_c > 20) ← HAVING
│
▼
Final Aggregate (AVG)
│
▼
Exchange
│
▼
Partial Aggregate
│
▼
Scan
注意:
- WHERE 在聚合前过滤行
- HAVING 在聚合后过滤组
- HAVING 中可引用聚合函数
代码示例
各种聚合写法
sql
-- 基础聚合
SELECT COUNT(*), AVG(current), MAX(voltage)
FROM meters
WHERE ts > now-1d;
-- 多列分组
SELECT location, groupid, AVG(current)
FROM meters
GROUP BY location, groupid;
-- 按子表聚合
SELECT tbname, LAST(current), LAST(voltage)
FROM meters
GROUP BY tbname;
-- 时序专属
SELECT tbname, FIRST(current), LAST(current), DIFF(LAST(current)-FIRST(current))
FROM meters
WHERE ts > today()
GROUP BY tbname;
性能对比
sql
-- 利用 SMA(快)
SELECT COUNT(*) FROM meters WHERE ts > now-1d;
-- 不能用 SMA(慢)
SELECT COUNT(*) FROM meters WHERE ts > now-1d AND current > 10;
-- 改为两步可恢复 SMA 优势
WITH filtered AS (
SELECT current FROM meters WHERE ts > now-1d AND current > 10
)
SELECT COUNT(*) FROM filtered;
-- (实际效果取决于版本是否支持优化)
性能考量
聚合内存消耗
| 场景 | 内存占用 |
|---|---|
| 单个 COUNT(*) | O(1) |
| GROUP BY 低基数(< 100) | 几 KB |
| GROUP BY 中基数(万级) | 几 MB |
| GROUP BY tbname(百万) | 几百 MB(需 QNode) |
| COUNT(DISTINCT high_card) | 大量内存 → 用 APPROX |
聚合优化清单
- 优先使用 SMA 友好的函数
- 避免数据列过滤(破坏 SMA)
- 大基数 GROUP BY 用 QNode
- 用 APPROX_COUNT_DISTINCT 替代精确版本
- 必要时增大 queryBufferSize
FAQ
Q1: GROUP BY tbname 在百万子表时如何优化?
- 启用 QNode 隔离查询负载
- 增加 numOfVnodeQueryThreads
- 缩小时间范围减少每子表数据
- 考虑使用预聚合(TSMA)
Q2: 为什么 AVG 比 SUM/COUNT 慢?
理论上一样(都是两阶段)。如果实测 AVG 慢,可能是因为 AVG 状态需要传输两个值(sum + count)而 SUM 只传一个。差异通常微小。
Q3: LAST() 一定从 Cache 走吗?
启用 CACHEMODEL='last_value' 或 'both' 时优先走缓存。如果查询带 WHERE 数据列过滤(如 WHERE current > 10),缓存不可用,会退回扫描。
Q4: COUNT(DISTINCT) 为什么这么慢?
精确去重需要全表唯一值集合,内存随基数线性增长。替代方案:
APPROX_COUNT_DISTINCT()用 HyperLogLog,误差 ~1%- 预计算(流计算或定期 INSERT 汇总表)
参考
系统构架篇
- 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 物理计划生成》
- 06-《TDengine 扫描算子》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。