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;

这样通常会生成一个比独立的子查询更好些的计划(例如,outer 的WHERE条件可能先把 X 连接到 A 上,这样就消除了 A 中的许多行, 因此避免了形成子查询的全部逻辑输出)。但是同时,我们增加了规划的时间; 在这里,我们用五路连接问题替代了两个独立的三路连接问题。这样的差别是巨大的,因为可能的计划数的是按照指数增长的。 如果有超过from_collapse_limitFROM项将会导致父查询,规划器将尝试通过停止提升子查询来避免卡在巨大的连接搜索问题中。你可以通过调高或调低这个运行时参数在规划时间和计划的质量之间取得平衡。

from_collapse_limitjoin_collapse_limit的命名相似,因为它们做的几乎是同一件事:一个控制规划器何时将把子查询"平面化",另外一个控制何时把显式连接平面化。通常,你要么把join_collapse_limit设置成和from_collapse_limit一样(这样显式连接和子查询的行为类似), 要么把join_collapse_limit设置为 1(如果你想用显式连接控制连接顺序)。 但是你可以把它们设置成不同的值,这样你就可以细粒度地调节规划时间和运行时间之间的平衡。