结合MySQL中B+树的索引结构和Explain关键子给出的查询计划,我们就可以有针对性的对一些慢SQL进行优化,这篇博客就来总结一下优化思路。
先声明一下表结构:
CREATE TABLE `s1` (
`id` INT NOT NULL AUTO_INCREMENT,
`key1` VARCHAR(100),
`key2` VARCHAR(100),
`key3` VARCHAR(100),
`key_part1` VARCHAR(100),
`key_part2` VARCHAR(100),
`key_part3` VARCHAR(100),
`common_field` VARCHAR(100),
PRIMARY KEY (`id`),
KEY idx_key1 (`key1`),
UNIQUE KEY idx_key2 (`key2`),
KEY idx_key3 (`key3`),
KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)
) Engine=InnoDB CHARSET=utf8;
CREATE TABLE `s2` (
`id` INT NOT NULL AUTO_INCREMENT,
`key1` VARCHAR(100),
`key2` VARCHAR(100),
`key3` VARCHAR(100),
`key_part1` VARCHAR(100),
`key_part2` VARCHAR(100),
`key_part3` VARCHAR(100),
`common_field` VARCHAR(100),
PRIMARY KEY (`id`),
KEY idx_key1 (`key1`),
UNIQUE KEY idx_key2 (`key2`),
KEY idx_key3 (`key3`),
KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)
) Engine=InnoDB CHARSET=utf8;
两张表除了表名外结构一样,除了id
主键索引外,还为key1
和idx_key3
建了普通索引,为key2
建了唯一索引,为key_part1
和key_part2
和key_part3
建了联合索引。s1表中有1000条数据,s2表中有10000条数据。
常见的 type(访问类型)从最优到最差分别为:system > const > eq_ref > ref > ref_or_null > range > index > ALL
,一般来说,得保证查询达到 range 级别,最好达到 ref,如果没有达到就需要考虑优化了。
尽量在查询的时候,指定查询需要的字段,减少SELECT *的使用,在可能使用索引覆盖的场景下,不仅可以节省MySQL查询优化器对于是否回表的判断开销,还可以减少MySQL回表的次数,扫描索引树就能获得结果。比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb";
这种情况的优化方式一般是尽量让搜索条件能用上索引,注意联合索引尽量不要出现中间断开的场景,比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" AND key_part3 = "aa";
从 key 长度为 303 可以得知只用到了联合索引的第一个列。
模糊查询可以使用右模糊查询,尽量不要使用左模糊查询,比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 LIKE "bb%"
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 LIKE "%bb"
一般的分页语句:
EXPLAIN SELECT * FROM s2 LIMIT 9000, 5;
在表记录连续不会物理删除的情况下,即主键连续自增的情况下,可以这样优化:
EXPLAIN SELECT * FROM s2 WHERE id > 9000 LIMIT 5;
显然优化后的 SQL 走了索引,而且扫描的行数大大减少,执行效率更高。但是,优化后的 SQL 在很多场景并不适用,因为表中可能某些记录被删后,主键空缺,导致结果不一致。
所以这种优化得满足以下两个条件:
一般的分页排序语句:
EXPLAIN SELECT * FROM s2 ORDER BY key_part1 LIMIT 9000, 5;
可以看到并没有使用到联合索引,这是因为扫描整个索引树并查找到没索引的行(可能要遍历多个索引树)的成本比扫描全表的成本更高,所以优化器放弃使用索引。
优化思路是让排序时返回的字段尽可能少,可以让排序和分页操作先查出主键,然后根据主键查到对应的记录,SQL 改写如下:
EXPLAIN SELECT * FROM s2 s INNER JOIN (SELECT id FROM s2 ORDER BY key_part1 LIMIT 9000, 5) t ON t.id = s.id;
原 SQL 使用的是 filesort 排序,而优化后的 SQL 使用的是索引排序,且分页和排序使用到了联合索引。
MySQL支持两种方式的排序 filesort 和 index,Using index 是指 MySQL 扫描索引本身完成排序,filesort 是使用文件排序。index 效率高,filesort 效率低。
排序分组优化方式一般是尽量让排序和分组字段用上索引,比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1;
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" GROUP BY key_part1 ORDER BY NULL;
order by 满足两种情况会使用 Using index:
比如:
EXPLAIN SELECT * FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1, key_part3;
需要注意的点:
用于排序的联合索引中间断开的场景,比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1, key_part3;
出现倒序正序交替的场景,比如:
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" AND key_part2 = "aa" ORDER BY key_part1, key_part2;
此时都是正序排序,Extra 提示 Using index 扫描索引就可以完成排序,如果正序倒序交替,比如:
EXPLAIN SELECT * FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1 ASC, key_part3 DESC;
此时 Extra 提示 Using filesort 需要用到文件排序。
限定条件用在 having 和 where 的比较:
EXPLAIN SELECT key_part1, COUNT(id) num FROM s1 GROUP BY key_part2 HAVING key_part1 = "aa";
EXPLAIN SELECT key_part1, COUNT(id) num FROM s1 WHERE key_part1 = "aa" GROUP BY key_part2;
MySQL的表关联常见有两种算法:
1、 嵌套循环连接 Nested-Loop Join(NLJ) 算法
循环从驱动表中读取行数据,在这行数据中获取到关联字段,根据关联字段在被驱动表里取出满足条件的行,然后取出两张表的结果合集。
EXPLAIN SELECT * FROM s2 INNER JOIN s1 ON s1.key3 = s2.key3;
SQL的执行过程大致如下:
整个过程会读取 s1 表的所有数据(扫描1000行),然后遍历每行数据中字段 key3 的值,根据 s1 表中 key3 的值索引扫描 s2 表中的对应行(扫描1000次 s2 表的索引,1次扫描可以认为最终只扫描 s2 表一行完整数据,也就是总共 s2 表也扫描了1000 行)。因此整个过程扫描了 2000 行。
2、基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法
把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行读取出来跟 join_buffer 中的数据做对比。
如果被驱动表的关联字段没索引,MySQL会选择 Block Nested-Loop Join 算法。
EXPLAIN SELECT * FROM s2 INNER JOIN s1 ON s1.common_field = s2.common_field;
Extra 中的 Using join buffer (Block Nested Loop)说明该关联查询使用的是 BNL 算法。 上面SQL的执行过程大致如下:
整个过程对表 s1 和 s2 都做了一次全表扫描,因此扫描的总行数为11000行(表 s1 的数据总量 1000 + 表 s2 的数据总量10000)。并且 join_buffer 里的数据是无序的,因此对表 s2 中的每一行,都要做 1000 次判断,所以内存中的判断次数是 1000 * 10000= 1000 万次。
可以看到这种算法的扫描行数为两张表的记录总和,并且还有两张表数据量乘积的内存比较操作,但是相比于磁盘扫描,内存比较操作要快得多。
被驱动表的关联字段没索引为什么要选择使用 BNL 算法而不使用 NLJ 呢?
如果第二条SQL使用 NLJ 算法,由于没有索引,所以只能遍历扫描全表,那么磁盘扫描表数据就达到了1000 * 10000= 1000 万次,而用 BNL 算法的磁盘扫描行数会少很多,相比于磁盘扫描,BNL 算法的内存计算会快得多。
因此MySQL对于被驱动表的关联字段没有索引的关联查询,一般为了减少磁盘扫描的 IO 开销,会倾向于使用 BNL 算法。如果有索引一般选择 NLJ 算法,有索引的情况下 NLJ 算法比 BNL 算法性能更高。
对于关联查询的优化:
straight_join 功能同 join 类似,但能指定让左边的表来驱动右边的表,能修改表优化器对于联表查询的执行顺序。
比如可以用 NLJ 算法查询的SQL语句,使用了 straight_join 后换成了 BNL 算法:
EXPLAIN SELECT * FROM s2 STRAIGHT_JOIN s1 ON s1.common_field = s2.key1;
强制用 s2 表作为驱动表后,原来能用 NLJ 算法现在MySQL使用了 BNL 算法来执行查询。使用的时候注意两点: