pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
从Postgres 7.1 开始,就可以用显式 JOIN 语法在一定程度上控制查询规划器。要理解这一点为何重要,先需要一些背景知识。
在一个简单的连接查询中,例如
SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id;
规划器可以自由地按任意顺序连接这些表。例如,它可以生成一个查询计划,先利用 WHERE 子句 a.id = b.id 将 A 连接到 B,然后再利用另一个 WHERE 子句把 C 连接到这个结果上。也可以先连接 B 和 C,再把 A 连接到所得结果上。甚至还可以先连接 A 和 C,再与 B 连接——但这样效率会很低,因为必须先形成 A 和 C 的完整笛卡尔积,而 WHERE 子句中并没有可用于优化这一连接的条件。(Postgres执行器中的所有连接都发生在两个输入表之间,因此结果必须以这些形式之一逐步构造出来。)关键在于,这些不同的连接可能性在语义上是等价的,但执行代价可能相差极大。因此,规划器会探索它们,力图找出最高效的查询计划。
当查询只涉及两个或三个表时,需要考虑的连接顺序并不多。但可能的连接顺序数量会随着表数增加而呈指数增长。输入表超过十个左右之后,对所有可能性做穷举搜索实际上就不再可行,甚至六七个表也可能让规划耗时长得令人厌烦。输入表过多时,Postgres规划器会从穷举搜索切换到一种遗传概率搜索,只考虑有限数量的可能性。(切换阈值由运行时参数 GEQO_THRESHOLD 控制,管理员指南中有描述。)遗传搜索耗时更少,但并不一定能找到最优计划。
当查询涉及外连接时,规划器比处理普通(内)连接时拥有更小的自由度。例如,考虑
SELECT * FROM a LEFT JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);
尽管这个查询的约束表面上与前一个非常相似,但它们的语义不同,因为如果 A 中有某一行无法匹配 B 和 C 连接结果中的任何行,该行仍然必须被输出。因此这里规划器对连接顺序没有选择:它必须先连接 B 和 C,再把 A 连接到该结果上。相应地,这个查询比前一个查询需要更少的规划时间。
在Postgres 7.1 中,规划器把所有显式 JOIN 语法都当作带有连接顺序约束来处理,即使对内连接来说这种约束在逻辑上并不必要。因此,尽管下面这些查询给出的结果相同:
SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id; SELECT * FROM a CROSS JOIN b CROSS JOIN c WHERE a.id = b.id AND b.ref = c.id; SELECT * FROM a JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);
第二个和第三个查询在规划上会比第一个花费更少时间。对于只有三个表的连接来说,这种效果微不足道;但当表很多时,它可能非常关键。
不必为了缩短搜索时间而完全约束连接顺序,因为可以在普通 FROM 列表中使用 JOIN 操作符。例如,
SELECT * FROM a CROSS JOIN b, c, d, e WHERE ...;
这会强制规划器先将 A 连接到 B,然后再与其他表连接,但不会进一步约束它的选择。在这个示例中,可能的连接顺序数量减少了 5 倍。
如果在一个复杂查询中混合使用外连接和内连接,你可能不希望约束规划器在 外连接内部对内连接的良好顺序的搜索。在 JOIN 语法中无法直接做到这一点,但可以通过使用子查询绕过这一语法限制。例如,
SELECT * FROM d LEFT JOIN
(SELECT * FROM a, b, c WHERE ...) AS ss
ON (...);
这里,连接 D 必须是查询计划的最后一步,但规划器可以自由地考虑 A,B,C 的各种连接顺序。
以这种方式约束规划器的搜索,是一种既可减少规划时间、又可引导规划器生成更好查询计划的实用技巧。如果规划器默认选择了糟糕的连接顺序,可以通过 JOIN 语法强制它采用更好的顺序——前提当然是确实知道哪个顺序更好。建议进行实验。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。