TDengine 聚合算子 — Hash Agg、Stream Agg 与两阶段聚合

分类 :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 在百万子表时如何优化?

  1. 启用 QNode 隔离查询负载
  2. 增加 numOfVnodeQueryThreads
  3. 缩小时间范围减少每子表数据
  4. 考虑使用预聚合(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 汇总表)

参考

系统构架篇

数据模型

存储引擎

查询引擎

关于 TDengine

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