PostgreSQL 的 work_mem 同时影响排序、哈希连接和哈希聚合,但这些操作消耗内存的方式并不相同。排序可以把数据分段写入临时文件,哈希表一旦装不下,则可能增加批次数、反复扫描临时数据,性能下降方式往往更陡峭。hash_mem_multiplier 的作用,就是在保留排序预算的同时,单独放宽哈希操作的内存上限。
它不是第二个 work_mem
哈希操作可使用的内存上限,可以简化理解为:
hash memory limit = work_mem × hash_mem_multiplier
例如:
SET work_mem = '8MB';
SET hash_mem_multiplier = 4.0;
此时单个排序节点的预算仍以 8MB 为基准,而单个哈希节点最多可以按约 32MB 的上限规划。受影响的典型执行节点包括 Hash Join 和 HashAggregate;普通 Sort 不会因为乘数提高而获得同样的扩容。
这里有两个容易忽略的边界:
- 这是每个执行节点的预算,不是整条 SQL 的总预算。一条计划可能同时包含多个排序或哈希节点。
- 并发会放大内存消耗。多个会话、并行 worker 和多个哈希节点都可能同时申请内存。
因此,把乘数从 2 调到 8,不能简单理解为“每个数据库连接只多用几十 MB”。容量评估必须结合查询并发和执行计划。
为什么哈希操作值得单独供给
当哈希表超过内存预算时,PostgreSQL 可能把输入划分成多个 batch,并把部分数据写入临时文件。EXPLAIN ANALYZE 中常见的观察信号包括:
Batches大于 1;- 出现
Disk Usage; - 临时块读写增加;
- 执行时间随着数据量增长突然恶化。
排序也会落盘,但提高全局 work_mem 会同时抬高所有排序和哈希节点的预算。如果真正的问题只出在大型哈希连接或高基数聚合上,提高 hash_mem_multiplier 通常更有针对性:排序继续使用较小预算,哈希表获得额外空间。
这并不意味着乘数越大越好。哈希表很小或查询本来就受 I/O、扫描量、数据倾斜限制时,提高内存不会带来明显收益。
在本地复现内存与批次变化
下面可以这样实践。示例使用 PostgreSQL 17 容器;运行前需要安装 Docker。不同机器和 PostgreSQL 版本生成的具体计划可能不同,重点是比较两次 HashAggregate 的 Batches、Memory Usage、Disk Usage 和总执行时间。
docker run --name pg-guc-lab \
-e POSTGRES_PASSWORD=postgres \
-d postgres:17
docker exec -i pg-guc-lab psql -U postgres <<'SQL'
CREATE TABLE events AS
SELECT
g AS id,
g % 200000 AS customer_id,
md5(g::text) AS payload
FROM generate_series(1, 1000000) AS g;
ANALYZE events;
SET work_mem = '4MB';
SET hash_mem_multiplier = 1.0;
EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, count(*)
FROM events
GROUP BY customer_id;
SET hash_mem_multiplier = 8.0;
EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, count(*)
FROM events
GROUP BY customer_id;
-- 该排序仍以 work_mem 为主要预算,不会乘以 hash_mem_multiplier。
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM events
ORDER BY payload;
SQL
实验完成后可以删除容器:
docker rm -f pg-guc-lab
如果优化器没有选择 HashAggregate,不要急着通过关闭其他执行策略来制造结论。先检查统计信息、分组基数和数据规模,因为生产环境中真正重要的是优化器自然选择的计划。
从单条查询开始调优
比起直接修改实例级配置,更稳妥的做法是在事务中测试目标查询:
BEGIN;
SET LOCAL work_mem = '8MB';
SET LOCAL hash_mem_multiplier = 4.0;
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.region, count(*)
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.region;
ROLLBACK;
SET LOCAL 只在当前事务内生效,适合验证某条报表或批处理任务。确认收益后,可以按角色设置,而不必影响整个实例:
ALTER ROLE reporting_user SET work_mem = '8MB';
ALTER ROLE reporting_user SET hash_mem_multiplier = 4.0;
角色级设置通常需要新连接才能看到效果。连接池环境还要确认连接是否被回收和重新建立。
如果已经启用 pg_stat_statements,还可以寻找临时块写入较高的语句:
SELECT
queryid,
calls,
temp_blks_read,
temp_blks_written,
mean_exec_time,
left(query, 120) AS query_sample
FROM pg_stat_statements
WHERE temp_blks_written > 0
ORDER BY temp_blks_written DESC
LIMIT 20;
临时块并不只来自哈希操作,所以仍需结合 EXPLAIN (ANALYZE, BUFFERS) 判断具体节点。
上线前检查清单
调整 hash_mem_multiplier 前,建议确认以下事项:
- 执行计划确实包含
Hash Join或HashAggregate; - 哈希节点存在多 batch、落盘或明显的临时 I/O;
- 调高乘数后执行时间和临时写入确实下降;
- 已按峰值并发、并行 worker 数量和每条计划的节点数量估算内存;
- 优先采用事务级、任务级或角色级配置,再考虑实例级修改;
- 保留排序查询的基准测试,避免用提高
work_mem的方式误伤整体容量。
hash_mem_multiplier 最有价值的地方不是“让 PostgreSQL 多用内存”,而是把两类行为不同的执行算子分开管理。先用执行计划确认哈希落盘,再给目标工作负载增加预算,通常比全局抬高 work_mem 更容易控制风险。