嵌套联接优化(JOIN)
表达联接的语法允许嵌套联接。以下讨论引用了“ JOIN子句”中描述的联接语法。
table_factor
与SQL标准相比,的语法得到了扩展。后者仅接受table_reference
,而不接受一对括号内的列表。如果我们将table_reference
项目列表中的每个逗号都视为等效于内部联接,则这是一个保守的扩展。例如:
SELECT *FROM t1LEFT JOIN (t2, t3, t4)ON (t2. =t1. AND t3.b=t1.b AND t4.c=t1.c)
等效于:
SELECT *FROM t1LEFT JOIN (t2CROSS JOIN t3CROSS JOIN t4)ON (t2. =t1. AND t3.b=t1.b AND t4.c=t1.c)
在MySQL中,CROSS JOIN
在语法上等效于INNER JOIN
;;他们可以互相替换。在标准SQL中,它们不是等效的。INNER JOIN
与ON
子句一起使用;CROSS JOIN
否则使用。
通常,在仅包含内部联接操作的联接表达式中可以忽略括号。考虑以下联接表达式:
t1LEFT JOIN (t2LEFT JOIN t3ON t2.b=t3.b OR t2.b IS NULL)ON t1. =t2.
除去左侧的括号和分组操作后,该join表达式将转换为该表达式:
(t1LEFT JOIN t2ON t1. =t2. )LEFT JOIN t3ON 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 t1LEFT JOIN (t2LEFT JOIN t3ON t2.b=t3.b OR t2.b IS NULL)ON t1. =t2. ; +------ +------ +------ +------ + | a | a | b | b | +------ +------ +------ +------ + | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | NULL | +------ +------ +------ +------ + mysql>SELECT *FROM (t1LEFT JOIN t2ON t1. =t2. )LEFT JOIN t3ON t2.b=t3.b OR t2.b IS NULL; +------ +------ +------ +------ + | a | a | b | b | +------ +------ +------ +------ + | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | 101 | +------ +------ +------ +------ +
在以下示例中,将外部联接操作与内部联接操作一起使用:
t1LEFT JOIN (t2, t3)ON t1. =t2.
该表达式不能转换为以下表达式:
t1LEFT JOIN t2ON t1. =t2. , t3
对于给定的表状态,两个表达式返回不同的行集:
mysql>SELECT *FROM t1LEFT JOIN (t2, t3)ON t1. =t2. ; +------ +------ +------ +------ + | a | a | b | b | +------ +------ +------ +------ + | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | NULL | +------ +------ +------ +------ + mysql>SELECT *FROM t1LEFT JOIN t2ON t1. =t2. , t3; +------ +------ +------ +------ + | a | a | b | b | +------ +------ +------ +------ + | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | 101 | +------ +------ +------ +------ +
因此,如果我们在使用外部联接运算符的联接表达式中省略括号,则可能会更改原始表达式的结果集。
更确切地说,我们不能忽略左外部联接操作的右操作数和右联接操作的左操作数的括号。换句话说,我们不能忽略外部联接操作的内部表表达式的括号。另一个操作数(外部表的操作数)的括号可以忽略。
下面的表达式:
(t1,t2)LEFT JOIN t3ON P(t2.b,t3.b)
对于任何表t1,t2,t3
以及P
属性t2.b
和条件上的任何条件,该表达式均等效于此表达式t3.b
:
t1, t2LEFT JOIN t3ON P(t2.b,t3.b)
每当连接表达式(joined_table
)中的连接操作执行顺序不是从左到右时,我们都在谈论嵌套连接。考虑以下查询:
SELECT *FROM t1LEFT JOIN (t2LEFT JOIN t3ON t2.b=t3.b)ON t1. =t2.WHERE t1. > 1SELECT *FROM t1LEFT JOIN (t2, t3)ON t1. =t2.WHERE (t2.b=t3.b OR t2.b IS NULL) AND t1. > 1
这些查询被认为包含以下嵌套联接:
t2LEFT JOIN t3ON t2.b=t3.b t2, t3
在第一个查询中,嵌套联接是通过左联接操作形成的。在第二个查询中,它由内部联接操作形成。
在第一个查询中,可以省略括号:联接表达式的语法结构将规定联接操作的执行顺序。对于第二个查询,尽管可以在没有括号的情况下明确地解释此处的联接表达式,但是不能省略括号。在我们的扩展语法中,(t2, t3)
第二个查询的括号是必需的,尽管从理论上讲可以在没有括号的情况下对其进行解析:由于查询LEFT JOIN
并ON
在表达式的左右定界符中扮演着角色,因此查询仍然具有明确的语法结构(t2,t3)
。
前面的示例说明了以下几点:
- 对于仅涉及内部联接(而不涉及外部联接)的联接表达式,可以删除括号并从左到右评估联接。实际上,可以按任何顺序评估表。
- 通常,对于外部联接或与内部联接混合的外部联接,情况并非如此。删除括号可能会改变结果。
具有嵌套外部联接的查询的执行方式与具有内部联接的查询的执行方式相同。更确切地说,利用了嵌套循环联接算法的一种变体。调用嵌套循环联接执行查询的算法(请参见“嵌套循环联接算法”)。假设对3个表的联接查询T1,T2,T3
具有以下形式:
SELECT *FROM T1INNER JOIN T2ON P1(T1,T2)INNER JOIN T3ON P2(T2,T3)WHERE P(T1,T2,T3)
这里,P1(T1,T2)
和P2(T3,T3)
是一些联接条件(在表达式上),而是P(T1,T2,T3)
在table的列上的条件T1,T2,T3
。
嵌套循环联接算法将以以下方式执行此查询:
FOR each row t1in T1 { FOR each row t2in T2 such that P1(t1,t2) { FOR each row t3in 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
互补的。
现在考虑带有嵌套外部联接的查询:
SELECT *FROM T1LEFT JOIN (T2LEFT JOIN T3ON P2(T2,T3))ON P1(T1,T2)WHERE P(T1,T2,T3)
对于此查询,修改嵌套循环模式以获得:
FOR each row t1in T1 { BOOL f1:=FALSE; FOR each row t2in T2 such that P1(t1,t2) { BOOL f2:=FALSE; FOR each row t3in 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
内部表的列的值补充。结果行将传递到输出的最终检查项或下一个嵌套循环,但前提是该行满足所有嵌入式外部联接的联接条件。
在该示例中,嵌入了以下表达式表示的外部联接表:
(T2LEFT JOIN T3ON P2(T2,T3))
对于具有内部联接的查询,优化器可以选择不同顺序的嵌套循环,例如:
FOR each row t3in T3 { FOR each row t2in T2 such that P2(t2,t3) { FOR each row t1in T1 such that P1(t1,t2) { IF P(t1,t2,t3) { t:=t1||t2||t3; OUTPUT t; } } } }
对于具有外部联接的查询,优化器只能选择以下顺序:外部表的循环优先于内部表的循环。因此,对于带有外部联接的查询,只能使用一个嵌套顺序。对于以下查询,优化器将评估两个不同的嵌套。在这两个嵌套中,T1
必须在外部循环中进行处理,因为它在外部联接中使用。T2
和T3
在内部联接中使用,因此联接必须在内部循环中处理。但是,由于联接是内部联接,T2
因此T3
可以按任何顺序进行处理。
SELECT * T1LEFT JOIN (T2,T3)ON P1(T1,T2) AND P2(T1,T3)WHERE P(T1,T2,T3)
一个嵌套计算T2
,然后T3
:
FOR each row t1in T1 { BOOL f1:=FALSE; FOR each row t2in T2 such that P1(t1,t2) { FOR each row t3in 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 t1in T1 { BOOL f1:=FALSE; FOR each row t3in T3 such that P2(t1,t3) { FOR each row t2in 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 t1in T1 such that C1(t1) { FOR each row t2in T2 such that P1(t1,t2) AND C2(t2) { FOR each row t3in 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 t1in T1 such that C1(t1) { BOOL f1:=FALSE; FOR each row t2in T2 such that P1(t1,t2) AND (f1?C2(t2):TRUE) { BOOL f2:=FALSE; FOR each row t3in 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)
。在这种情况下,下推谓词也由一个标志来保护,该标志防止检查谓词中是否存在NULL
由相应外部联接操作生成的-补行。
如果是由WHERE
条件谓词引起的,则禁止在同一嵌套连接中通过键从一个内部表访问另一个内部表。