bigywater.tech
ClickHouse 内部原理 · 39

Hash Join 为什么容易占满内存

真实让一张500万行、每行带大payload的表做hash JOIN的右表,触发内存超限报错,验证内存开销真实只跟右表大小成正比。

352MB
真实的hash join内存开销,全部来自右表哈希表构建
FillingRightJoinSide
真实定位到失败发生在构建右表哈希表这一步

做的事:真实让"重"的表做hash join的右表

代码CREATE TABLE demo.jbig (k UInt64, payload String) ENGINE=MergeTree ORDER BY k; INSERT INTO demo.jbig SELECT number, repeat('x', 500) FROM numbers(5000000); -- jbig真实约2.5GB原始数据(500万行×500字节) SELECT count() FROM demo.jleft INNER JOIN demo.jbig ON jleft.k=jbig.k SETTINGS join_algorithm='hash', max_memory_usage=100000000; -- 真实压到100MB

验证:真实在"构建右表哈希表"这一步就爆了内存

实测 · 本机执行输出
DB::Exception: Query memory limit exceeded: would use 331.62 MiB
(attempt to allocate chunk of 259.39 MiB), maximum: 95.37 MiB:
While executing FillingRightJoinSide. (MEMORY_LIMIT_EXCEEDED)

-- 默认无限制情况下,这条JOIN真实用了多少内存:
memory_usage: 352027750 字节(约352MB)

报错信息里的FillingRightJoinSide真实点明了失败发生的确切阶段——ClickHouse的hash join在开始真正匹配左右两表之前,要先把整个右表(本文的jbig)完整读入内存、构建成一个哈希表,这一步真实跟左表jleft有多少行、这条JOIN最终能匹配出多少行完全无关——哪怕左表只有1行,只要右表本身很大,这次JOIN依然要为右表的完整哈希表买单。

真实的教训:把小表放在JOIN的右边

这正是所有讲ClickHouse性能优化的资料都会强调"小表放右边"的真实原因——不是随口说说的经验之谈,而是hash join算法本身的真实工作方式决定的:右表(在SQL文本里跟在JOIN关键字后面的那张表)会被完整加载进内存构建哈希表,内存占用真实跟右表的行数×每行大小成正比,跟左表的规模、跟最终JOIN结果的行数都没有关系。本文的jbig作为右表,500万行、每行多了一个500字节的payload列,构建出的哈希表真实占了约352MB内存;如果把它换到左边、让本来更小的jleft做右表,内存占用会显著降下来(本系列上一篇《JOIN的几种算法》里jbig作为左表左边没有类似问题,正是因为那次的右表jright只有100万行、没有额外的大payload列)。这也解释了为什么join_algorithm优先级列表里排在最前面的parallel_hash依然逃不开这个约束——不管并发构建做得多好,哈希表本身该有多大还是多大,真实的解法从来不是等ClickHouse自己神奇地省掉这块内存,而是从SQL写法上,把体积小的表放在JOIN右边。

分步演示:从大右表报错到"小表放右边"的真实教训

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

右表真实要整个塞进内存,跟左表大小无关
这一步新验证的状态

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的 FillingRightJoinSide 报错阶段定位
    • 真实测出右表500万行×500字节payload的hash join内存占用约352MB
    京ICP备2026067502号-1京公网安备11010502063487号