2024 年 9 月 26 日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持的版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1

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(如果您想使用显式连接来控制连接顺序)。但是,如果您试图微调规划时间与运行时间之间的权衡,则可能会将它们设置为不同的值。

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步说明,请使用 此表单 报告文档问题。