Hash Join是PostgreSQL对等值JOIN的默认偏好路径,但仅当内存足够、统计准确且子查询满足严格提升条件(无聚合/LIMIT/易变函数等)时才启用;否则退化为物化或Nested Loop,且EXISTS/IN常转为Hash Semi Join而非真正Hash Join。

Hash Join 是 PostgreSQL 优化器对等值 JOIN 的默认偏好路径
PostgreSQL 的查询规划器在面对 INNER JOIN 或 LEFT JOIN 且关联条件为等值(=)时,只要内存足够、统计信息准确,会优先估算 Hash Join 的代价并选中它。这不是“偏爱 JOIN”,而是因为 Hash Join 的执行模型天然适配 JOIN 的语义:构建一张哈希表(build side),再流式探测另一张表(probe side)。子查询本身不携带这种“两阶段流式处理”的结构暗示,优化器无法直接套用。
子查询需先被“提升”才能走 Hash Join,但提升有硬性限制
PostgreSQL 只有在子查询满足全部以下条件时,才会调用 pull_up_subqueries 将其提升为 JOIN:
- 不能含
GROUP BY、ORDER BY、LIMIT、DISTINCT、聚合函数(如MAX()) - 不能引用外层变量后又出现在非相关位置(即不能是“半相关”)
- 不能使用易变函数(如
now()、random()) - 子查询必须是单表、单列投影(如
SELECT user_id FROM orders WHERE status = 'paid')
一旦违反任一条件,子查询就会被物化(Materialize)或降级为 Nested Loop,根本不会进入 Hash Join 的候选池。
EXISTS/IN 子查询常被转为 Semi Join,而非 Hash Join
即使子查询被成功提升,优化器也大概率生成 Hash Semi Join 或 Merge Semi Join,而不是普通 Hash Join。二者关键区别在于:
-
Hash Semi Join:只返回左表匹配的行,右表仅用于判断存在性,不输出右表字段 —— 这对应EXISTS或IN语义 -
Hash Join:左右表字段都参与输出,行数可能因一对多而膨胀 —— 这是普通JOIN的行为
所以你看到执行计划里有 Hash 字样,不等于就是 Hash Join;得看节点类型是 Hash Join 还是 Hash Semi Join。后者虽快,但和 JOIN 走的不是同一条物理路径。
手动干预时,JOIN 更容易触发 Hash Join
当发现慢查询实际走了 Nested Loop,想强制走 Hash Join,对 JOIN 写法可以直接关掉嵌套循环:
SET enable_nestloop = off; SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id;
但对子查询写法,比如 WHERE id IN (SELECT ...),即使关掉 nestloop,优化器也可能退化为 Materialize + Seq Scan,因为它没有“build/probe”两表的明确划分,无法直接映射到 Hash 结构。
真正容易被忽略的是:Hash Join 的性能优势高度依赖 work_mem 设置。如果子查询物化后临时表过大,或 JOIN 的 build side 超出 work_mem,PostgreSQL 会自动降级为 Hash Join (Batch 2) 甚至磁盘哈希,此时性能反而比优化良好的 Nested Loop 更差。











