在PostgreSQL中,Hash Join是一种常见的两表关联算法,尤其在小表驱动大表时效率很高。它的核心步骤是先扫描驱动表(外表),在内存中构建哈希表,再扫描内表逐行探测匹配。如果构建阶段的哈希表无法完全放入分配的work_mem内存,数据库就会把多余数据写入临时文件,形成磁盘溢出,性能随之急剧下降。

一、Hash Join与work_mem的基本关系
Hash Join的构建端需要将外表的连接键和相关列全部载入内存,生成哈希桶。PostgreSQL为每个排序或哈希节点单独分配work_mem大小的内存,而不是为整个查询只分配一次。也就是说,一个复杂查询里可能同时存在多个哈希节点,每个都能吃到一份work_mem。
当外表数据量对应的哈希表超过work_mem时,执行器会启用“分批哈希(Hybrid Hash)”策略,把部分桶溢写到pgsql_tmp目录下的临时文件。之后的探测阶段需要反复读写磁盘,I/O开销会抵消掉Hash Join本身的优势。通过调大work_mem,可以让哈希表完整驻留内存,从而消除溢出。
二、如何确认是否发生了磁盘溢出
最直观的办法是使用EXPLAIN (ANALYZE, BUFFERS) 观察执行计划。在Hash节点或Hash Join节点下方,如果看到“Batches: 1”说明没有溢出;若Batches大于1,或者出现了“Disk: xxxxkB”字样,就代表发生了落盘。
下面是一段典型的诊断语句,注意代码中的HTML特殊字符已经转义:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT a.id, b.name FROM orders a JOIN customers b ON a.customer_id = b.id WHERE a.create_date >= '2023-01-01';
执行后重点看输出里的Hash Cond以及Batches数值。若Batches为4、Disk显示约两万kB,就意味着本应内存处理的哈希被拆成了四批并写入磁盘,此时调整work_mem往往能立竿见影。
三、调整work_mem的几种方式
全局修改会影响所有会话,通常不推荐直接改postgresql.conf里的work_mem,因为高并发下多个哈希节点叠加可能耗尽内存。更安全的做法是在会话级或语句级设置。
会话级设置仅对当前连接有效,适合运维人员手动跑报表:
SET work_mem = '64MB'; -- 后续查询将使用新的work_mem SELECT count(*) FROM large_a JOIN large_b ON large_a.k = large_b.k;
如果只想对某条SQL生效,可以用CTE包裹并在事务里设置,或者在建表时通过SET LOCAL控制存储过程内的行为。对于定时任务,也可以在psql脚本开头写SET.work_mem,避免影响其他业务。
四、合理估算work_mem大小
不能盲目调大。一般先用下面思路估算:取外表在连接键上的去重后数据体积,加上行头开销,除以期望的Batches数。例如外表约300MB,希望Batches为1,则work_mem至少设到320MB左右。但也要考虑实例总内存,公式可参考:max_work_mem_per_query × 并发数 < 可用内存 × 0.6。
可以用一张小表记录不同场景下的实测值,辅助决策:
| 外表大小 | 原work_mem | 溢出批数 | 调后work_mem | 耗时变化 |
|---|---|---|---|---|
| 200MB | 16MB | 8 | 256MB | 从12s降至1.8s |
| 500MB | 32MB | 16 | 512MB | 从40s降至4s |
上表说明,只要内存允许,把work_mem提升到能覆盖外表哈希的体积,性能提升非常明显。但生产环境务必在从库或低峰期验证。
五、其他配合优化手段
除了调work_mem,还应检查统计信息是否准确。若ANALYZE不及时,规划器可能误判外表大小,选错驱动表。定期执行ANALYZE或打开autovacuum可减少此类问题。
另外,如果业务允许,可把高频关联的小表设为CTE并提前过滤,减少进入Hash构建端的数据量。例如先用WHERE条件把外表从千万行缩到十万行,原本需要512MB的哈希可能只需32MB,也就不必大幅调整work_mem。
六、风险与监控
work_mem过大最直接的风险是OOM。每个哈希节点独立占用,一个查询含三个哈希连接,设置512MB就可能吃掉1.5GB。建议使用pg_stat_activity配合监控脚本,观察临时文件生成量:
SELECT datname, temp_files, temp_bytes FROM pg_stat_database WHERE temp_files > 0 ORDER BY temp_bytes DESC;
当temp_bytes持续走高,说明磁盘溢出普遍,需结合业务节奏适度上调并控制并发。通过这种闭环调优,才能在性能与稳定之间找到平衡点。
PostgreSQLHash_Joinwork_mem修改时间:2026-08-05 15:00:32