GLOBAL JOIN 是广播式连接:协调节点先执行右表查询并全量拉取到内存,再序列化广播至所有分片节点,导致数据复制N份、内存与网络开销剧增;非GLOBAL的分布式JOIN也会隐式广播,引发查询放大。

GLOBAL JOIN 会把右表全量广播到每个节点
ClickHouse 的 GLOBAL JOIN 不是“分布式哈希连接”,而是“广播式连接”:它会在协调节点(coordinator)上先执行右表的查询,把结果集**完整拉取到内存中**,再序列化后发送给所有参与查询的分片节点。哪怕右表只有 100 万行、每行平均 2KB,广播数据就接近 2GB——这还没算序列化开销和各节点本地构建哈希表的内存消耗。
常见误判点:GLOBAL IN 和 GLOBAL JOIN 行为一致,但很多人只注意了语法差异,没意识到它们共享同一套广播机制。
- 右表若含大列(如
String超过 100KiB),广播体积指数级放大 - 集群有 N 个分片,右表数据就被复制 N 份,在各节点分别建哈希表
- 即使右表加了
LIMIT,也只在 coordinator 端生效;广播前不触发裁剪,LIMIT对内存无缓解作用
非 GLOBAL 的 JOIN 在分布式表上也会隐式广播
当 left_table 和 right_table 都是分布式表,且没加 GLOBAL 时,ClickHouse 默认行为是:对每个 left 分片,都去所有 right 分片拉全量数据做 JOIN —— 这叫“查询放大”(query amplification)。比如 left 有 4 个分片、right 有 4 个分片,实际执行的 JOIN 次数是 4 × 4 = 16 次,每次都在本地加载一份 right 全量副本。
这不是 bug,是 ClickHouse 去中心化架构下的默认策略。它不假设数据分布规律,所以宁可多传,也不冒险漏数据。
- 用
EXPLAIN查看执行计划,如果出现ReadFromStorage(Distributed)后紧接JoiningTransform,基本确认发生了隐式广播 -
system.processes中查memory_usage,高内存占用的查询往往对应 right 表读取阶段 - 右表若本身是
MergeTree引擎但未按 JOIN KEY 分区,优化器无法下推过滤,只能全扫
怎么避免 GLOBAL / 隐式广播导致的 OOM?
核心思路不是调大 max_memory_usage,而是从数据布局和查询结构上切断广播链路。
- 优先把右表建为本地表(
ReplacingMergeTree或ReplicatedReplacingMergeTree),JOIN 时显式指定db.table_local,确保只读本节点副本 - 右表数据量可控(Dictionary 替代 JOIN:内存常驻、支持缓存、不参与查询计划膨胀
- 必须用
GLOBAL JOIN时,强制限制右表结果集大小:SELECT ... FROM (SELECT * FROM right_table WHERE ... LIMIT 10000) AS t GLOBAL JOIN left_table ...—— 注意LIMIT必须包在子查询里,否则无效 - 检查
join_algorithm设置:设为'partial_merge'可降低内存峰值(但要求 JOIN KEY 有序,且仅对INNER/LEFT有效)
容易被忽略的细节:Dictionary 也不是完全安全的
Dictionary 加载时会把整个源表拉到内存,如果源表用了 clickhouse_driver 直连本地节点,而该节点内存不足,一样会触发 MEMORY_LIMIT_EXCEEDED。更隐蔽的是:SYSTEM RELOAD DICTIONARY 是同步阻塞操作,期间所有依赖该字典的查询都会卡住——这点在凌晨自动刷新时特别容易引发雪崩。
真正稳的方案,是让 Dictionary 源来自 File 或 HTTP,并配合 lifetime 主动控制加载时机,而不是依赖定时刷新。










