本文问题
- 在包括JOIN操作的查询中,括号有什么影响?
- MySQL中,是否可以更改联接的顺序?
- 什么是嵌套循环,对于嵌套循环,如何进行优化?
JOIN顺序
括号对于联接操作的影响
通常,在只包含内连接操作的语句中可以去除括号。直接去除外联接操作中的括号可能会改变表达式的结果。
示例
假设有以下状态的表t1,t2,t3
-
t1
包括行(1),(2)
-
t2
包括行(1,101)
-
t3
包括行(101)
外联接示例
# 初始语句
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
这两个表达式的结果是不相同的。
第一个表达值返回结果集(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 |
+------+------+------+------+
结论
如果忽略join
表达式中外联接操作中的括号,可能会更改原始表达式的结果。更确切的说,不能忽略左联接右侧操作符中的的括号,或者右联接左侧操作符中的括号。换句话说,我们不能忽略在外联接操作中的内联接操作的括号。其他操作符中(外联接外部)的括号可以被忽略。
比如下面这个表达式:
(t1,t2) LEFT JOIN t3 ON P(t2.b,t3.b)
等同与:
t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)
# 因为(t1,t2)在LEFT JOIN的左侧,所以可以直接忽略该括号。
# 或者说,因为(t1,t2)在外联接(LEFT JOIN)的外侧,所以可以忽略该括号
嵌套联接
嵌套联接定义
当在join
表达式中执行join
操作的顺序不是直接从左到右执行时,我们称之为嵌套联接。
例如以下查询:
SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b) ON t1.a=t2.a
WHERE t1.a > 1
# 先执行t2 left join t3,然后再执行t1 left join上一步的结果
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,t3)然后执行t2 left join结果
# join(t2,t3)=join(j2 inner join t3)
上述表达式中的t2 LEFT JOIN t3 ON t2.b=t3.b
和t2, t3
就是嵌套联接内层的联接。
在第一个查询中是一个左联接操作,在第二个查询中是个内联接操作。
括号对于嵌套联接的影响
在第一个查询中,可以忽略括号,join
表达式的语法结果决定连接执行的顺序是一致的。
但是第二个查询不能忽略括号,尽管这里的join
表达式可以在忽略括号的情况下明确地解释。在我们的语法中,第二个查询中(t2,t3)
需要使用括号,尽管理论上可以在忽略括号的情况下进行查询:我们仍然需要对这个查询使用明确的语法结果,因为LEFT JOIN
和ON
是表达式(t2,t3)
的左右分隔符的角色。
结论
上述示例表明:
- 对于只调用
inner joins
的join
表达式,可以忽略括号,并且left join
等同于right join
。事实上,表在任何顺序上join
结果都是一致的 - 但是,对于外连接或者混合了内连接的外连接,删除括号可能会导致结果集变更。
嵌套外连接查询执行过程
嵌套外联接查询的执行类似于内连接。更确切地说,是利用nested-loop join
算法的变体。回想一下使用NLJ
执行查询的算法。假设有一个覆盖了T1,T2,T3
3个表的join查询:
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(T2,T3)
是join
条件。而P(T1,T2,T3)
是一个跨越3个表的WHERE
条件。
nested-loop join
算法将会按照以下顺序执行查询:
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
中的行,并使用NULL
表示t3
中的每一列。这种行被称为NULL-complemented
(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 { #对于每个T1表中的行
BOOL f1:=FALSE; # 标志f1=false
FOR each row t2 in T2 such that P1(t1,t2) { # 对于每个符合t1,t2条件的T2中的行
BOOL f2:=FALSE; # 标志f2=false
FOR each row t3 in T3 such that P2(t2,t3) { #对于每个符合t2,t3条件的 T3中的行
IF P(t1,t2,t3) { #如果符合WHERE条件
t:=t1||t2||t3; OUTPUT t; #输出3个表的列值
} # 判断结束
f2=TRUE; # f2=Ture
f1=TRUE; # f2=True
} # T3表循环结束
IF (!f2) { # 如果f2=false
IF P(t1,t2,NULL) { #如果符合t2,t2表的条件
t:=t1||t2||NULL; OUTPUT t; # 输出符合条件的t1,t2表的列值,t3列值用NULL代替
} # 判断结束
f1=TRUE; # f1=true
}
} #T2循环结束
IF (!f1) {
IF P(t1,NULL,NULL) { # 如果符合t1表的条件
t:=t1||NULL||NULL; OUTPUT t; #输出符合t1表条件的列值,t2,t3表中的列值都用null代替
}
}
} #T1循环结束
通常,对于外连接的每个内部的循环操作,会设置一个标志,这个标志在循环开始时关闭,并在循环结束后检查标志。如果当前循环外表中的行的内部操作中发现了匹配的行,标志开启。在循环结束后,如果标志仍然是关闭的,表示当前外表中的行在内层表中没有发现匹配的数据,在这种情况下,使用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;
}
}
}
条件外推("pushed-down" conditions
)
内联接优化示例
在上面讨论内联接的嵌套循环算法时,没有提到可能对查询性能有巨大影响的条件。没有提到"pushed-down" conditions
。假设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
表中进入内循环的行数。然后,查询的执行效率可以大量提升。
pushed-down
条件,将循环内部的WHERE
条件,外推到相关表的循环外部处理。
外联接优化示例
对于外连接查询,只有外部表在内部表中具有匹配的行之后才对WHERE
条件进行检查。
也就是说,对于外联接查询,不能将嵌套循环内部的条件直接外推到循环外部进行处理。
还是上面的示例
P(T1,T2,T3)=C1(T1) AND C(T2) AND C3(T3)
对于这个示例,嵌套循环算法使用如下使用pushed-down
条件:
FOR each row t1 in T1 such that C1(t1) { #对于每个符合条件的t1表行
BOOL f1:=FALSE; # 设置标志f1为FALSE
FOR each row t2 in T2
such that P1(t1,t2) AND (f1?C2(t2):TRUE) { # 对于T2中行,满足P1(t1,t2)条件,如果f1=TRUE,还需要满足C2(t2)条件,否则只满足P1条件即可
BOOL f2:=FALSE; #设置标志f2为FALSE
FOR each row t3 in T3
such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) { #对于T3中的行
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;
}
}
问题答案
- 在包括JOIN操作的查询中,括号有什么影响?
在只包含内连接操作的语句中可以直接去除括号,更改内联接顺序不会变更结果集
但是不能直接忽略在外联接操作中的内联接操作的括号,会改变联接顺序,导致结果集变更。 - MySQL中,是否可以更改联接的顺序?
对于内联接,可以任意更改处理联接中的表的顺序。
对于外联接,必须先处理外部表,然后再处理内部表。 - 什么是嵌套循环,对于嵌套循环,如何进行优化?
- 从联接顺序方面
嵌套内联接可以任意更改嵌套顺序
嵌套外联接,外联接的顺序不能更改。如果一个内联接在嵌套连接的最内层,可以更改这个内联接相关的表的顺序。 - 从
WHERE
条件方面
嵌套内联接可以直接将WHERE
条件外推到循环外部进行处理,对于不符合WHERE
的行,不需要进入循环。
嵌套内联接无法直接将WHERE
条件外推到循环外部,只有在循环内部的表中查找到匹配的行后才能进行WHERE
条件判断
网友评论