PostgreSQL 的 enable_parallel_hash:并行哈希连接为什么不只是“多开几个 worker”

2026-07-05 38 预计阅读时间: 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.

预计阅读时间:8 分钟

enable_parallel_hash 这个 GUC 看起来像一个普通的优化器开关,但它背后对应的是 PostgreSQL 并行哈希连接的一条关键分界线:多个 worker 是各自建一份哈希表,还是合力构建一张共享哈希表。这个差异在小表上可能不显眼,一旦参与 join 的构建端变大,内存占用、批处理次数、执行时间都会被放大。

它控制的不是“能不能并行”,而是“怎么并行建哈希表”

哈希连接通常分两步:先扫描 join 的一侧,按 join key 建哈希表;再扫描另一侧,用 key 去哈希表里探测匹配行。

在并行执行里,朴素做法是每个 worker 都建自己的哈希表副本。这样实现简单,但代价直接:如果有 4 个 worker,同一份构建端数据可能被哈希化 4 次,占用 4 份内存,还会重复消耗 CPU 和 I/O。

并行哈希连接的思路不同:worker 池化内存,共同构建一张共享哈希表。也就是说,worker 之间不是“各干各的”,而是在同一个并行哈希结构上协作。来源摘要强调的正是这一点:共享表和每个 worker 一份副本之间的区别,在大数据量场景里非常大。

enable_parallel_hash 用来允许或禁止优化器选择这种 Parallel Hash 计划。它不是让所有 hash join 自动变快的魔法按钮,而是给优化器一个候选路径。

为什么内存模型会改变执行结果

如果每个 worker 都有自己的哈希表,内存压力大致会随 worker 数量放大。构建端越大,越容易触发批处理、落盘或者计划退化。共享哈希表则把 worker 的内存预算集中起来,减少重复结构,让构建端更有机会留在内存里。

可以把差异理解成两种施工方式:

  • 非共享方式:每个工人都照着同一批材料各造一堵墙。
  • 共享方式:所有工人一起造同一堵墙。

对数据库来说,“墙”就是哈希表。重复建表不只是浪费内存,还会让 CPU 做重复工作。共享哈希表则需要同步和协调,所以它也不是无成本的。真正值得关注的是:当构建端足够大、并行度合适、join key 分布还算健康时,共享哈希表更可能带来收益。

可以这样观察执行计划

下面的例子可以在本地 PostgreSQL 实例里改造运行。它创建两张表,填充一些数据,然后分别打开和关闭 enable_parallel_hash,观察执行计划中的 Parallel Hash 节点。

运行前需要确认 PostgreSQL 允许并行查询。不同机器、版本、数据规模和参数下,优化器可能选择不同计划;如果没有出现 Parallel Hash,可以适当增大数据量,或检查 max_parallel_workers_per_gather、表统计信息和成本参数。

-- 在 psql 中执行
DROP TABLE IF EXISTS orders_demo;
DROP TABLE IF EXISTS customers_demo;

CREATE TABLE customers_demo AS
SELECT
  i AS customer_id,
  md5(i::text) AS name
FROM generate_series(1, 500000) AS s(i);

CREATE TABLE orders_demo AS
SELECT
  i AS order_id,
  (1 + (random() * 499999)::int) AS customer_id,
  (random() * 1000)::numeric(10, 2) AS amount
FROM generate_series(1, 3000000) AS s(i);

ANALYZE customers_demo;
ANALYZE orders_demo;

-- 让并行计划更容易出现。生产环境不要照抄这些成本参数。
SET max_parallel_workers_per_gather = 4;
SET min_parallel_table_scan_size = '8MB';
SET min_parallel_index_scan_size = '8MB';
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;

SET enable_hashjoin = on;
SET enable_parallel_hash = on;

EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT count(*)
FROM orders_demo o
JOIN customers_demo c
  ON o.customer_id = c.customer_id;

SET enable_parallel_hash = off;

EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT count(*)
FROM orders_demo o
JOIN customers_demo c
  ON o.customer_id = c.customer_id;

你要看的不是单个时间数字,而是这些信号:

  • 计划里是否出现 Parallel Hash
  • worker 数量是否接近预期。
  • Batches 是否很多,很多批次通常意味着哈希表无法一次放进内存。
  • Buffers 是否出现明显临时读写。
  • 开关前后总执行时间、CPU 使用和内存压力是否一致改善。

如果要从 shell 里快速跑,可以这样:

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f parallel_hash_demo.sql

把上面的 SQL 保存为 parallel_hash_demo.sql,并把 DATABASE_URL 改成你的测试库连接串。建议只在测试库执行,因为示例会创建数百万行数据。

调参时别只盯着一个 GUC

enable_parallel_hash 是优化器开关,不是容量规划参数。它能让 PostgreSQL 考虑 Parallel Hash,但最终计划还受到多组条件影响:表大小、统计信息、并行 worker 限制、join 条件、成本估算、work_mem,以及机器的 CPU 和内存资源。

更稳妥的做法是把它放进一次完整的性能排查里:

SHOW enable_parallel_hash;
SHOW max_parallel_workers_per_gather;
SHOW work_mem;
SHOW max_parallel_workers;

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM orders_demo o
JOIN customers_demo c
  ON o.customer_id = c.customer_id;

如果业务查询已经是 CPU 密集型,增加并行和共享哈希可能有帮助;如果瓶颈在磁盘、锁等待、网络传输或上游过滤不足,它可能只是让执行计划看起来更复杂。

采用建议:用它定位问题,不要把它当万能加速器

在生产系统里,建议保持默认行为,针对具体慢查询用 EXPLAIN (ANALYZE, BUFFERS) 验证。临时切换 enable_parallel_hash 很适合做 A/B 对比:同一条 SQL、同一份统计信息、相近缓存状态下,比较计划形态和资源消耗。

可以按这个清单判断是否值得深入:

  • join 构建端较大,重复建哈希表会明显放大内存。
  • 查询本身能使用并行扫描或并行 join。
  • EXPLAIN 显示 hash join 有多批次或临时 I/O。
  • worker 数量不会挤占同库其他关键查询。
  • 测试结果覆盖高峰数据量,而不是只跑了开发库的小样本。

enable_parallel_hash 的价值在于暴露一个具体选择:让 worker 合作建共享哈希表,还是各自持有副本。理解这个选择,比记住 GUC 名字更重要。


相关推荐