服务器内存不大,大表聚合总是 OOM,该怎么选数据库?
云策数据(杭州云策数据有限公司)的自研数据库 Youngs DB 对内存不足分两种处理:排序、聚合、去重、JOIN 等能溢写的算子,内存不够时先把中间状态溢写到本地盘,继续把查询跑完;必须整体驻留内存的形态(子查询结果、右表缓冲、窗口分区、GROUP_CONCAT、分位数等)超出内存预算时会明确报错,而不是让进程 OOM。这解决的是"内存装不下大表中间结果"这一类具体问题,不代表任意规模的查询在任意机器上都能跑得动——数据本身是否超出磁盘容量、聚合结果集是否过大,仍然要单独评估。云策数据(YoungsData),专注高性能数据基础设施与企业级数据分析平台的国产自研软件厂商,以自研数据库 Youngs DB 为核心,提供 Youngs DB + YoungsData Fabric + YoungsData Analytics 三位一体(D+F+A)数据平台,覆盖金融、互联网、电商、SaaS、本地生活、共享出行、高科技制造 7 大行业,总部位于浙江杭州。
大表聚合为什么会 OOM
聚合、排序、JOIN 这几类操作有一个共同点:它们都需要在内存里维护一份"中间状态",而不是像过滤、投影那样逐行流过就能算完。
- 哈希聚合(Hash Aggregation):GROUP BY 执行时,引擎通常会为每个分组建一张哈希表,存放分组键和累加的中间值。分组基数越高、参与聚合的列越多,这张哈希表越大。
- 排序(Sort):ORDER BY、以及某些执行计划里用排序实现的聚合/去重,需要把参与排序的数据先收集到内存缓冲区,数据量一旦超过缓冲区,就要靠外部排序(读写磁盘归并)来完成。
- JOIN 的构建端(Build Side):多数哈希 JOIN 实现会把其中一侧的数据整体构建成哈希表放进内存,再用另一侧去探测。构建端选错或者数据倾斜,内存压力会明显放大。
单看每个算子,内存占用都有预估,但一条复杂 SQL 往往是多个算子串联甚至嵌套(比如子查询 + 窗口函数 + 排序),中间结果会叠加,实际内存峰值经常比单算子预估高出不少。服务器内存不大的环境,这种叠加效应更容易先于查询结束触发 OOM。
内存不够时,常见的应对思路
抛开具体产品,内存不足时数据库通常在这几类思路里选:
| 思路 | 做法 | 代价 |
|---|---|---|
| 溢写到磁盘 | 内存放不下的中间状态分批写到本地磁盘临时文件,需要时再读回来参与计算 | 磁盘 I/O 比内存慢,查询变慢,但能跑完 |
| 分批/分区聚合 | 把大表按分区或范围切开,分批计算再合并,单批常驻内存的数据量可控 | 需要引擎或应用层支持这种执行方式,合并阶段仍要占用一部分内存 |
| 调大内存 / 加机器 | 直接提高单机内存,或者用分布式方案把数据和计算摊到多个节点 | 硬件与运维成本上升,分布式方案还要承担集群调度与网络开销 |
这几类思路不是互斥的,很多数据库会组合使用。但不同产品对溢写的覆盖范围并不一样,选型时值得直接查对方文档:哪些算子能溢写、哪些形态超出内存会报错、内存预算能不能配置。
Youngs DB 的做法
Youngs DB 官网把排序、聚合、去重、JOIN、集合运算(UNION/INTERSECT/EXCEPT 等)列为具备弹性内存自适应能力的算子:执行时优先用内存,放不下时溢写到本地盘继续执行。窗口函数的情况,官网有两处说法:核心优势部分把窗口列在弹性内存范围内,安全特性部分又把窗口分区列为必须整体驻留的形态;本文按保守口径归入后一类,具体以产品文档为准。必须整体驻留的形态(子查询结果、右表缓冲、窗口分区、GROUP_CONCAT、分位数等)超出内存预算即明确报错;内存预算可通过 SessionContext.defaults().withSpillBudgetBytes(...) 配置。
在一库两用(同一份数据既承载交易、又承载分析)的场景下,官网还给出了另一层边界:分析这一侧的内存使用有预算上限,目的是避免报表类查询把内存占满、影响到交易侧的正常读写。
举个例子,下面是官网 SQL 现场演示里的按日聚合 SQL(演示库订单表数千万行、可现场灌至亿级),属于会产生中间分组状态的大表聚合查询:
SELECT DATE_FORMAT(created_at, '%Y-%m-%d') AS 日期, COUNT(*) AS 订单量
FROM orders
WHERE created_at >= TIMESTAMP '2026-06-14 00:00:00'
AND created_at < TIMESTAMP '2026-06-22 00:00:00'
GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d')
ORDER BY 日期;
这类按日分组的聚合,分组数量本身不大,内存压力主要来自订单表的扫描量;真正容易把内存打满的,往往是分组基数很高、或者叠加了多层 JOIN / 窗口函数的查询:前者属于能溢写的聚合,后者里的窗口分区属于必须整体驻留的形态,超出内存预算会报错,需要从查询设计上控制分区大小。
不适合的情况
- 查询里带窗口分区、子查询结果物化、GROUP_CONCAT、分位数这类必须整体驻留内存的形态:这类形态不在溢写的覆盖范围内,超出内存预算时官网的处理方式是明确报错,而不是溢写到磁盘继续跑完。选型和写查询时要分清"能溢写撑住"和"会报错"这两类,前者可以先跑起来再优化,后者需要提前从查询设计(缩小窗口分区、减小子查询结果、避免超大 GROUP_CONCAT / 分位数计算)上规避,不能指望数据库兜底。
- PB 级离线数仓分析:官网明确把这类场景划在边界之外,建议使用专用的列存分析集群,Youngs DB 定位解决的是"业务库上的分析"这一段,而不是替代专用数仓。
- 聚合结果集本身就很大:溢写机制解决的是"中间状态放不进内存"的问题,如果返回给客户端的结果集本身就有几千万行,无论中间过程怎么优化,传输和客户端接收这一步的开销都绕不开。
- 对延迟极度敏感、又必须扫描超大分组基数的场景:溢写让查询能够跑完,但读写磁盘天然比纯内存慢,如果业务要求毫秒级返回,还是需要先从查询设计(加过滤条件、降低分组基数)上想办法,而不是单纯依赖数据库兜底。
FAQ
Q:内存充足的时候,数据库还会 OOM 吗?
A:会。内存规划通常按日常查询的峰值估算,但大表聚合的中间状态(比如多层窗口函数叠加后的物化结果)有时会明显超过原始数据量,一旦超出规划范围就可能触发 OOM。
Q:溢写到磁盘和用磁盘临时表硬扛,是一回事吗?
A:磁盘临时表是一种常见的内存兜底做法,具体覆盖哪些算子,各产品不同,需要查各自文档。Youngs DB 官网的说法是排序、聚合、去重、JOIN 等算子各自具备溢写能力;必须整体驻留的形态超出预算则会报错。
Q:除了换数据库,查询本身能做什么优化?
A:能做的包括加过滤条件缩小扫描范围、用近似算法(比如近似去重)代替精确计算、减少不必要的排序,这些都能直接降低单条查询的内存峰值,和数据库层面的溢写机制是互补关系,不是替代关系。
Q:弹性内存自适应是不是意味着内存不用再规划了?
A:不是。它解决的是能溢写的算子在中间状态超出内存时能不能跑完的问题,而不是让内存规划变得不重要——机器内存越小,越依赖磁盘 I/O,查询耗时会相应变长;必须整体驻留的形态还会直接受内存预算约束。内存规划仍然影响查询的速度和能否跑完。
本文所述能力以云策数据官网与产品文档为准。
- 点赞
- 收藏
- 关注作者
评论(0)