聚合查询不能流式处理,因mysql需全量扫描、分组、计算后才返回结果,无法分块发送中间状态;oom主因是扫描范围过大而非结果集大。

聚合查询本身不支持流式处理——GROUP BY、SUM、COUNT 等操作必须由 MySQL 服务端一次性计算完成,结果集再返回客户端。所以直接对大表做 SELECT COUNT(*) FROM huge_table 或 SELECT user_id, SUM(amount) FROM orders GROUP BY user_id,哪怕只返回几万行聚合结果,只要中间过程或最终结果超出 JVM 堆内存,照样 OOM。
为什么聚合查询不能直接流式?
流式查询依赖数据库驱动边读边吐,前提是服务端能分块发送原始行;但聚合必须等全量数据扫描+分组+计算完才生成结果行。MySQL 不会把未完成的 GROUP BY 中间状态发给客户端。
-
SELECT COUNT(*)这种标量聚合,结果永远只有 1 行,OOM 几乎不可能——除非你写错了,比如漏了WHERE条件导致扫描整张亿级表,这时 OOM 是扫描过程耗尽内存,不是结果集导致的 -
GROUP BY类聚合的 OOM 风险点在:分组键基数高(如按create_time秒级分组)、或聚合字段本身是大文本(GROUP_CONCAT拼接超长字符串)、或未加有效过滤条件导致扫描行数爆炸 - 即使你在 JDBC 层设了
setFetchSize(Integer.MIN_VALUE),对聚合 SQL 也无效——驱动无法流式,MySQL 服务端根本不支持该协议语义
聚合前先缩小扫描范围(最有效)
90% 的聚合 OOM 实际源于全表扫描。重点不是“怎么流式聚合”,而是“怎么不让它扫全表”。
- 强制走索引:用
EXPLAIN看type是否为range或更优,key显示实际使用的索引;避免WHERE字段没索引、或用了函数/隐式转换(如WHERE DATE(create_time) = '2024-01-01') - 时间范围必须精确:字段是
DATETIME就传'2024-01-01 00:00:00',别传字符串让 MySQL 自己转;时区要和表字段一致(CONVERT_TZ比DATE()更安全) - 聚合键预筛选:比如想统计“近 7 天活跃用户订单金额”,先用子查询或临时表提取出这 7 天的
user_id集合,再对这个小集合做JOIN + GROUP BY - 避开高峰执行:聚合通常慢且重,不要在业务高峰期跑
SELECT ... GROUP BY,尤其涉及ORDER BY或LIMIT时,MySQL 可能建临时表并排序
替代方案:用分页 + 应用层合并
当聚合结果确实很大(如按天分组有 3650 行),且必须导出,可人工拆解为可流式的小单元。
- 按主键/时间分段:例如
WHERE create_time BETWEEN ? AND ?,每次取 1 天或 1 小时的数据做聚合,结果写入 CSV 后清空内存,循环执行 - 避免
OFFSET分页:改用游标式分页,如WHERE id > last_id ORDER BY id LIMIT 1000,防止大偏移导致重复扫描 - 应用层合并注意去重和精度:如果聚合含
AVG或STDDEV,不能简单平均各段结果,需保留各段的SUM和COUNT,最后统一算 - MyBatis-Plus 不支持聚合流式,但可以用
QueryWrapper动态拼WHERE时间条件,配合普通查询分批执行
真正需要流式的场景其实是“聚合前的明细”
很多所谓“聚合查询 OOM”,真实需求是:先查出明细,再在 Java 里 Collectors.groupingBy 或写入 Spark 处理。这时候流式就起作用了。
- 把
SELECT user_id, amount, create_time FROM orders WHERE ...改成流式查询(setFetchSize(Integer.MIN_VALUE)+TYPE_FORWARD_ONLY),逐条读、逐条分组累加,内存只存分组键和当前值 - MyBatis-Plus 流式要求
ResultHandler回调 + XML 中fetchSize="-2147483648",且 SQL 不能含聚合函数 - 注意连接独占:流式 ResultSet 未关闭前,该连接不能复用——别在回调里又发起新查询,否则抛
Streaming result set is still active
聚合查询的 OOM 很少是“结果太大”,大多是“扫描太宽”。与其纠结怎么流式聚合,不如花十分钟看一眼 EXPLAIN 输出——那个 rows 值是不是比你预想的多了一两个数量级。











