连接策略负责组合两路输入,并不是某段 SQL 永久绑定的属性。只查询三个客户时,通过索引反复定位购买记录可能很合算;查询所有购买记录时,扫描一遍并构建哈希表可能更合适。PostgreSQL 根据预计工作、可用访问路径、顺序和内存预期,在合法候选中选择。表有多大确实重要,但过滤后还剩多少行,比表的名字或它平时被称为“大表”更重要。
反复查找与复用查找表
嵌套循环每取一条外层记录,就利用这条记录执行一次内层路径。内层并不一定扫描整张表,也可以进行索引或位图查找。外层结果很少、内层定位足够精确时,重复执行的成本可能很低。读计划时,应把内层节点的每轮行数与 loops 放在一起看,才能知道实际重复了多少工作,而不是看到循环就直接判断它低效。
哈希连接从一路输入建立临时哈希表,再用另一边的记录探测匹配。这是执行期间的数据结构,与 USING hash 建立的持久索引没有直接关系;不需要先建一个哈希索引才能使用哈希连接。真正的构建侧是计划中 Hash 节点下方的子树。应查看这个位置,不能假设 SQL 里写在左边或右边的表必然负责构建,也不能只比较两张基础表的原始大小。
如果两路输入已经具备适当顺序,或者排序的成本值得支付,合并连接也是候选。因此,嵌套循环和哈希连接并没有穷尽规划器的全部选择。不同过滤条件、输出要求和合法连接条件,都可能改变哪些方案值得考虑。算法名称只是计划的一部分,不能代替对输入与结果规模的分析。
坏掉的样子
外层结果被低估时,原本看似便宜的单次内层查找会被重复过多次数。哈希连接也可能因为超出内存预算而分成多批,产生临时读写,并不保证始终完整留在内存中。应检查批次数和临时缓冲区活动,不能看见 Hash 就推导出“恒定的单次查找成本”或“所有规模下都线性且很快”。
大量重复连接键还可能产生巨大的结果集合,无论使用哪种查找策略都需要产生查询要求的那些行。意外的多对多连接,应通过修正关系或查询语义解决,而不是只把连接节点换个名字。如果两侧每个键都重复很多次,乘出来的输出规模本身就可能是主要瓶颈。
原生 PostgreSQL 不会解释 /*+ HashJoin */ 这样的优化器提示注释。扩展可以提供提示功能,但那属于另外的环境契约。规划器开关可以支持受控对比实验,不过不能因为一次小数据实验,就永久在全局关闭某种连接方式;其他查询的输入和收益可能完全不同。
快速验证
在 PostgreSQL 18 的 psql 中执行下面事务。它创建五百个客户和五万条购买记录,每个客户对应一百条购买记录,并在引用列上建立普通 B-tree 索引。两条查询计算同一种连接关系的金额总和,区别只在于是否限制客户范围。
\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE customers (id integer PRIMARY KEY);
CREATE TEMP TABLE purchases (id integer PRIMARY KEY,customer_id integer,amount integer);
INSERT INTO customers SELECT g FROM generate_series(1,500) AS g;
INSERT INTO purchases SELECT g,1+(g-1)%500,10 FROM generate_series(1,50000) AS g;
CREATE INDEX purchases_customer ON purchases(customer_id);
ANALYZE customers;
ANALYZE purchases;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT sum(p.amount) FROM customers c JOIN purchases p ON p.customer_id=c.id WHERE c.id<=3;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT sum(p.amount) FROM customers c JOIN purchases p ON p.customer_id=c.id;
DO $$ BEGIN
IF (SELECT sum(p.amount) FROM customers c JOIN purchases p ON p.customer_id=c.id WHERE c.id<=3)
IS DISTINCT FROM 3000::bigint THEN RAISE EXCEPTION 'wrong selective join'; END IF;
IF (SELECT sum(p.amount) FROM customers c JOIN purchases p ON p.customer_id=c.id)
IS DISTINCT FROM 500000::bigint THEN RAISE EXCEPTION 'wrong broad join'; END IF;
END $$;
ROLLBACK;
PostgreSQL 18.6 实测中,限定客户编号不超过三时采用嵌套循环。内层位图堆扫描每轮返回一百条购买记录,一共执行三轮。连接输出三百行,金额总和为三千。嵌套循环里出现位图扫描是正常组合:扫描节点描述内层记录怎样被取到,连接节点描述怎样反复利用外层记录组合结果,两者不是互斥选项。
去掉客户过滤后,计划使用哈希连接。构建侧读取五百个客户,探测侧扫描五万条购买记录,最后金额总和为五十万。本夹具的哈希表只有一个批次,这只是当前数据规模下的观察,不能推广为所有哈希连接的保证。代码中的断言分别验证两种范围的总和,因此换一个环境即使出现不同节点,也必须返回相同的正确答案。
保持工作等价再比较策略
这两条语句请求的结果范围不同,直接比较耗时不能证明某种算法普遍更快。要评估同一个负载的候选策略,应固定参数、输出、数据集与并发条件,再一起观察总循环次数、处理行数、缓冲区访问、内存和延迟。估计偏差确实支持统计陈旧的诊断时,再刷新统计;如果主要开销来自反复查找,则检查内层有没有合适的访问路径。正确修复可能是一条针对性的索引或更合理的估计,不必变成对某种连接算法的长期偏好。
来源
- PostgreSQL 18:规划器与优化器,候选访问路径与连接策略,已核验 2026-09-10。
- PostgreSQL 18:使用 EXPLAIN,行数、循环次数与计划解读,已核验 2026-09-10。
- PostgreSQL 18:查询规划配置,规划方法开关及其作用范围,已核验 2026-09-10。