1. MySQL怎样优化WHERE子句
该节讨论为处理WHERE子句而进行的优化。例子中使用了SELECT语句,但相同的优化也适用DELETE和UPDATE语句中的WHERE子句。
请注意对MySQL优化器的工作在不断进行中,因此该节并不完善。MySQL执行了大量的优化,本文中所列的并不详尽。
下面列出了MySQL执行的部分优化:
· 去除不必要的括号:
· ((a AND b) AND c OR (((a AND b) AND (c AND d))))
· -> (a AND b AND c) OR (a AND b AND c AND d)
· 常量重叠:
· (a<b AND b=c) AND a=5
· -> b>5 AND b=c AND a=5
· 去除常量条件(由于常量重叠需要):
· (B>=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)
· -> B=5 OR B=6
· 索引使用的常数表达式仅计算一次。
-
对于MyISAM和HEAP表,在一个单个表上的没有一个WHERE的COUNT(*)直接从表中检索信息。当仅使用一个表时,对NOT NULL表达式也这样做。
-
无效常数表达式的早期检测。MySQL快速检测某些SELECT语句是不可能的并且不返回行。
-
如果不使用GROUP BY或分组函数(COUNT()、MIN()……),HAVING与WHERE合并。
-
对于联接内的每个表,构造一个更简单的WHERE以便更快地对表进行WHERE计算并且也尽快跳过记录。
-
所有常数的表在查询中比其它表先读出。常数表为:
-
空表或只有1行的表。
-
与在一个PRIMARY KEY或UNIQUE索引的WHERE子句一起使用的表,这里所有的索引部分使用常数表达式并且索引部分被定义为NOT NULL。
-
下列的所有表用作常数表:
mysql> SELECT * FROM t WHERE primary_key=1;
mysql> SELECT * FROM t1,t2
WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
-
尝试所有可能性便可以找到表联接的最好联接组合。如果所有在ORDER BY和GROUP BY的列来自同一个表,那么当联接时,该表首先被选中。
-
如果有一个ORDER BY子句和不同的GROUP BY子句,或如果ORDER BY或GROUP BY包含联接队列中的第一个表之外的其它表的列,则创建一个临时表。
-
如果使用SQL_SMALL_RESULT,MySQL使用内存中的一个临时表。
-
每个表的索引被查询,并且使用最好的索引,除非优化器认为使用表扫描更有效。是否使用扫描取决于是否最好的索引跨越超过30%的表。优化器更加复杂,其估计基于其它因素,例如表大小、行数和I/O块大小,因此固定比例不再决定选择使用索引还是扫描。
-
在一些情况下,MySQL能从索引中读出行,甚至不查询数据文件。如果索引使用的所有列是数值类,那么只使用索引树来进行查询。
-
输出每个记录前,跳过不匹配HAVING子句的行。
下面是一些快速查询的例子:
SELECT COUNT(*) FROM tbl_name;
SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;
SELECT MAX(key_part2) FROM tbl_name
WHERE key_part1=constant;
SELECT ... FROM tbl_name
ORDER BY key_part1,key_part2,... LIMIT 10;
SELECT ... FROM tbl_name
ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;
下列查询仅使用索引树就可以解决(假设索引的列为数值型):
SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;
SELECT COUNT(*) FROM tbl_name
WHERE key_part1=val1 AND key_part2=val2;
SELECT key_part2 FROM tbl_name GROUP BY key_part1;
下列查询使用索引按排序顺序检索行,不用另外的排序:
SELECT ... FROM tbl_name
ORDER BY key_part1,key_part2,... ;
SELECT ... FROM tbl_name
ORDER BY key_part1 DESC, key_part2 DESC, ... ;
2. 范围优化
range访问方法使用单一索引来搜索包含在一个或几个索引值距离内的表记录的子集。可以用于单部分或多元素索引。后面的章节将详细描述如何从WHERE子句提取区间。
2.1 单元素索引的范围访问方法
对于单元素索引,可以用WHERE子句中的相应条件很方便地表示索引值区间,因此我们称为范围条件而不是“区间”。
单元素索引范围条件的定义如下:
· 对于BTREE和HASH索引,当使用=、<=>、IN、IS NULL或者IS NOT NULL操作符时,关键元素与常量值的比较关系对应一个范围条件。
· 对于BTREE索引,当使用>、<、>=、<=、BETWEEN、!=或者<>,或者LIKE 'pattern'(其中 'pattern'不以通配符开始)操作符时,关键元素与常量值的比较关系对应一个范围条件。
· 对于所有类型的索引,多个范围条件结合OR或AND则产生一个范围条件。
前面描述的“常量值”系指:
· 查询字符串中的常量
· 同一联接中的const或system表中的列
· 无关联子查询的结果
· 完全从前面类型的子表达式组成的表达式
下面是一些WHERE子句中有范围条件的查询的例子:
SELECT * FROM t1
WHERE key_col > 1
AND key_col < 10;
SELECT * FROM t1
WHERE key_col = 1
OR key_col IN (15,18,20);
SELECT * FROM t1
WHERE key_col LIKE 'ab%'
OR key_col BETWEEN 'bar' AND 'foo';
请注意在常量传播阶段部分非常量值可以转换为常数。
MySQL尝试为每个可能的索引从WHERE子句提取范围条件。在提取过程中,不能用于构成范围条件的条件被放弃,产生重叠范围的条件组合到一起,并且产生空范围的条件被删除。
例如,考虑下面的语句,其中key1是有索引的列,nonkey没有索引:
SELECT * FROM t1 WHERE
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
(key1 < 'bar' AND nonkey = 4) OR
(key1 < 'uux' AND key1 > 'z');
key1的提取过程如下:
\1. 用原始WHERE子句开始:
\2. (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
\3. (key1 < 'bar' AND nonkey = 4) OR
\4. (key1 < 'uux' AND key1 > 'z')
\5. 删除nonkey = 4和key1 LIKE '%b',因为它们不能用于范围扫描。删除它们的正确途径是用TRUE替换它们,以便进行范围扫描时不会丢失匹配的记录。用TRUE替换它们后,可以得到:
6. (key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR
7. (key1 < 'bar' AND TRUE) OR
8. (key1 < 'uux' AND key1 > 'z')
\9. 取消总是为true或false的条件:
· (key1 LIKE 'abcde%' OR TRUE)总是true
· (key1 < 'uux' AND key1 > 'z')总是false
用常量替换这些条件,我们得到:
(key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)
删除不必要的TRUE和FALSE常量,我们得到
(key1 < 'abc') OR (key1 < 'bar')
10.将重叠区间组合成一个产生用于范围扫描的最终条件:
11. (key1 < 'bar')
总的来说(如前面的例子所述),用于范围扫描的条件比WHERE子句限制少。MySQL再执行检查以过滤掉满足范围条件但不完全满足WHERE子句的行。
范围条件提取算法可以处理嵌套的任意深度的AND/OR结构,并且其输出不依赖条件在WHERE子句中出现的顺序。
2.2 多元素索引的范围访问方法
多元素索引的范围条件是单元素索引的范围条件的扩展。多元素索引的范围条件将索引记录限制到一个或几个关键元组内。使用索引的顺序,通过一系列关键元组来定义关键元组区间。
例如,考虑定义为key1(key_part1, key_part2, key_part3)的多元素索引,以及下面的按关键字顺序所列的关键元组:
key_part1 key_part2 key_part3
NULL 1 'abc'
NULL 1 'xyz'
NULL 2 'foo'
1 1 'abc'
1 1 'xyz'
1 2 'abc'
2 1 'aaa'
条件key_part1 = 1定义了下面的范围:
(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1,+inf,+inf)
范围包括前面数据集中的第4、5和6个元组,可以用于范围访问方法。
通过对比,条件key_part3 = 'abc'不定义单一的区间,不能用于范围访问方法。
下面更加详细地描述了范围条件如何用于多元素索引中。
· 对于HASH索引,可以使用包含相同值的每个区间。这说明区间只能由下面形式的条件产生:
· key_part1 cmp const1
· AND key_part2 cmp const2
· AND ...
· AND key_partN cmp constN;
这里,const1,const2,...为常量,cmp是=、<=>或者IS NULL比较操作符之一,条件包括所有索引部分。(也就是说,有N 个条件,每一个对应N-元素索引的每个部分)。
关于常量的定义,参见7.2.5.1节,“单元素索引的范围访问方法”。
例如,下面为三元素HASH索引的范围条件:
key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
· 对于BTREE索引,区间可以对结合AND的条件有用,其中每个条件用一个常量值通过=、<=>、IS NULL、>、<、>=、<=、!=、<>、BETWEEN或者LIKE 'pattern' (其中'pattern'不以通配符开头)比较一个关键元素。区间可以足够长以确定一个包含所有匹配条件(或如果使用<>或!=,为两个区间)的记录的单一的关键元组。例如,对于条件:
· key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
单一区间为:
('foo',10,10)
< (key_part1,key_part2,key_part3)
< ('foo',+inf,+inf)
创建的区间可以比原条件包含更多的记录。例如,前面的区间包括值('foo',11,0),不满足原条件。
· 如果包含区间内的一系列记录的条件结合使用OR,则形成包括一系列包含在区间并集的记录的一个条件。如果条件结合使用了AND,则形成包括一系列包含在区间交集内的记录的一个条件。例如,对于两部分索引的条件:
· (key_part1 = 1 AND key_part2 < 2)
· OR (key_part1 > 5)
区间为:
(1, -inf) < (key_part1, key_part2) < (1, 2)
(5, -inf) < (key_part1, key_part2)
在该例子中,第1行的区间左侧的约束使用了一个关键元素,右侧约束使用了两个关键元素。第2行的区间只使用了一个关键元素。EXPLAIN输出的key_len列表示所使用关键字前缀的最大长度。
在某些情况中,key_len可以表示使用的关键元素,但可能不是你所期望的。假定key_part1和key_part2可以为NULL。则key_len列显示下面条件的两个关键元素的长度:
key_part1 >= 1 AND key_part2 < 2
但实际上,该条件可以变换为:
key_part1 >= 1 AND key_part2 IS NOT NULL
7.2.5.1节,“单元素索引的范围访问方法”描述了如何进行优化以结合或删除单元素索引范围条件的区间。多元素索引范围条件的区间的步骤类似。
3.索引合并优化
索引合并方法用于通过range扫描搜索行并将结果合成一个。合并会产生并集、交集或者正在进行的扫描的交集的并集。
在EXPLAIN输出中,该方法表现为type列内的index_merge。在这种情况下,key列包含一列使用的索引,key_len包含这些索引的最长的关键元素。
例如:
SELECT * FROM tbl_name WHERE key_part1 = 10 OR key_part2 = 20;
SELECT * FROM tbl_name
WHERE (key_part1 = 10 OR key_part2 = 20) AND non_key_part=30;
SELECT * FROM t1, t2
WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
AND t2.key1=t1.some_col;
SELECT * FROM t1, t2
WHERE t1.key1=1
AND (t2.key1=t1.some_col OR t2.key2=t1.some_col2);
索引合并方法有几种访问算法 (参见EXPLAIN输出的Extra字段):
· 交集
· 联合
· 排序并集
后面几节更加详细地描述了这些方法。
注释:索引合并优化算法具有以下几个已知缺陷:
· 如果可以对某些关键字进行范围扫描,则不考虑索引合并。例如,下面的查询:
· SELECT * FROM t1 WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
对于该查询,可以有两个方案:
\1. 使用(goodkey1 < 10 OR goodkey2 < 20)条件进行索引合并扫描。
\2. 使用badkey < 30条件进行范围扫描。
然而,优化器只考虑第2个方案。如果这不是你想要的,你可以通过使用IGNORE INDEX或FORCE INDEX让优化器考虑index_merge。下面的查询使用索引合并执行:
SELECT * FROM t1 FORCE INDEX(goodkey1,goodkey2)
WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
SELECT * FROM t1 IGNORE INDEX(badkey)
WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
· 如果查询有一个复杂的WHERE子句,有较深的AND/OR嵌套关系,MySQL不选择该优选方案,通过下面的识别法则尝试分布各条件:
· (x AND y) OR z = (x OR z) AND (y OR z)
· (x OR y) AND z = (x AND z) OR (y AND z)
index_merge访问方法的不同变量之间的选择和其它访问方法基于各适用选项的成本估计。
3.1 索引合并交集访问算法
该访问算法可以用于当WHERE子句结合AND被转换为不同的关键字的几个范围条件,每个条件为下面之一:
· 以这种形式,即索引有确切的N部分(即包括了所有索引部分):
· key_part1=const1 AND key_part2=const2 ... AND key_partN=constN
· 任何InnoDB或BDB表的主键的范围条件。
下面是一些例子:
SELECT * FROM innodb_table WHERE primary_key < 10 AND key_col1=20;
SELECT * FROM tbl_name
WHERE (key1_part1=1 AND key1_part2=2) AND key2=2;
索引合并交集算法同时对所有使用的索引进行扫描,并产生从合并的索引扫描接收的行序列的交集。
如果使用的索引包括查询中使用的所有列,所有表记录均不搜索,并且在这种情况下EXPLAIN的输出包含Extra字段中的Using index。下面是一个此类查询的例子:
SELECT COUNT(*) FROM t1 WHERE key1=1 AND key2=1;
如果使用的索引未包括查询中使用的所有列,只有满足所有使用的关键字的范围条件才搜索所有记录。
如果某个合并条件是InnoDB或BDB表的主键的一个条件,不用于记录查询,但用于过滤使用其它条件搜索的记录。
3.2 索引合并并集访问算法
该算法的适用标准类似于索引合并方法交集算法的标准。算法可以用于当WHERE子句结合OR被转换为不同的关键字的几个范围条件的时候,每个条件为下面之一:
· 以这种形式,即索引有确切的N部分(即包括了所有索引部分):
· key_part1=const1 AND key_part2=const2 ... AND key_partN=constN
· 任何InnoDB或BDB表的主键的范围条件。
· 索引合并方法交集算法适用的一个条件。
下面是一些例子:
SELECT * FROM t1 WHERE key1=1 OR key2=2 OR key3=3;
SELECT * FROM innodb_table WHERE (key1=1 AND key2=2) OR
(key3='foo' AND key4='bar') AND key5=5;
3.3 索引合并排序并集访问算法
该访问算法可以用于当WHERE子句结合OR被转换为不同的关键字的几个范围条件,但索引合并方法联合算法并不适用的时候。
下面是一些例子:
SELECT * FROM tbl_name WHERE key_col1 < 10 OR key_col2 < 20;
SELECT * FROM tbl_name
WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col=30;
排序联合算法和联合算法的区别是排序联合算法必须先索取所有记录的行ID,然后在返回记录前对它们进行排序。
4. MySQL如何优化IS NULL
MySQL可以对可以结合col_name = constant_value使用的col_name IS NULL进行相同的优化。例如,MySQL可以使用索引和范围用IS NULL搜索NULL。
SELECT * FROM tbl_name WHERE key_col IS NULL;
SELECT * FROM tbl_name WHERE key_col <=> NULL;
SELECT * FROM tbl_name
WHERE key_col=const1 OR key_col=const2 OR key_col IS NULL;
如果WHERE子句包括声明为NOT NULL的列的col_name IS NULL条件,表达式则优化。当列会产生NULL时,不会进行优化;例如,如果来自LEFT JOIN右侧的表。
MySQL也可以优化组合col_name = expr AND col_name IS NULL,这是解决子查询的一种常用形式。当使用优化时EXPLAIN显示ref_or_null。
该优化可以为任何关键元素处理IS NULL。
下面是一些优化的查询例子,假定表t2的列a和b有一个索引:
SELECT * FROM t1 WHERE t1.a=expr OR t1.a IS NULL;
SELECT * FROM t1, t2 WHERE t1.a=t2.a OR t2.a IS NULL;
SELECT * FROM t1, t2
WHERE (t1.a=t2.a OR t2.a IS NULL) AND t2.b=t1.b;
SELECT * FROM t1, t2
WHERE t1.a=t2.a AND (t2.b=t1.b OR t2.b IS NULL);
SELECT * FROM t1, t2
WHERE (t1.a=t2.a AND t2.a IS NULL AND ...)
OR (t1.a=t2.a AND t2.a IS NULL AND ...);
ref_or_null首先读取参考关键字,然后单独搜索NULL关键字的行。
请注意该优化只可以处理一个IS NULL。在后面的查询中,MySQL只对表达式(t1.a=t2.a AND t2.a IS NULL)使用关键字查询,不能使用b的关键元素:
SELECT * FROM t1, t2
WHERE (t1.a=t2.a AND t2.a IS NULL)
OR (t1.b=t2.b AND t2.b IS NULL);
5. MySQL如何优化DISTINCT
在许多情况下结合ORDER BY的DISTINCT需要一个临时表。
请注意因为DISTINCT可能使用GROUP BY,必须清楚MySQL如何使用所选定列的一部分的ORDER BY或HAVING子句中的列。参见12.10.3节,“具有隐含字段的GROUP BY”。
在大多数情况下,DISTINCT子句可以视为GROUP BY的特殊情况。例如,下面的两个查询是等效的:
SELECT DISTINCT c1, c2, c3 FROM t1 WHERE c1 > const;
SELECT c1, c2, c3 FROM t1 WHERE c1 > const GROUP BY c1, c2, c3;
由于这个等效性,适用于GROUP BY查询的优化也适用于有DISTINCT子句的查询。这样,关于DISTINCT查询的优化的更详细的情况,参见7.2.13节,“MySQL如何优化GROUP BY``”。
结合LIMIT row_count和DISTINCT后,MySQL发现唯一的row_count行后立即停止。
如果不使用查询中命名的所有表的列,MySQL发现第1个匹配后立即停止扫描未使用的表。在下面的情况中,假定t1在t2之前使用(可以用EXPLAIN检查),发现t2中的第1行后,MySQL不再(为t1中的任何行)读t2:
SELECT DISTINCT t1.a FROM t1, t2 where t1.a=t2.a;
6. MySQL如何优化LEFT JOIN和RIGHT JOIN
在MySQL中,A LEFT JOIN B join_condition执行过程如下:
· 根据表A和A依赖的所有表设置表B。
· 根据LEFT JOIN条件中使用的所有表(除了B)设置表A。
· LEFT JOIN条件用于确定如何从表B搜索行。(换句话说,不使用WHERE子句中的任何条件)。
· 可以对所有标准联接进行优化,只是只有从它所依赖的所有表读取的表例外。如果出现循环依赖关系,MySQL提示出现一个错误。
· 进行所有标准WHERE优化。
· 如果A中有一行匹配WHERE子句,但B中没有一行匹配ON条件,则生成另一个B行,其中所有列设置为NULL。
· 如果使用LEFT JOIN找出在某些表中不存在的行,并且进行了下面的测试:WHERE部分的col_name IS NULL,其中col_name是一个声明为 NOT NULL的列,MySQL找到匹配LEFT JOIN条件的一个行后停止(为具体的关键字组合)搜索其它行。
RIGHT JOIN的执行类似LEFT JOIN,只是表的角色反过来。
联接优化器计算表应联接的顺序。LEFT JOIN和STRAIGHT_JOIN强制的表读顺序可以帮助联接优化器更快地工作,因为检查的表交换更少。请注意这说明如果执行下面类型的查询,MySQL进行全扫描b,因为LEFT JOIN强制它在d之前读取:
SELECT *
FROM a,b LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
WHERE b.key=d.key;
在这种情况下修复时用a的相反顺序,b列于FROM子句中:
SELECT *
FROM b,a LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
WHERE b.key=d.key;
MySQL可以进行下面的LEFT JOIN优化:如果对于产生的NULL行,WHERE条件总为假,LEFT JOIN变为普通联接。
例如,在下面的查询中如果t2.column1为NULL,WHERE 子句将为false:
SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;
因此,可以安全地将查询转换为普通联接:
SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;
这样可以更快,因为如果可以使查询更佳,MySQL可以在表t1之前使用表t2。为了强制使用表顺序,使用STRAIGHT_JOIN。
7. MySQL如何优化嵌套Join
表示联接的语法允许嵌套联接。
同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,1,101,101}、{2,NULL,NULL,NULL}的结果,第2个表达式返回行{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.
对于给定的表状态,第1个表达式返回行{1,1,101,101}、{2,NULL,NULL,NULL},第2个表达式返回行{1,1,101,101}、{2,NULL,NULL,101}:
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、t3和属性t2.b和t3.b的任何条件P,下面的表达式:
(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
认为是嵌套的。第1个查询结合左联接操作则形成嵌套的联接,而在第二个查询中结合内联接操作形成嵌套联接。
在第1个查询中,括号可以忽略:联接表达式的语法结构与联接操作的执行顺序相同。但对于第2个查询,括号不能省略,尽管如果没有括号,这里的联接表达式解释不清楚。(在外部扩展语法中,需要第2个查询的(t2,t3)的括号,尽管从理论上对查询分析时不需要括号:这些查询的语法结构将仍然不清楚,因为LEFT JOIN和ON将充当表达式(t2,t3)的左、右界定符的角色)。
前面的例子说明了这些点:
· 对于只包含内联接(而非外联接)的联接表达式,可以删除括号。你可以移除括号并从左到右评估(或实际上,你可以按任何顺序评估表)。
· 总的来说,对外联接却不是这样。去除括号可能会更改结果。
· 总的来说,对外联接和内联接的结合,也不是这样。去除括号可能会更改结果。
含嵌套外联接的查询按含内联接的查询的相同的管道方式执行。更确切地说,利用了嵌套环联接算法。让我们回忆嵌套环联接执行查询时采用什么算法。
假定我们有一个如下形式的表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组成的行”。
现在让我们考虑带嵌套的外联接的查询:
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;
}
}
}
}
对于有外联接的查询,优化器可以只选择这样的顺序:外表的环优先于内表的环。这样,对于有外联接的查询,只可能有一种嵌套顺序。在下面的查询中,优化器将评估两个不同的嵌套:
SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
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) {
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;
}
}
}
和
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;
}
}
}
在两个嵌套中,必须在外环中处理T1,因为它用于外联接中。T2和T3用于内联接中,因此联接必须在内环中处理。但是,因为该联接是一个内联接,T2和T3可以以任何顺序处理。
当讨论内联接嵌套环的算法时,我们忽略了部分详情,可能对查询执行的性能的影响会很大。我们没有提及所谓的“下推”条件。假定可以用连接公式表示我们的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))提取下推前提。在这种情况下,下推前提也受一个标志保护,防止检查由相应外联接操作所产生的NULL-补充的行的断言。
请注意如果从判断式的WHERE条件推导出,根据从一个内表到相同嵌套联接的另一个表的关键字进行的访问被禁止。(在这种情况下,我们可以使用有条件关键字访问,但是该技术还未用于MySQL 5.1中)。
8. MySQL如何简化外部联合
在许多情况下,一个查询的FROM子句的表的表达式可以简化。
在分析阶段,带右外联接操作的查询被转换为只包含左联接操作的等效查询。总的来说,根据以下原则进行转换:
(T1, ...) RIGHT JOIN (T2,...) ON P(T1,...,T2,...) =
(T2, ...) LEFT JOIN (T1,...) ON P(T1,...,T2,...)
所有T1 INNER JOIN T2 ON P(T1,T2)形式的内联接表达式被替换为T1,T2、P(T1,T2),并根据WHERE条件(或嵌入连接的联接条件,如果有)联接为一个连接。
当优化器为用外联接操作的联接查询评估方案时,它只考虑在访问内表之前访问外表的操作的方案。优化器选项受到限制,因为只有这样的方案允许我们用嵌套环机制执行带外联接操作的查询。
假定我们有一个下列形式的查询:
SELECT * T1 LEFT JOIN T2 ON P1(T1,T2)
WHERE P(T1,T2) AND R(T2)
R(T2)大大减少了表T2中匹配的行数。如果我们这样执行查询,优化器将不会有其它选择,只能在访问表T2之前访问表T1,从而导致执行方案非常低。
幸运的是,如果WHERE条件拒绝null,MySQL可以将此类查询转换为没有外联接操作的查询。如果为该操作构建的NULL补充的行评估为FALSE或UNKNOWN,则该条件称为对于某个外联接操作拒绝null。
因此,对于该外联接:
T1 LEFT JOIN T2 ON T1.A=T2.A
类似下面的条件为拒绝null:
T2.B IS NOT NULL,
T2.B > 3,
T2.C <= T1.C,
T2.B < 2 OR T2.C > 1
类似下面的条件不为拒绝null:
T2.B IS NULL,
T1.B < 3 OR T2.B IS NOT NULL,
T1.B < 3 OR T2.B > 3
检查一个外联接操作的条件是否拒绝null的总原则很简单。以下情况下为拒绝null的条件:
· 形式为A IS NOT NULL,其中A是任何内表的一个属性
· 包含内表引用的判断式,当某个参量为NULL时评估为UNKNOWN
· 包含用于连接的拒绝null的条件的联合
· 拒绝null的条件的逻辑和
一个条件可以对于一个查询中的一个外联接操作为拒绝null的而对于另一个不为拒绝null的。在下面的查询中:
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
LEFT JOIN T3 ON T3.B=T1.B
WHERE T3.C > 0
WHERE条件对于第2个外联接操作为拒绝null的但对于第1个不为拒绝null的。
如果WHERE条件对于一个查询中的一个外联接操作为拒绝null的,外联接操作被一个内联接操作代替。
例如,前面的查询被下面的查询代替:
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
INNER JOIN T3 ON T3.B=T1.B
WHERE T3.C > 0
对于原来的查询,优化器将评估只与一个访问顺序T1、T2、T3兼容的方案。在替换的查询中,还考虑了访问顺序T3、T1、T2。
一个外联接操作的转化可以触发另一个的转化。这样,查询:
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
LEFT JOIN T3 ON T3.B=T2.B
WHERE T3.C > 0
将首先转换为查询:
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
INNER JOIN T3 ON T3.B=T2.B
WHERE T3.C > 0
该查询等效于查询:
SELECT * FROM (T1 LEFT JOIN T2 ON T2.A=T1.A), T3
WHERE T3.C > 0 AND T3.B=T2.B
现在剩余的外联接操作也可以被一个内联接替换,因为条件T3.B=T2.B为拒绝null的,我们可以得到一个根本没有外联接的查询:
SELECT * FROM (T1 INNER JOIN T2 ON T2.A=T1.A), T3
WHERE T3.C > 0 AND T3.B=T2.B
有时我们可以成功替换嵌入的外联接操作,但不能转换嵌入的外联接。下面的查询:
SELECT * FROM T1 LEFT JOIN
(T2 LEFT JOIN T3 ON T3.B=T2.B)
ON T2.A=T1.A
WHERE T3.C > 0
被转换为:
SELECT * FROM T1 LEFT JOIN
(T2 INNER JOIN T3 ON T3.B=T2.B)
ON T2.A=T1.A
WHERE T3.C > 0,
只能重新写为仍然包含嵌入式外联接操作的形式:
SELECT * FROM T1 LEFT JOIN
(T2,T3)
ON (T2.A=T1.A AND T3.B=T2.B)
WHERE T3.C > 0。
如果试图转换一个查询中的嵌入式外联接操作,我们必须考虑嵌入式外联接的联接条件和WHERE条件。在下面的查询中:
SELECT * FROM T1 LEFT JOIN
(T2 LEFT JOIN T3 ON T3.B=T2.B)
ON T2.A=T1.A AND T3.C=T1.C
WHERE T3.D > 0 OR T1.D > 0
WHERE条件对于嵌入式外联接不为拒绝null的,但嵌入式外联接T2.A=T1.A AND T3.C=T1.C的联接条件为拒绝null的。因此该查询可以转换为:
SELECT * FROM T1 LEFT JOIN
(T2, T3)
ON T2.A=T1.A AND T3.C=T1.C AND T3.B=T2.B
WHERE T3.D > 0 OR T1.D > 0
9. MySQL如何优化ORDER BY
在某些情况中,MySQL可以使用一个索引来满足ORDER BY子句,而不需要额外的排序。
即使ORDER BY不确切匹配索引,只要WHERE子句中的所有未使用的索引部分和所有额外的ORDER BY 列为常数,就可以使用索引。下面的查询使用索引来解决ORDER BY部分:
SELECT * FROM t1
ORDER BY key_part1,key_part2,... ;
SELECT * FROM t1
WHERE key_part1=constant
ORDER BY key_part2;
SELECT * FROM t1
ORDER BY key_part1 DESC, key_part2 DESC;
SELECT * FROM t1
WHERE key_part1=1
ORDER BY key_part1 DESC, key_part2 DESC;
在某些情况下,MySQL不能使用索引来解决ORDER BY,尽管它仍然使用索引来找到匹配WHERE子句的行。这些情况包括:
· 对不同的关键字使用ORDER BY:
· SELECT * FROM t1 ORDER BY key1, key2;
· 对关键字的非连续元素使用ORDER BY:
· SELECT * FROM t1 WHERE key2=constant ORDER BY key_part2;
· 混合ASC和DESC:
· SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
· 用于查询行的关键字与ORDER BY中所使用的不相同:
· SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
· 你正联接许多表,并且ORDER BY中的列并不是全部来自第1个用于搜索行的非常量表。(这是EXPLAIN输出中的没有const联接类型的第1个表)。
· 有不同的ORDER BY和GROUP BY表达式。
· 使用的表索引的类型不能按顺序保存行。例如,对于HEAP表的HASH索引情况即如此。
通过EXPLAIN SELECT ...ORDER BY,可以检查MySQL是否可以使用索引来解决查询。如果Extra列内有Using filesort,则不能解决查询。参见7.2.1节,“EXPLAIN语法(获取关于SELECT的信息)``”。
文件排序优化不仅用于记录排序关键字和行的位置,并且还记录查询需要的列。这样可以避免两次读取行。文件排序算法的工作象这样:
\1. 读行匹配WHERE子句的行,如前面所示。
\2. 对于每个行,记录构成排序关键字和行位置的一系列值,并且记录查询需要的列。
\3. 根据排序关键字排序元组
\4. 按排序的顺序检索行,但直接从排序的元组读取需要的列,而不是再一次访问表。
该算法比以前版本的Mysql有很大的改进。
为了避免速度变慢,该优化只用于排序元组中的extra列的总大小不超过max_length_for_sort_data系统变量值的时候。(将该变量设置得太高的的迹象是将看到硬盘活动太频繁而CPU活动较低)。
如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试下面的策略:
· 增加sort_buffer_size变量的大小。
· 增加read_rnd_buffer_size变量的大小。
· 更改tmpdir指向具有大量空闲空间的专用文件系统。该选项接受几个使用round-robin(循环)模式的路径。在Unix中路径应用冒号(‘:’)区间开,在Windows、NetWare和OS/2中用分号(‘;’)。可以使用该特性将负载均分到几个目录中。注释:路径应为位于不同物理硬盘上的文件系统的目录,而不是同一硬盘的不同的分区。
默认情况下,MySQL排序所有GROUP BY col1,col2,...查询的方法如同在查询中指定ORDER BY col1,col2,...。如果显式包括一个包含相同的列的ORDER BY子句,MySQL可以毫不减速地对它进行优化,尽管仍然进行排序。如果查询包括GROUP BY但你想要避免排序结果的消耗,你可以指定ORDER BY NULL禁止排序。例如:
INSERT INTO foo
SELECT a, COUNT(*) FROM bar GROUP BY a ORDER BY NULL;
10. MySQL如何优化GROUP BY
满足GROUP BY子句的最一般的方法是扫描整个表并创建一个新的临时表,表中每个组的所有行应为连续的,然后使用该临时表来找到组并应用累积函数(如果有)。在某些情况中,MySQL能够做得更好,通过索引访问而不用创建临时表。
为GROUP BY使用索引的最重要的前提条件是 所有GROUP BY列引用同一索引的属性,并且索引按顺序保存其关键字(例如,这是B-树索引,而不是HASH索引)。是否用索引访问来代替临时表的使用还取决于在查询中使用了哪部分索引、为该部分指定的条件,以及选择的累积函数。
有两种方法通过索引访问执行GROUP BY查询,如下面的章节所描述。在第1个方法中,组合操作结合所有范围判断式使用(如果有)。第2个方法首先执行范围扫描,然后组合结果元组。
松散索引扫描
使用索引时最有效的途径是直接搜索组域。通过该访问方法,MySQL使用某些关键字排序的索引类型(例如,B-树)的属性。该属性允许使用 索引中的查找组而不需要考虑满足所有WHERE条件的索引中的所有关键字。既然该访问方法只考虑索引中的关键字的一小部分,它被称为松散索引扫描。如果没有WHERE子句, 松散索引扫描读取的关键字数量与组数量一样多,可以比所有关键字数小得多。 松散索引扫描查找满足范围条件的每个组的第1个关键字,并且再次读取尽可能最少数量的关键字。在下面的条件下是可以的:
· 查询针对一个单表。
· GROUP BY包括索引的第1个连续部分(如果对于GROUP BY,查询有一个DISTINCT子句,则所有显式属性指向索引开头)。
· 只使用累积函数(如果有)MIN()和MAX(),并且它们均指向相同的列。
· 索引的任何其它部分(除了那些来自查询中引用的GROUP BY)必须为常数(也就是说,必须按常量数量来引用它们),但MIN()或MAX() 函数的参数例外。
此类查询的EXPLAIN输出显示Extra列的Using indexforgroup-by。
下面的查询提供该类的几个例子,假定表t1(c1,c2,c3,c4)有一个索引idx(c1,c2,c3):
SELECT c1, c2 FROM t1 GROUP BY c1, c2;
SELECT DISTINCT c1, c2 FROM t1;
SELECT c1, MIN(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT MAX(c3), MIN(c3), c1, c2 FROM t1 WHERE c2 > const GROUP BY c1, c2;
SELECT c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;
由于上述原因,不能用该快速选择方法执行下面的查询:
SELECT c1, SUM(c2) FROM t1 GROUP BY c1;
\2. GROUP BY子句中的域不引用索引开头,如下所示:
SELECT c1,c2 FROM t1 GROUP BY c2, c3;
\3. 查询引用了GROUP BY部分后面的关键字的一部分,并且没有等于常量的等式,例如:
SELECT c1,c3 FROM t1 GROUP BY c1, c2;