代表性面试主题

数据工程面试题:中间结果超出内存时如何设计稳健的外部哈希聚合?

数据困难
Offer.cc 编辑团队发布 更新

题干

当 GROUP BY 的唯一分组数量可能超过内存时,你会如何设计一个既保留内存内性能、又能稳定溢写到存储的哈希聚合算子?

题干与适用场景

你负责 OLAP 执行引擎中的 GROUP BY。输入规模和基数不稳定,聚合状态可能超过内存。请说明如何避免查询在临界点突然失败或性能断崖,并给出验证方法。

本文适合数据工程、查询执行引擎和数据库内核岗位。假设聚合是阻塞算子,结果必须在读完输入后才能输出;不假设输入已按分组键排序。

面试官考察点

  • 能否解释为什么哈希聚合通常是内存内首选,以及为什么它天然难以直接溢写。
  • 能否把内存管理、页布局、并行合并和 I/O 背压放在同一个执行模型里。
  • 能否区分“估算后切换算法”和“运行时自适应”的失败边界。
  • 能否用可复现实验证明吞吐、峰值内存和尾延迟,而不是只背产品名词。

回答前需要澄清的问题

  1. 分组键基数和聚合状态大小的上界是多少?若无法给出上界,就必须设计溢写路径。
  2. 存储介质和可接受的查询延迟是什么?本地 NVMe、网络盘和对象存储不能使用同一 I/O 假设。
  3. 是否要求精确结果?近似聚合可以使用 sketch,但会改变题目约束。
  4. 是否允许结果重排?允许时可比较排序聚合;禁止时需保持哈希路径的输出语义。

30 秒回答框架

“我先把 GROUP BY 视为阻塞算子,按分组状态和内存预算建立基线。内存足够时使用并行哈希聚合;接近预算时,不重启查询或突然切换到完全不同的磁盘算法,而是让同一套页化状态在内存和存储之间渐进溢写。缓冲管理器负责淘汰和回读,线程本地 sink 再 combine,最后 finalize 和输出。验证时用逐步增加基数的实验,观察峰值内存、溢写量、吞吐和失败率,并保留排序聚合作为低基数或已排序输入的替代方案。”

分步骤深入解答

1. 先建立状态预算

估算每个分组的键、计数器、哈希元数据和对齐开销,再乘以预期基数。预算必须包含页目录、临时缓冲和并发线程的局部状态。只用输入字节数估算会漏掉高基数带来的状态膨胀。

2. 选择统一的页化状态

将聚合状态放入可寻址页。页在内存时使用适合 CPU 访问的布局,压力升高时由统一缓冲管理器写入存储;回读时恢复页地址或偏移。这样溢写不会要求把整个算子序列化成另一种格式,也不会在单行超过预算时重启查询。

3. 处理并行阶段和背压

并行执行可分为 sink、combine、finalize、get-data:线程先累积局部状态,再合并页引用,最后由单次 finalize 决定输出。溢写必须受缓冲管理器和 I/O 队列背压控制,否则更多线程只会制造随机写放大。对热点分组键要记录倾斜,必要时拆分大页或限制单分组状态。

4. 比较替代方案

若输入已按分组键排序,可流式聚合,几乎不需要保存全部状态;低基数且状态稳定时,纯内存哈希最快。排序聚合适合可接受排序成本、需要有序输出或哈希状态极易倾斜的场景。预估后在运行中突然切换到磁盘算法会让单个新增分组触发不可预测的性能断崖。

5. 设计可复现验证

固定输入宽度,逐步放大唯一分组数,使状态从内存内跨过预算。记录每个阶段的吞吐、峰值 RSS、读写字节、溢写页数、回读次数和 p95 延迟。重复热缓存与冷缓存实验,并注入 I/O 限速;正确性用独立排序聚合结果校验,不能只比较执行时间。

高质量示范回答

我会先确认这是精确的阻塞聚合,且分组状态可能超过内存。基线方案是并行哈希表,但我会把状态放在统一的页化缓冲管理器中:内存紧张时淘汰冷页到存储,回读时继续使用同一逻辑结构。线程按 sink、combine、finalize、get-data 协作,I/O 队列设置背压,避免并发把存储打满。输入已排序时我会改用流式聚合,低基数时保留纯内存哈希。最后用逐步增加基数的热冷缓存实验,验证精确结果、峰值内存、溢写量和 p95 延迟,确认跨过预算时性能平滑退化而不是突然失败。

常见错误

  • 错误表现:只说“内存不够就落盘”。失败原因:没有说明页布局、回读、并发和背压,无法判断是否会产生随机 I/O 放大。修正方法:给出统一缓冲管理和阶段边界。
  • 错误表现:依赖基数估算,超限后重启查询。失败原因:估算误差会把临界数据变成性能断崖。修正方法:说明运行时渐进溢写和不重启的路径。
  • 错误表现:声称哈希聚合总是优于排序聚合。失败原因:已排序输入、低基数和严重倾斜会改变结论。修正方法:明确替代方案的适用条件。
  • 错误表现:只报告平均吞吐。失败原因:溢写通常先影响尾延迟和失败率。修正方法:同时报告峰值内存、I/O、p95 和正确性。

追问及应对

如果存储延迟突然升高怎么办?

降低新线程进入 sink 的速率,扩大可观测的 I/O 队列水位并优先保留热点页。若仍无法满足 SLO,应返回资源不足,而不是无限堆积内存。

如果一个分组键占据大部分状态怎么办?

将该键的聚合状态拆成可合并的分片,限制单页大小,并在 finalize 阶段合并。若聚合函数不可分解,就必须明确降低并行度或拒绝该计划。

什么时候改用排序聚合?

输入已有有序保证、需要有序输出,或哈希状态的随机访问成本超过排序与顺序扫描成本时,排序聚合更合适。回答时要指出排序的临时空间也可能溢写。

如何证明没有性能断崖?

以同一数据集逐级提高基数,绘制数据规模与延迟曲线;在内存预算附近检查是否只有平滑斜率变化,并对比直接切换磁盘算法的曲线。报告硬件、缓存状态和 I/O 限制。

结果页也放不进内存怎么办?

让下游使用流式 get-data,或把最终结果按页输出到临时关系,再由消费者顺序读取。不要为了“返回结果”重新构造一个未受预算控制的巨大数组。

公开来源

同类题目