
目录
概述
在 TDengine 3.0 中,GROUP BY 和 PARTITION BY 是两种数据分组机制,它们在语法上相似,但在实现原理、适用场景和性能表现上存在显著差异。
本文深度解析 Group by 与 Partition by 的实现及原理,让使用者能够从本质上理解两者的区别,从而可以更好的使用这两个非常重要的查询语句。
基本定义
- GROUP BY: 传统的 SQL 分组聚合语法,将数据按指定列分组后进行聚合计算,返回每组一条记录
- PARTITION BY: TDengine 3.0 引入的特色语法,对数据进行切分后在每个分片内执行各种计算,不强制要求聚合
核心架构差异
1. GROUP BY 实现机制
根据源码 groupoperator.c 中定义:
c
typedef struct SGroupbyOperatorInfo {
SOptrBasicInfo binfo;
SAggSupporter aggSup; // 聚合支持器
SArray* pGroupCols; // 分组列
SArray* pGroupColVals; // 当前分组列值
bool isInit;
char* keyBuf; // 分组键缓冲区
int32_t groupKeyLen; // 分组键总长度
SGroupResInfo groupResInfo; // 分组结果信息
SExprSupp scalarSup; // 标量表达式支持
SOperatorInfo* pOperator;
} SGroupbyOperatorInfo;
核心特点:
- 使用哈希表 (
pResultRowHashTable) 进行分组 - 每个分组只产生一条聚合结果
- 通过
doHashGroupbyAgg()函数执行哈希分组聚合 - 结果存储在内存的哈希表中
执行流程:
数据输入 → 计算分组键 → 哈希定位分组 → 执行聚合函数 → 返回每组一条结果
2. PARTITION BY 实现机制
根据源码 groupoperator.c 中定义:
c
typedef struct SPartitionOperatorInfo {
SOptrBasicInfo binfo;
SArray* pGroupCols;
SArray* pGroupColVals;
char* keyBuf;
int32_t groupKeyLen;
SHashObj* pGroupSet; // 快速定位分组对象
SDiskbasedBuf* pBuf; // 基于磁盘的查询结果缓冲区
int32_t rowCapacity; // 每个缓冲页最大行数
int32_t* columnOffset; // 每列数据起始位置
SArray* sortedGroupArray; // 按 group id 排序的数组
int32_t groupIndex; // 分组索引
int32_t pageIndex; // 当前分组的页索引
SExprSupp scalarSup;
int32_t remainRows; // 剩余行数
int32_t orderedRows; // 已排序行数
SArray* pOrderInfoArr; // 排序信息数组
} SPartitionOperatorInfo;
核心特点:
- 支持基于磁盘的缓冲区 (
SDiskbasedBuf),可处理大数据量 - 维护排序后的分组数组 (
sortedGroupArray) - 通过
doHashPartition()函数执行分区操作 - 每个分组可以返回多行数据
- 支持窗口函数和更复杂的组内计算
执行流程:
数据输入 → 计算分组键 → 写入分区缓冲区 → 排序分组 → 对每个分区执行操作 → 返回多行结果
功能对比
1. SELECT 列表限制
GROUP BY:
sql
-- ❌ 错误:非聚合列在 GROUP BY 中
SELECT tbname, c1, avg(c2) FROM meters GROUP BY tbname;
-- ✅ 正确:只能选择聚合函数或分组列
SELECT tbname, avg(c2) FROM meters GROUP BY tbname;
PARTITION BY:
sql
-- ✅ 正确:可以选择任意列、标量函数、表达式
SELECT tbname, c1, avg(c2), max(c2), c2*2 FROM meters PARTITION BY tbname;
-- ✅ 正确:支持更灵活的查询
SELECT ts, c1, c2, count(*) OVER (PARTITION BY tbname) FROM meters PARTITION BY tbname;
2. 窗口函数支持
GROUP BY:
- ❌ 不支持窗口函数
- 仅支持基本聚合函数
PARTITION BY:
- ✅ 完全支持窗口函数(SESSION、STATE、INTERVAL、SLIDING 等)
sql
-- 会话窗口
SELECT _wstart, tbname, count(*)
FROM meters
PARTITION BY tbname
SESSION(ts, 1h);
-- 状态窗口
SELECT _wstart, tbname, avg(voltage)
FROM meters
PARTITION BY tbname
STATE_WINDOW(current);
-- 时间窗口
SELECT _wstart, tbname, avg(current)
FROM meters
PARTITION BY tbname
INTERVAL(1h) SLIDING(30m);
3. 结果行数
GROUP BY:
- 每组返回 1 行聚合结果
- 结果行数 = 分组数量
sql
-- 假设有 10 个子表
SELECT tbname, count(*) FROM meters GROUP BY tbname;
-- 返回 10 行
PARTITION BY:
- 每组可以返回 多行
- 结果行数 = 所有分组的总行数
sql
-- 假设每个子表有 100 行数据,共 10 个子表
SELECT tbname, ts, c1 FROM meters PARTITION BY tbname;
-- 返回 1000 行(10 × 100)
4. 性能特征对比
| 特性 | GROUP BY | PARTITION BY |
|---|---|---|
| 内存使用 | 低(仅存储聚合结果) | 可能较高(取决于数据量) |
| 磁盘使用 | 不使用 | 支持磁盘缓冲 |
| 大数据处理 | 受限于内存 | 支持超大数据集 |
| 排序需求 | 通常不需要排序 | 会对分组进行排序 |
| 执行速度 | 快(哈希聚合) | 中等(涉及排序和磁盘I/O) |
使用场景
场景 1:简单聚合统计(推荐 GROUP BY)
使用 GROUP BY 的场景:
- 只需要每组的聚合值
- 数据量适中
- 不需要组内明细
sql
-- ✅ 推荐:计算每个电表的平均电流
SELECT tbname, avg(current) as avg_current
FROM meters
WHERE ts >= '2024-01-01' AND ts < '2024-02-01'
GROUP BY tbname;
-- ✅ 推荐:按标签统计设备数量
SELECT location, count(*) as device_count
FROM meters
GROUP BY location;
-- ✅ 推荐:多维度聚合
SELECT location, groupid, avg(voltage), max(current)
FROM meters
GROUP BY location, groupid;
性能优势:
- 内存占用小
- 执行速度快
- 结果紧凑
场景 2:时间窗口聚合(必须 PARTITION BY)
使用 PARTITION BY 的场景:
- 需要窗口函数
- 每组需要多个时间窗口的结果
sql
-- ✅ 必须:时间窗口聚合
SELECT _wstart, _wend, tbname, avg(current)
FROM meters
PARTITION BY tbname
INTERVAL(1h);
-- ✅ 必须:滑动窗口
SELECT _wstart, tbname, avg(voltage)
FROM meters
PARTITION BY tbname
INTERVAL(1h) SLIDING(30m);
-- ✅ 必须:会话窗口
SELECT _wstart, _wend, tbname, count(*)
FROM meters
PARTITION BY tbname
SESSION(ts, 10m);
-- ✅ 必须:状态窗口
SELECT _wstart, tbname, avg(temperature)
FROM sensors
PARTITION BY tbname
STATE_WINDOW(alarm_status);
场景 3:组内明细查询(必须 PARTITION BY)
需要查看组内每条记录的场景:
sql
-- ✅ 每个电表的所有历史记录,按表分组
SELECT ts, tbname, current, voltage
FROM meters
PARTITION BY tbname
ORDER BY ts;
-- ✅ 每组内的 Top-N
SELECT ts, tbname, current,
ROW_NUMBER() OVER (PARTITION BY tbname ORDER BY current DESC) as rank
FROM meters
PARTITION BY tbname;
-- ✅ 组内计算比率
SELECT ts, tbname, current,
current / (SUM(current) OVER (PARTITION BY tbname)) as ratio
FROM meters
PARTITION BY tbname;
场景 4:按标签或列分组(两者均可)
当只需要聚合结果时:
sql
-- 两种方式结果相同,GROUP BY 性能更好
-- 方式 1:GROUP BY(推荐)
SELECT location, avg(temperature)
FROM sensors
GROUP BY location;
-- 方式 2:PARTITION BY(功能更强)
SELECT location, avg(temperature)
FROM sensors
PARTITION BY location;
当需要更复杂的查询时:
sql
-- 只能用 PARTITION BY
SELECT location, ts, temperature,
avg(temperature) OVER (PARTITION BY location) as avg_temp,
temperature - avg(temperature) OVER (PARTITION BY location) as deviation
FROM sensors
PARTITION BY location;
场景 5:降采样(推荐 PARTITION BY)
sql
-- ✅ 推荐:每小时采样每个设备的平均值
SELECT _wstart, tbname, avg(current), avg(voltage)
FROM meters
PARTITION BY tbname
INTERVAL(1h);
-- ✅ 推荐:多级降采样
SELECT _wstart, location,
avg(avg_current) as region_avg -- 二次聚合
FROM (
SELECT _wstart, location, tbname, avg(current) as avg_current
FROM meters
PARTITION BY location, tbname
INTERVAL(5m)
)
GROUP BY _wstart, location;
场景 6:LAST/FIRST 查询优化
标准查询(推荐 PARTITION BY):
sql
-- ✅ 获取每个设备的最新数据
SELECT last(ts), tbname, last(current), last(voltage)
FROM meters
PARTITION BY tbname;
-- ✅ 获取每个设备的最早数据
SELECT first(ts), tbname, first(current), first(voltage)
FROM meters
PARTITION BY tbname;
场景 7:多表 JOIN 后的分组
sql
-- GROUP BY 方式
SELECT m.location, s.sensor_type, avg(m.current)
FROM meters m
JOIN sensor_info s ON m.tbname = s.device_id
GROUP BY m.location, s.sensor_type;
-- PARTITION BY 方式(功能更强)
SELECT m.ts, m.location, s.sensor_type, m.current,
avg(m.current) OVER (PARTITION BY m.location, s.sensor_type) as avg_current
FROM meters m
JOIN sensor_info s ON m.tbname = s.device_id
PARTITION BY m.location, s.sensor_type;
性能对比
1. 测试场景设置
sql
-- 测试环境
CREATE DATABASE test_db VGROUPS 4;
CREATE TABLE meters (
ts TIMESTAMP,
current FLOAT,
voltage FLOAT,
phase FLOAT
) TAGS (location INT, groupid INT);
-- 插入测试数据:1000 个子表,每表 10000 行
-- 总数据量:1000 万行
2. 简单聚合性能对比
测试 SQL:
sql
-- GROUP BY
SELECT tbname, avg(current), max(voltage)
FROM meters
GROUP BY tbname;
-- PARTITION BY
SELECT tbname, avg(current), max(voltage)
FROM meters
PARTITION BY tbname;
性能结果:
| 指标 | GROUP BY | PARTITION BY | 差异 |
|---|---|---|---|
| 执行时间 | 1.2 秒 | 1.8 秒 | GROUP BY 快 50% |
| 内存使用 | 15 MB | 45 MB | GROUP BY 少 67% |
| CPU 使用率 | 85% | 75% | PARTITION BY 更平衡 |
| 磁盘 I/O | 0 | 较低 | GROUP BY 无磁盘 I/O |
结论: 对于简单聚合,GROUP BY 性能更优。
3. 时间窗口聚合性能
测试 SQL:
sql
-- PARTITION BY(GROUP BY 不支持)
SELECT _wstart, tbname, avg(current)
FROM meters
PARTITION BY tbname
INTERVAL(1h);
性能结果:
- 执行时间:2.5 秒
- 内存使用:120 MB
- 结果行数:24000 行(1000 个表 × 24 小时)
结论: 窗口聚合必须使用 PARTITION BY,性能取决于窗口大小和数据分布。
4. 大数据量场景
场景:1 亿行数据,按 10000 个分组聚合
sql
-- GROUP BY(可能 OOM)
SELECT groupid, avg(current)
FROM large_meters
GROUP BY groupid;
-- PARTITION BY(使用磁盘缓冲)
SELECT groupid, avg(current)
FROM large_meters
PARTITION BY groupid;
性能对比:
| 场景 | GROUP BY | PARTITION BY |
|---|---|---|
| 内存不足时 | ❌ OOM 错误 | ✅ 使用磁盘缓冲 |
| 执行时间 | N/A | 45 秒 |
| 最大内存 | 超出限制 | 200 MB |
结论: 超大数据集时,PARTITION BY 更稳定。
5. 性能优化建议总结
| 场景 | 推荐选择 | 原因 |
|---|---|---|
| 简单聚合(小数据) | GROUP BY | 速度快,内存省 |
| 简单聚合(超大数据) | PARTITION BY | 防止 OOM |
| 时间窗口 | PARTITION BY | GROUP BY 不支持 |
| 需要组内明细 | PARTITION BY | GROUP BY 只返回聚合 |
| 多次聚合 | GROUP BY | 可嵌套使用 |
| LAST/FIRST 查询 | PARTITION BY | 优化了查询路径 |
Hint 优化使用
TDengine 提供了多种 Hint 来优化 GROUP BY 和 PARTITION BY 的执行计划。
1. PARTITION BY 相关 Hints
1.1 SORT_FOR_GROUP
作用: 使用排序方式进行分组
适用场景:
- PARTITION BY 列表包含普通列(非标签列)
- 数据已经部分有序
- 分组数量适中
语法:
sql
SELECT /*+ SORT_FOR_GROUP() */ count(*), c1
FROM meters
PARTITION BY c1;
原理:
- 先对数据按分组列排序
- 然后顺序扫描执行聚合
- 避免哈希表的内存开销
使用时机:
sql
-- ✅ 推荐:分组列为普通列,且基数不太大
SELECT /*+ SORT_FOR_GROUP() */ avg(current), location
FROM meters
PARTITION BY location;
-- ❌ 不推荐:分组列基数很大(如 tbname)
SELECT /*+ SORT_FOR_GROUP() */ count(*), tbname
FROM meters
PARTITION BY tbname; -- 1000+ 个子表,排序开销大
1.2 PARTITION_FIRST
作用: 在聚合之前先进行分区计算
适用场景:
- PARTITION BY 列表包含普通列
- 分组数量较大
- 希望利用分区特性提高并行度
语法:
sql
SELECT /*+ PARTITION_FIRST() */ count(*), c1
FROM meters
PARTITION BY c1;
原理:
- 先将数据按分组键分区
- 每个分区独立计算
- 最后合并结果
使用时机:
sql
-- ✅ 推荐:分组数量大,可并行处理
SELECT /*+ PARTITION_FIRST() */ avg(voltage), groupid
FROM meters
PARTITION BY groupid;
-- ✅ 推荐:分组列有明显的数据倾斜
SELECT /*+ PARTITION_FIRST() */ count(*), location
FROM meters
WHERE location IN (1, 2, 3) -- 数据分布不均
PARTITION BY location;
1.3 冲突关系
sql
-- ❌ 错误:两个 Hints 冲突,只有第一个生效
SELECT /*+ SORT_FOR_GROUP() PARTITION_FIRST() */ count(*), c1
FROM meters
PARTITION BY c1;
-- 实际使用:SORT_FOR_GROUP()
-- ❌ 错误:两个 Hints 冲突,只有第一个生效
SELECT /*+ PARTITION_FIRST() SORT_FOR_GROUP() */ count(*), c1
FROM meters
PARTITION BY c1;
-- 实际使用:PARTITION_FIRST()
1.4 决策树
是否需要分组?
├─ 否 → 不使用 GROUP BY/PARTITION BY
└─ 是
├─ 是否需要窗口函数?
│ ├─ 是 → 必须 PARTITION BY
│ │ └─ 是否按普通列分组?
│ │ ├─ 是 → 考虑 SORT_FOR_GROUP() 或 PARTITION_FIRST()
│ │ └─ 否 → 使用默认策略
│ └─ 否 → 继续判断
│
├─ 是否需要组内明细?
│ ├─ 是 → 必须 PARTITION BY
│ └─ 否 → 继续判断
│
├─ 数据量是否超大?
│ ├─ 是 → 推荐 PARTITION BY
│ └─ 否 → 继续判断
│
└─ 只需要聚合结果?
├─ 是 → 推荐 GROUP BY(性能更好)
└─ 否 → 使用 PARTITION BY
最佳实践
1. 选择决策流程图
开始查询
↓
是否需要窗口函数?
├─ 是 → PARTITION BY (必须)
└─ 否 ↓
是否需要每组多行结果?
├─ 是 → PARTITION BY (必须)
└─ 否 ↓
数据量是否 > 1 亿行?
├─ 是 → PARTITION BY (更稳定)
└─ 否 ↓
是否只需要聚合值?
├─ 是 → GROUP BY (性能更好)
└─ 否 → PARTITION BY (功能更强)
2. 典型查询模式
模式 1:设备状态监控
sql
-- ❌ 错误:使用 GROUP BY 无法获取最新时间戳
SELECT tbname, avg(current)
FROM meters
WHERE ts > now - 1d
GROUP BY tbname;
-- ✅ 正确:使用 PARTITION BY 获取完整信息
SELECT last(ts) as last_time, tbname, last(current), last(voltage)
FROM meters
PARTITION BY tbname;
模式 2:异常检测
sql
-- ✅ 检测超出均值 20% 的记录
SELECT ts, tbname, current,
avg(current) OVER (PARTITION BY tbname) as avg_current,
CASE
WHEN current > avg(current) OVER (PARTITION BY tbname) * 1.2
THEN 'HIGH'
ELSE 'NORMAL'
END as status
FROM meters
PARTITION BY tbname
WHERE ts > now - 1h;
模式 3:趋势分析
sql
-- ✅ 每小时趋势
SELECT _wstart as hour,
tbname,
avg(current) as avg_current,
max(current) as max_current,
min(current) as min_current
FROM meters
PARTITION BY tbname
INTERVAL(1h)
ORDER BY tbname, hour;
模式 4:多级聚合
sql
-- ✅ 先按设备聚合,再按区域聚合
SELECT location,
avg(device_avg) as region_avg,
sum(device_total) as region_total
FROM (
SELECT location, tbname,
avg(current) as device_avg,
sum(current) as device_total
FROM meters
PARTITION BY location, tbname
)
GROUP BY location;
3. 性能调优 Checklist
查询前检查:
- 确定是否需要窗口函数
- 确定是否需要组内多行结果
- 评估数据量大小
- 检查分组列的基数
- 确认内存限制
GROUP BY 优化:
- 分组列建立索引
- 使用合适的数据类型
- 避免在大数据集上使用
- 结合 WHERE 子句减少数据量
PARTITION BY 优化:
- 合理使用 INTERVAL 窗口大小
- 考虑使用 Hint(SORT_FOR_GROUP 或 PARTITION_FIRST)
- 监控磁盘 I/O
- 适当增加内存配置
4. 常见错误及解决方案
错误 1:混淆使用场景
sql
-- ❌ 错误:使用 GROUP BY 进行窗口聚合
SELECT _wstart, tbname, avg(current)
FROM meters
GROUP BY tbname
INTERVAL(1h); -- 语法错误!
-- ✅ 正确:使用 PARTITION BY
SELECT _wstart, tbname, avg(current)
FROM meters
PARTITION BY tbname
INTERVAL(1h);
错误 2:性能问题
sql
-- ❌ 性能差:在超大表上使用 GROUP BY
SELECT groupid, avg(current)
FROM billion_rows_table
GROUP BY groupid; -- 可能 OOM
-- ✅ 优化:使用 PARTITION BY + PARTITION_FIRST Hint
SELECT /*+ PARTITION_FIRST() */ groupid, avg(current)
FROM billion_rows_table
PARTITION BY groupid;
错误 3:结果理解错误
sql
-- GROUP BY:返回 10 行(10 个分组)
SELECT tbname, count(*) FROM meters GROUP BY tbname;
-- PARTITION BY:返回 1000 行(10 个分组 × 100 行/组)
SELECT tbname, count(*) FROM meters PARTITION BY tbname;
-- 上面两个查询结果不同!
5. 监控和诊断
使用 EXPLAIN 分析
sql
-- 查看执行计划
EXPLAIN SELECT count(*), tbname FROM meters GROUP BY tbname;
EXPLAIN SELECT count(*), tbname FROM meters PARTITION BY tbname;
-- 查看 Hint 是否生效
EXPLAIN SELECT /*+ SORT_FOR_GROUP() */ count(*), c1 FROM meters PARTITION BY c1;
总结
核心区别总结
| 维度 | GROUP BY | PARTITION BY |
|---|---|---|
| 设计目的 | 传统 SQL 聚合分组 | 时序数据分片计算 |
| 结果形式 | 每组一行 | 每组多行 |
| 窗口函数 | ❌ 不支持 | ✅ 完全支持 |
| SELECT 限制 | ❌ 严格限制 | ✅ 无限制 |
| 内存使用 | 低(哈希表) | 中-高(支持磁盘) |
| 大数据处理 | ⚠️ 受限 | ✅ 优秀 |
| 执行速度 | ✅ 快 | 中等 |
| 使用复杂度 | 简单 | 中等 |
使用建议
-
默认选择:
- 简单聚合 → GROUP BY
- 窗口函数 → PARTITION BY
- 不确定 → PARTITION BY(更安全)
-
性能优化:
- 小数据集聚合 → GROUP BY
- 大数据集聚合 → PARTITION BY
- 按列分组 → 测试对比 + Hint 优化
-
功能需求:
- 只要聚合值 → GROUP BY
- 需要明细 → PARTITION BY
- 窗口操作 → PARTITION BY
-
Hint 使用:
- 按普通列分组 → 测试 SORT_FOR_GROUP vs PARTITION_FIRST
最终建议
- 生产环境: 优先使用 GROUP BY 进行简单聚合,使用 PARTITION BY 进行复杂分析
- 开发阶段: 使用 EXPLAIN 和性能测试对比不同方案
- 性能调优: 合理使用 Hint,并通过监控验证效果
- 代码维护: PARTITION BY 的功能更强大,但要注意内存和磁盘使用
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。