用 hash_mem_multiplier 拆开 PostgreSQL 哈希与排序的内存预算

2026-07-29 20 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

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 JoinHashAggregate;普通 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 版本生成的具体计划可能不同,重点是比较两次 HashAggregateBatchesMemory UsageDisk 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 JoinHashAggregate
  • 哈希节点存在多 batch、落盘或明显的临时 I/O;
  • 调高乘数后执行时间和临时写入确实下降;
  • 已按峰值并发、并行 worker 数量和每条计划的节点数量估算内存;
  • 优先采用事务级、任务级或角色级配置,再考虑实例级修改;
  • 保留排序查询的基准测试,避免用提高 work_mem 的方式误伤整体容量。

hash_mem_multiplier 最有价值的地方不是“让 PostgreSQL 多用内存”,而是把两类行为不同的执行算子分开管理。先用执行计划确认哈希落盘,再给目标工作负载增加预算,通常比全局抬高 work_mem 更容易控制风险。


相关推荐