Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

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_limit FROM 项。你可以通过向上或向下调整此运行时参数来权衡规划时间与规划质量。

from_collapse_limitjoin_collapse_limit 名称相近,因为它们的功能几乎相同:一个控制计划程序何时 展平 子查询,而另一个控制计划程序何时展平显式连接。通常,你可以将 join_collapse_limit 设置为等于 from_collapse_limit(以便显式连接和子查询以相似的方式起作用),或将 join_collapse_limit 设置为 1(如果你希望利用显式连接来控制连接顺序)。但是,如果你正在尝试微调规划时间和运行时间之间的权衡,则可以对它们进行不同的设置。