连接策略按两侧行数来选
连接策略负责组合两路输入,并不是某段 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 实测中,限定客户编号不超过三时采用嵌套循环。内层位图堆扫描每轮返回一百条购买记录,一共执行三轮。连接输出三百行,金额总和为三千。嵌套循环里出现位图扫描是正常组合:扫描节点描述内层记录怎样被取到,连接节点描述怎样反复利用外层记录组合结果,两者不是互斥选项。 ...