13.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 连接到该结果上。因此, 这个查询比前面一个花在规划上的时间少。

明确的连接语法(INNER JOINCROSS JOIN, 或者无修饰的 JOIN)语义上和和 FROM 中 列出输入关系是一样的,因此我们没有必要约束连接顺序。 但是我们可以告诉 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(如果你想用明确连接控制连接顺序)。 但是你可以把它们设置成不同的值,这样你就可以在规划时间和运行时间之间进行仔细的调节。