Skip to main content

8.嵌套连接优化

表达连接的语法允许嵌套连接。

与 SQL标准相比,table_factor 的语法得到了扩展。 后者仅接受 table_reference,而不接受一对括号内的列表。 如果我们将 table_reference 项列表中的每个逗号视为等效于内部联接,那么这是一个保守的扩展。 例如:

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。

通常,在仅包含内连接操作的连接表达式中可以忽略括号。 考虑这个连接表达式:

t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
ON t1.a=t2.a

删除括号并将左侧的分组操作后,该连接表达式将转换为以下表达式:

(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 具有以下状态:

  • t1包含行 (1)(2)
  • t2包含行 (1,101)
  • t3包含行 (101)

在本例中,第一个表达式返回包含行 (1,1,101,101), (2,NULL,NULL,NULL) 的结果集,而第二个表达式返回行 (1,1,101,101), (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、t3 以及属性 t2.b 和 t3.b 上的任何条件 P,等效于以下表达式:

t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)

只要连接表达式 (joined_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) 的左右分隔符。

前面的示例说明了以下几点:

  • 对于仅涉及内连接(而不是外连接)的连接表达式,可以删除括号并从左到右计算连接。 事实上,表可以按任何顺序进行评估。
  • 一般来说,对于外连接或与内连接混合的外连接来说,情况并非如此。 删除括号可能会改变结果。

具有嵌套外连接的查询与具有内连接的查询以相同的管道方式执行。 更准确地说,利用了嵌套循环连接算法的变体。 回想一下嵌套循环连接执行查询的算法。 假设对 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 表示该表的每一列都使用 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;
}
}
}

在讨论内连接的嵌套循环算法时,我们忽略了一些对查询执行性能影响可能很大的细节。 我们没有提到所谓的“下推”条件。 假设我们的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;
}
}

如果是由 WHERE 条件的谓词引发的,则禁止在同一嵌套连接中通过键从一个内表到另一个内表进行访问。