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 名字更重要。