2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 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;

规划器可以自由地以任何顺序连接给定的表。例如,它可以生成一个连接 A 和 B 的查询计划,使用 WHERE 条件 a.id = b.id,然后使用另一个 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 或先连接 A 到 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 连接到其他表之前先连接 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(如果您想用显式连接控制连接顺序)。但如果您试图微调规划时间和运行时间之间的权衡,您可能需要设置不同的值。

提交更正

如果您在文档中发现任何不正确之处、与您对特定功能的体验不符之处或需要进一步说明之处,请使用 此表格 报告文档问题。