受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

14.3. 用显式JOIN子句控制规划器 #

可以在一定程度上用显式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子句中并没有可用于优化这一连接的条件。(PostgreSQL执行器中的所有连接都发生在两个输入表之间,因此结果必须以这些形式之一逐步构造出来。)关键在于,这些不同的连接可能性在语义上是等价的,但执行代价可能相差极大。因此,规划器会探索它们,力图找出最高效的查询计划。

当查询只涉及两个或三个表时,需要考虑的连接顺序并不多。但可能的连接顺序数量会随着表数增加而呈指数增长。输入表超过十个左右之后,对所有可能性做穷举搜索实际上就不再可行,甚至六七个表也可能让规划耗时长得令人厌烦。输入表过多时,PostgreSQL规划器会从穷举搜索切换到一种遗传概率搜索,只考虑有限数量的可能性。(切换阈值由运行时参数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 连接到该结果上。相应地,这个查询比前一个查询需要更少的规划时间。在其他情况下,规划器可能会判断多种连接顺序都是安全的。例如:

SELECT * FROM a LEFT JOIN b ON (a.bid = b.id) LEFT JOIN c ON (a.cid = c.id);

将 A 首先连接到 B 或 C 都是有效的。当前,只有FULL JOIN完全约束连接顺序。大多数涉及LEFT JOINRIGHT JOIN的实际情况都在某种程度上可以被重新排列。

显式连接语法(INNER JOINCROSS JOIN或无修饰的JOIN)在语义上和FROM中列出输入关系是一样的, 因此它不约束连接顺序。

即使大多数类型的JOIN并不会完全约束连接顺序,仍然可以指示PostgreSQL查询规划器将所有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);

但如果告诉规划器遵循JOIN顺序,那么第二个和第三个查询在规划上会比第一个花费更少时间。对于只有三个表的连接来说,这种效果微不足道;但当表很多时,它可能非常关键。

要强制规划器遵循显式JOIN给出的连接顺序,可以将运行时参数join_collapse_limit设置为 1。(其他可能值见下文讨论。)

不必为了缩短搜索时间而完全约束连接顺序,因为可以在普通FROM列表的某一项中使用JOIN操作符。例如:

SELECT * FROM a CROSS JOIN b, c, d, e WHERE ...;

如果设置join_collapse_limit = 1,就会强制规划器先将 A 连接到 B,然后再与其他表连接,但不会进一步约束它的选择。在这个示例中,可能的连接顺序数量减少了 5 倍。

以这种方式约束规划器的搜索,是一种既可减少规划时间、又可引导规划器生成更好查询计划的实用技巧。如果规划器默认选择了糟糕的连接顺序,可以通过JOIN语法强制它采用更好的顺序,前提当然是确实知道哪个顺序更好。建议进行实验。

与此密切相关、同样会影响规划时间的另一个问题,是将子查询折叠进父查询。例如:

SELECT *
FROM x, y,
    (SELECT * FROM a, b, c WHERE something) AS ss
WHERE somethingelse;

这种情况可能出现在使用包含连接的视图时;该视图的SELECT规则会被插入到引用视图的位置,从而得到一个与上面非常相似的查询。通常,规划器会尝试把子查询折叠进父查询,得到:

SELECT * FROM x, y, a, b, c WHERE something AND somethingelse;

这通常会生成比单独规划子查询更好的计划。(例如,外层WHERE条件可能会先把 X 连接到 A,从而消除 A 中的大量行,也就避免了形成子查询完整逻辑输出的需要。)但与此同时,规划时间也增加了;这里把两个彼此独立的三路连接问题,替换成了一个五路连接问题。由于可能性数量呈指数增长,这种差别可能非常大。为了避免陷入巨大的连接搜索问题,如果折叠子查询会使父查询产生超过from_collapse_limitFROM项,规划器就会尝试通过停止提升子查询来规避这一点。可以通过调高或调低这个运行时参数,在规划时间和计划质量之间做权衡。

from_collapse_limitjoin_collapse_limit名称相似,因为它们做的几乎是同一件事:一个控制规划器何时将子查询平面化,另一个控制何时将显式连接平面化。通常,要么把join_collapse_limit设置为与from_collapse_limit相同,这样显式连接与子查询的行为类似;要么把join_collapse_limit设置为 1,如果想使用显式连接控制连接顺序。不过,也可以把它们设置为不同的值,以更细致地调节规划时间与运行时间之间的平衡。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。