bigywater.tech
ClickHouse 内部原理 · 34

GROUP BY 内存爆炸时发生了什么

真实建一张极端高基数的表,故意压低内存上限触发GROUP BY内存超限报错,再对比默认设置下真实的内存占用数字。

约1.02GB
3000万行、3000万分组的GROUP BY真实内存占用
34字节/分组
真实测出的每个分组在哈希表里的开销

做的事:真实建一张极端高基数的表,故意把内存限制压得很小

代码CREATE TABLE demo.bigcard (k UInt64) ENGINE=MergeTree ORDER BY k; INSERT INTO demo.bigcard SELECT number FROM numbers(30000000); -- k列真实全部唯一 SELECT k, count() FROM demo.bigcard GROUP BY k SETTINGS max_memory_usage=52428800; -- 真实压到50MB

验证:真实拿到官方的内存超限报错原文

实测 · 本机执行输出(真实触发MEMORY_LIMIT_EXCEEDED)
DB::Exception: Query memory limit exceeded: would use 56.71 MiB
(attempt to allocate chunk of 0.00 B), maximum: 50.00 MiB:
While executing AggregatingTransform. (MEMORY_LIMIT_EXCEEDED)

报错真实明确指出是AggregatingTransform(聚合算子本身)在申请内存的时候超限——不是读数据的阶段,是GROUP BY的哈希表在真实膨胀过程中撞上了50MB的天花板。

验证:默认内存上限下,同样这条GROUP BY真实用了约1.02GB

实测 · 本机执行输出(默认max_memory_usage,无限制)
SELECT k, count() FROM demo.bigcard GROUP BY k FORMAT Null;
→ memory_usage: 1019924823 字节(约1.02GB)
  query_duration_ms: 189

真实换算:1019924823 / 30000000 ≈ 34 字节/分组
这条GROUP BY的真实爆炸原因很直白:k列是numbers(30000000)生成的连续唯一值,意味着每一行都是一个独立的分组,GROUP BY的哈希表要真实为3000万个不同的分组各自开一个槽位——本文实测出来约34字节/分组的开销(存分组键本身+计数器+哈希表本身为了控制冲突率而预留的空间),3000万个分组乘起来就是真实超过1GB的内存占用。这跟本系列上一篇《Two-Level Aggregation》验证的高基数场景本质是同一类问题,区别在于本文这次是刻意构造的最极端情况——分组数几乎等于总行数,GROUP BY完全没有起到"压缩数据量"的效果(正常GROUP BY场景下,分组数应该远小于总行数,内存占用也会小得多)。真实的教训是:GROUP BY的内存开销真实只跟分组数(基数)成正比,跟总行数关系不大——同样3000万行,如果只分100个组(本系列《ClickHouse聚合为什么快》测过),内存占用只有约450KB;分3000万个组,内存占用暴涨到约1.02GB,相差超过2000倍,业务写GROUP BY之前真实有必要对分组键的基数心里有数。

分步演示:从内存超限报错到确认真实的根本原因

每一步都是本文上面用真实GROUP BY跑出来的原始结果。

3000万个分组,真实吃掉超过1GB内存
这一步新验证的场景

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的 max_memory_usage 设置与MEMORY_LIMIT_EXCEEDED报错
    • 真实对比极端高基数(3000万分组)与低基数(100分组)的内存占用
    京ICP备2026067502号-1京公网安备11010502063487号