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(如果你想用明确连接控制连接顺序)。 但是你可以把它们设置成不同的值,这样你就可以在规划时间和运行时间之间进行仔细的调节。