Hash Join落盘主因是work_mem不足致哈希表整桶刷磁盘,引发IO爆炸;需先通过EXPLAIN(ANALYZE,BUFFERS)确认Disk Usage非零、排除数据倾斜与统计信息过期,再按操作节点独享原则设32MB~64MB并用SET LOCAL临时调整。

Hash Join落盘不是“慢一点”,是IO爆炸
Hash Join在work_mem不足时写磁盘,性能断崖式下跌——不是算法本身变慢,而是从内存随机访问退化成磁盘随机读写。一次哈希探查(probe)本该是纳秒级指针跳转,变成毫秒级磁盘寻道,放大10⁵倍以上。
典型症状包括:EXPLAIN (ANALYZE, BUFFERS)中出现大量Temp Read/Temp Write块、Execution Time里IO耗时占比超70%、Shared Hit骤降而Local Hit暴增。
- PostgreSQL不会只写“一部分”哈希表:它按桶(bucket)切片,一旦内存不够,就整批刷到临时文件,后续每次probe都要反复读磁盘
- 临时文件默认放在
pg_temp_*目录,不走shared_buffers缓存,OS page cache也难命中(因访问模式高度随机) - 日志里若出现
writing to disk due to insufficient memory,就是明确信号,不用再猜
怎么确认真是work_mem不够,而不是其他问题?
别一看到Hash Join就调work_mem——数据倾斜、统计信息过期、类型不一致这三类问题更隐蔽,且调内存完全无效。
- 先跑
EXPLAIN (ANALYZE, BUFFERS),检查Hash节点的Disk Usage是否非零;如果为0,说明根本没落盘,work_mem不是瓶颈 - 查
pg_stat_progress_hash(v12+),看hash_probe_total_buckets / hash_buckets_used比值:若远大于3,说明哈希桶严重稀疏,是work_mem太小被迫建了过多低效桶 - 快速验数据倾斜:
SELECT key, COUNT(*) FROM table GROUP BY key ORDER BY 2 DESC LIMIT 5;若TOP1频次 > 平均值×10,加内存基本白费 - 检查JOIN字段类型:比如
CAST(u.id AS TEXT) = o.user_id会导致索引失效+类型强制转换,优化器只能硬Hash,且无法缩小驱动表规模
work_mem设多大才真有用?
work_mem是“每个操作节点”独享的上限,不是整个查询或会话的总和。一个含2个Hash Join + 1个Sort的查询,可能同时吃掉3×work_mem。
- 估算下限:对驱动表Join键粗略计算——
(行数 × (键长度 + 8)) × 1.5(1.5是哈希填充因子预留) - 中小负载建议从
'32MB'起步,高分析负载可试'64MB';超过'128MB'需严格评估并发压力 - 临时调优务必用
SET LOCAL work_mem = '64MB',仅对当前事务生效;全局改work_mem极易引发OOM - 注意连接池干扰:pgbouncer默认拦截
SET命令,需确认其ignore_startup_parameters未屏蔽work_mem
为什么调了work_mem还是写磁盘?
数值看着够,但实际仍落盘,问题常出在并发、并行或操作叠加的细节里。
- 开了并行查询(
max_parallel_workers_per_gather = 4),每个worker都独立申请work_mem,但pg_stat_progress_hash只反映主进程状态,容易误判 - 同一查询含Hash Join + Sort + DISTINCT,三者各自吃一份
work_mem,总开销是三倍,你只给了单个额度 - 单位写错:写成
'64M'(缺B)会被静默转成64字节;写成67108864虽对,但易手误少零 -
shared_buffers和effective_cache_size再大也没用——它们不参与哈希表构建,调了纯属浪费
真正卡住的点往往不在work_mem本身,而在你没意识到:一个查询里可能有多个独立内存消费者,而你只盯着Hash节点看。











