原文地址:https://dev.mysql.com/doc/refman/5.7/en/nested-join-optimization.html
译文:
8.2.1.7嵌套连接优化
连接的语法允许嵌套连接。下面讨论中涉及到的连接语法可以参考Section 13.2.9.2, “JOIN Syntax”。
与SQL标准相比,table_factor的语法得到了扩展。SQL标准只接受table_reference,不接受括号内的列表。如果我们认为table_referenceitems列表中的每个逗号等价于内连接,那么这是一个保守的扩展。
例如:
SELECT * FROM t1 LEFT JOIN (t2, t3, t4)
ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)
等价于:
SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)
在MySQL中,cross join与inner join在语法上是等价的,它们可以相互替换。但在标准的SQL中,它们是不同的,inner join后跟on子句,cross join是不跟on子句的连接(cross join可以理解成全相乘或笛卡尔积)。
通常情况下,只含有inner join操作符的连接表达式中的圆括号可以忽略。考虑下面的连接表达式:
t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
ON t1.a=t2.a
在左移括号和分组操作后,该join表达式转换为如下表达式:
(t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3
ON t2.b=t3.b OR t2.b IS NULL
然而,这两个表达是不相等的。为了了解这一点,假设表t1、t2和t3具有以下状态:
1)表t1包含行 (1)
和(2);
2)表t2包含行(1,101);
3)表t3包含行(101)。
这种情况下,前面的第一个表达式返回的结果集包含行(1,1,101,102)和行(2,NULL,NULL,NULL),而第二个表达式的返回结果返回行(1,1,101,102)和行(2,NULL,NULL,101):
mysql> SELECT *
FROM t1
LEFT JOIN
(t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
ON t1.a=t2.a;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | NULL |
+------+------+------+------+
mysql> SELECT *
FROM (t1 LEFT JOIN t2 ON t1.a=t2.a)
LEFT JOIN t3
ON t2.b=t3.b OR t2.b IS NULL;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | 101 |
+------+------+------+------+
在下面的例子中,外连接操作符与会与内连接操作符结合使用:
t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
上述表达式不能转化成下面的形式:
t1 LEFT JOIN t2 ON t1.a=t2.a, t3
对于给定的状态的表,上述两个表达式会返回不同的结果集:
mysql> SELECT *
FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | NULL |
+------+------+------+------+
mysql> SELECT *
FROM t1 LEFT JOIN t2 ON t1.a=t2.a, t3;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | 101 |
+------+------+------+------+
因此,如果我们在连接表达式中忽略了外连接操作符的圆括号,我们可能会改变原始表达式返回的结果集。
更确切地说,我们不能忽略左外连接操作的右操作数中的括号和右外连接的左操作数中的括号。换句话说,我们不能忽略外部连接操作中的内部表表达式的括号。其他操作数(外部表的操作数)的括号可以忽略。
例如表达式:
(t1,t2) LEFT JOIN t3 ON P(t2.b,t3.b)
上述表达式等价于如下表达式:
t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)
当连接表达式(join_table)中的连接操作的执行顺序不是从左到右时,我们将讨论嵌套连接。考虑如下查询:
SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b) ON t1.a=t2.a
WHERE t1.a > 1
SELECT * FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
WHERE (t2.b=t3.b OR t2.b IS NULL) AND t1.a > 1
上述查询被认为是包含如下所示连接:
t2 LEFT JOIN t3 ON t2.b=t3.b
t2, t3
在第一个查询中,使用左连接操作形成嵌套连接。在第二个查询中,嵌套连接是通过内部连接操作形成的。
在第一个查询中,括号可以省略:连接表达式的语法结构将规定以相同的顺序执行连接操作。对于第二个查询,圆括号不能省略,尽管这里的连接表达式可以在没有圆括号的情况下得到明确的解释。在我们的扩展语法中,第二个查询中(t2,t3)里的括号是需要的,虽然理论上可以在没有括号的情况下解析查询:对于查询,我们仍然会有明确的语法结构,因为left join和on扮演了表达式(t2,t3)左右分隔符的角色。
前面的例子证明了以下观点:
1)对于只包含内部连接(而不是外部连接)的连接表达式,可以删除括号,并从左到右计算连接。事实上,表可以按任何顺序计算;
2)一般来说,对于外部连接或混合了内部连接的外部连接,情况并非如此。去掉括号可能会改变结果。
具有嵌套外部连接的查询以与具有内部连接的查询相同的管道方式执行。更准确地说,它利用了嵌套循环连接算法的一种变体。回想一下以嵌套循环连接算法执行查询 (可以参考Section 8.2.1.6, “Nested-Loop Join Algorithms”)。假设3个表T1、T2、T3上的连接查询具有以下形式:
SELECT * FROM T1 INNER JOIN T2 ON P1(T1,T2)
INNER JOIN T3 ON P2(T2,T3)
WHERE P(T1,T2,T3)
这里P1(T1,T2)和P2(T3,T3)是一些连接条件(在表达式上),而P(T1,T2,T3)是表T1、T2、T3列上的条件。
嵌套循环连接算法以如下方式执行查询:
FOR each row t1 in T1 {
FOR each row t2 in T2 such that P1(t1,t2) {
FOR each row t3 in T3 such that P2(t2,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
符号t1||t2||t3表示将行t1、t2和t3的列串联起来构造的行。在下面的一些示例中,如果表名出现,则表示对该表的每一列使用NULL。例如,t1||t2||NULL表示通过连接行t1和行t2的列构造的行,t3的每一列为都为NULL。这样的行被称为NULL补。
现在考虑一个嵌套外部连接的查询:
SELECT * FROM T1 LEFT JOIN
(T2 LEFT JOIN T3 ON P2(T2,T3))
ON P1(T1,T2)
WHERE P(T1,T2,T3)
对于该查询,修改嵌套循环模式,得到:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t2 in T2 such that P1(t1,t2) {
BOOL f2:=FALSE;
FOR each row t3 in T3 such that P2(t2,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f2=TRUE;
f1=TRUE;
}
IF (!f2) {
IF P(t1,t2,NULL) {
t:=t1||t2||NULL; OUTPUT t;
}
f1=TRUE;
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
通常,对于外部连接操作中第一个内表的任何嵌套循环,都会引入一个标志,该标志在循环之前关闭,在循环之后检查。当外部表中的当前行与表示内部操作数的表匹配时,将打开该标志。如果循环结束时标志仍然关闭,则未找到与外部表的当前行匹配的项。在这种情况下,行由内部表列的NULL值补充。只有当该行满足所有嵌入的外部连接的连接条件时,结果行被传递到输出的最终检查,或者进入下一个嵌套循环。
在本例中,嵌入了以下表达式表示的外部连接表:
(T2 LEFT JOIN T3 ON P2(T2,T3))
对于具有内部连接的查询,优化器可以选择不同顺序的嵌套循环,如下所示:
FOR each row t3 in T3 {
FOR each row t2 in T2 such that P2(t2,t3) {
FOR each row t1 in T1 such that P1(t1,t2) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
对于使用外连接的查询,优化器只能选择外部表的循环先于内部表的循环这样的顺序。因此,对于我们使用外连接的查询,只有一个嵌套顺序是可能的。对于下面的查询,优化器计算两个不同的嵌套。在这两个嵌套中,T1必须在外部循环中处理,因为它在外部连接中使用。T2和T3表用于内部连接,因此连接必须在内部循环中处理。但是,因为连接是一个内连接,T2和T3表可以按照任意一种顺序处理。
SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
WHERE P(T1,T2,T3)
一个嵌套计算先T2,然后是T3:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t2 in T2 such that P1(t1,t2) {
FOR each row t3 in T3 such that P2(t1,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f1:=TRUE
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
另一个嵌套先计算T3,然后是T2:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t3 in T3 such that P2(t1,t3) {
FOR each row t2 in T2 such that P1(t1,t2) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f1:=TRUE
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
在讨论内连接的嵌套循环算法时,我们忽略了一些对查询执行的性能可能有巨大影响的细节。我们没有提及所谓的"下推条件"。假设我们的where条件P(T1,T2,T3)可以用一个连接式表示:
P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3).
在这种情况下,MySQL使用如下所示的嵌套循环算法执行具有内连接的查询:
FOR each row t1 in T1 such that C1(t1) {
FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2) {
FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
可以看到,每个结合点C1(T1)、C2(T2)、C3(T3)都被从最内部的循环推到最外部的循环,在那里可以计算它们。如果C1(T1)是一个非常严格的条件,那么这个条件下推可能会极大地减少从T1表中传到内部循环的行数。因此,执行的查询时间将会大大减少。
对于使用外连接的查询,where条件只有在外部表中的当前行在内部表找到对应的匹配项后才会被检查。因此,将条件推出内嵌套循环的优化不能直接应用于具有外连接的查询。在这里,我们必须引入条件下推谓词,这些谓词由在遇到匹配时才打开的标志保护。
回想一下下面这个带有外连接的例子:
P(T1,T2,T3)=C1(T1) AND C(T2) AND C3(T3)
对于这个例子,使用保护下推条件的内嵌循环算法是这样的:
FOR each row t1 in T1 such that C1(t1) {
BOOL f1:=FALSE;
FOR each row t2 in T2
such that P1(t1,t2) AND (f1?C2(t2):TRUE) {
BOOL f2:=FALSE;
FOR each row t3 in T3
such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) {
IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) {
t:=t1||t2||t3; OUTPUT t;
}
f2=TRUE;
f1=TRUE;
}
IF (!f2) {
IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) {
t:=t1||t2||NULL; OUTPUT t;
}
f1=TRUE;
}
}
IF (!f1 && P(t1,NULL,NULL)) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
一般情况下,下推谓词可以从连接条件如P1(T1,T2)和P(T2,T3)中提取出来。在这种情况下,下推谓词还是由一个标志保护,该标志阻止检查由对应外连接操作产生的空补行的谓词。
如果访问方法是由来自where条件的谓词引发的,则禁止索引键在同一个嵌套连接中从一个内表访问另一个内表。
PS:由于水平有限,译文中难免存在谬误,欢迎批评指正。