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 JOIN
或RIGHT JOIN
的大多数实际情况可以在一定程度上重新排列。
显式内部联接语法(INNER JOIN
、CROSS 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_limit和join_collapse_limit的命名类似,因为它们的作用几乎相同:一个控制计划程序何时“平铺”子查询,另一个控制计划程序何时平铺显式连接。通常,您会将join_collapse_limit
设置为等于from_collapse_limit
(以便显式连接和子查询的行为类似),或将join_collapse_limit
设置为 1(如果您想使用显式连接控制连接顺序)。但如果您尝试微调计划时间和运行时间之间的权衡,则可以对它们进行不同的设置。