目录
2.Simple Nested-Loop Join(简单嵌套循环连接)
3.Index Nested-Loop Join(索引嵌套循环连接)
4.Block Nested-Loop Join(块嵌套循环连接)
一.关联查询优化(多表查询)
1.1数据准备
#分类
CREATE TABLE IF NOT EXISTS `type` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
#图书
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
#向分类表中添加20条记录
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
#向图书表中添加20条记录
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
1.2采用左外连接
# 情况1:左外连接
#连接的时候就和“嵌套循环”一样
#每次从驱动表里选取一条记录去被驱动表里整个遍历一遍
#将符合连接条件的放到结果集中
#驱动表和被驱动表-->EXPLAIN执行结果的记录中,上面的是驱动表,下面的是被驱动表
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
结论:type 有All
添加索引优化
#添加索引优化
#给被驱动表加了索引可以避免全表扫描
ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
可以看到第二行的 type 变为了 ref,rows 也变成了1,优化比较明显。这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引 。
1.3采用内连接
drop index Y on book;#(如果已经删除了可以不用再执行该操作)
LEFT JOIN 换成 INNER JOIN(MySQL自动选择驱动表)
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
#添加索引
ALTER TABLE book ADD INDEX Y ( card);
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
ALTER TABLE type ADD INDEX X (card);
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
#如果发现type和book交换了 是因为INNER JOIN 的两表地位一样,选取cost小的执行
#结论:对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的
结论:
1.对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的
2.对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现
3.对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”
1.4join语句原理
1.驱动表和被驱动表
驱动表就是主表,被驱动表就是从表、非驱动表
查看哪个是驱动表、哪个是被驱动表:EXPLAIN执行结果的记录中,上面的是驱动表,下面的是被驱动表
对于内连接来说:
SELECT * FROM A JOIN B ON ...
A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。通过explain关键字可以查看
对于外连接来说:
SELECT * FROM A LEFT JOIN B ON ...
#或者
SELECT * FROM B RIGHT JOIN A ON ...
通常,大家会认为A就是驱动表,B就是被驱动表,但也未必。测试如下:
#JOIN 底层原理
CREATE TABLE a(f1 INT,f2 INT,INDEX(f1))ENGINE=INNODB;
CREATE TABLE b(f1 INT,f2 INT)ENGINE=INNODB;
INSERT INTO a VALUES(1,1),(2,2),(3,3 ),(4,4),(5,5),(6,6);
INSERT INTO b VALUES(3,3 ),(4,4),(5,5),(6,6),(7,7),(8,8);
SELECT * FROM b;
#测试1
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) WHERE (a. f2=b.f2);
# 优化成内连接了 b驱动表 a被驱动表
#测试3
EXPLAIN SELECT * FROM a JOIN b ON(a.f1=b.f1) WHERE (a. f2=b.f2);
#测试2
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) AND (a.f2=b.f2);
# a驱动表 b被驱动表
2.Simple Nested-Loop Join(简单嵌套循环连接)
算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:
可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据1000条计算,则A*B=10万次。开销统计如下:
开销统计 | SNLJ |
外表扫描次数: | 1 |
内表扫描次数: | A |
读取记录数: | A+B*A |
JOIN比较次数: | B*A |
回表读取记录次数: | 0 |
当然mysql肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join 优化算法
3.Index Nested-Loop Join(索引嵌套循环连接)
Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数
,所以要求被驱动表上必须有索引
才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。
开销统计 | SNLJ | INLJ |
外表扫描次数: | 1 | 1 |
内表扫描次数: | A | 0–>直接用索引 |
读取记录数: | A+B*A | A+B(match)–>A+B匹配的数目 |
JOIN比较次数: | B*A | A*Index(Height) |
回表读取记录次数: | 0 | B(match)(if possible) |
如果被驱动表加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。
4.Block Nested-Loop Join(块嵌套循环连接)
如果存在索引,那么会使用index的方式进行join,如果join时被驱动表的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率
注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让join buffer中可以存放更多的列。
开销统计 | SNLJ | INLJ | BNLJ |
外表扫描次数: | 1 | 1 | 1 |
内表扫描次数: | A | 0 | A* used_column_size / join_buffer_size+1 |
读取记录数: | A+B*A | A+B(match) | A+B*(A * used_column_size /join_buffer_size) |
JOIN比较次数: | B*A | A *Index(Height) | B*A |
回表读取记录次数: | 0 | B(match)(if possible) | 0 |
参数设置
block_nested_loop
通过show variables like '%optimizer_switch%'查看block_nested_loop状态。默认是开启的。
join_buffer_size
驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k 。
show variables like '%join_buffer%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| join_buffer_size | 262144 |
+------------------+--------+
1 row in set (0.00 sec)
5.小结
1、整体效率比较: INLJ > BNLJ > SNLJ
2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数目) (小的度量单位指的是表行数*每行大小)
3、为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)
4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)
5、减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)
6、保证被驱动表的J0IN字段已经创建了索引
7、需要JOIN的字段,数据类型保持绝对一致
8、LEFT JOlN时,选择小表作为驱动表,大表作为被驱动表。减少外层循环的次数。INNER JOIN时,MySQL会自动将小结果集的表选为驱动表。选择相信MySQL优化策略
9、能够直接多表关联的尽量直接关联,不用子查询(减少查询的趟数)
10、不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用JOIN来代替子查询
11、衍生表建不了索引
6.Hash Join
从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join默认都会使用hash join
Nested Loop:
对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。
Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行
这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。
在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高IO的性能。
它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1=B.COL2),这是由Hash的特点决定的
类别 | Nested Loop | Hash Join |
使用条件 | 任何条件 | 等值连接(=) |
相关资源 | CPU、磁盘IO | 内存、临时空间 |
特点 | 当有高选择性索引或进行限制性搜索时效率比较高能能够快速返回第一次的搜索结果 | 当缺乏索引或索引条件模糊时,Hash Join比Nested于率比较高,Loop有效。在数据仓库环境下,如果表的纪录数多,效率高 |
缺点 | 当索引丢失或者查询条件限制不够时,效率很低。当表的纪录数多时,效率低 | 为建立哈希表,需要大量内存。第一次的结果返回较慢 |
二.子查询优化
使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。
子查询是MySQL的一项重要的功能,可以通过一个SQL语句实现比较复杂的查询。但是,子查询的执行效率不高。原因:
① 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询
② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响
③ 对于返回结果集比较大的子查询。其对查询性能的影响也就越大
在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。
举例1:查询学生表中是班长的学生信息
- 使用子查询
- 推荐使用多表查询
#4. 子查询的优化
#创建班级表中班长的索引
CREATE INDEX idx_monitor ON class(monitor);
#子查询
#查询班长的信息
EXPLAIN SELECT * FROM student stu1
WHERE stu1.`stuno` IN (
SELECT monitor
FROM class c
WHERE monitor IS NOT NULL
);
#推荐使用多表查询
EXPLAIN SELECT stu1.* FROM student stu1 JOIN class c
ON stu1.`stuno` = c.`monitor`
WHERE c.`monitor` IS NOT NULL;
/*
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+-----------------------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+-----------------------+--------+----------+--------------------------+
| 1 | SIMPLE | stu1 | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | NULL |
| 1 | SIMPLE | <subquery2> | NULL | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 5 | atguigudb2.stu1.stuno | 1 | 100.00 | NULL |
| 2 | MATERIALIZED | c | NULL | index | idx_monitor | idx_monitor | 5 | NULL | 9952 | 100.00 | Using where; Using index |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+-----------------------+--------+----------+--------------------------+
3 rows in set, 1 warning (0.09 sec)
*/
#查询不为班长的学生信息
#方式一
EXPLAIN SELECT SQL_NO_CACHE a.*
FROM student a
WHERE a.stuno NOT IN (
SELECT monitor FROM class b
WHERE monitor IS NOT NULL)
/*
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | Using where |
| 2 | SUBQUERY | b | NULL | index | idx_monitor | idx_monitor | 5 | NULL | 9952 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+--------+----------+--------------------------+
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | NULL |
| 1 | SIMPLE | b | NULL | ref | idx_monitor | idx_monitor | 5 | atguigudb2.a.stuno | 1 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
2 rows in set, 2 warnings (0.09 sec)
*/
#方式二
EXPLAIN SELECT SQL_NO_CACHE a.*
FROM student a LEFT OUTER JOIN class b
ON a.stuno =b.monitor
WHERE b.monitor IS NULL;
/*
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | NULL |
| 1 | SIMPLE | b | NULL | ref | idx_monitor | idx_monitor | 5 | atguigudb2.a.stuno | 1 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+------+---------------+-------------+---------+--------------------+--------+----------+--------------------------+
2 rows in set, 2 warnings (0.02 sec)
*/
结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
三.排序优化
3.1排序优化
问题:在WHERE条件字段上加索引,但是为什么在ORDER BY字段上还要加索引呢?
回答:
在MySQL中,支持两种排序方式,分别是FileSort
和Index
排序。
Index排序中,索引可以保证数据的有序性,不需要再进行排序,`效率更高
FileSort 排序则一般在内存中进行排序,占用CPU 较多。如果待排结果较大,会产生临时文件I/O到磁盘进行排序的情况,效率校低
Using filesort: 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buiffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫FileSot 排序。
using index: 通过有序索引顺序扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高
优化建议:
1.SQL中,可以在WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中避免全表扫描,在ORDER EY子句避免使用 FileSort排序。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,还是要避免,以提高查询效率。
2.尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引
3.无法使用lndex时,需要对FileSort方式进行调优
3.2测试
#5. 排序优化
#删除student和class表中的非主键索引
CALL proc_drop_index('atguigudb2','student');
CALL proc_drop_index('atguigudb2','class');
SHOW INDEX FROM student;
SHOW INDEX FROM class;
#过程一:
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
*/
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
*/
#过程二:order by时不limit,索引失效
#创建索引
CREATE INDEX idx_age_classid_name ON student (age,classid,NAME);
#不限制,索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+----------------+
*/
#EXPLAIN SELECT SQL_NO_CACHE age,classid,name,id FROM student ORDER BY age,classid;
#不回表,覆盖索引 Using Index
#增加limit过滤条件,使用上索引了。
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
/*
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+-------+
| 1 | SIMPLE | student | NULL | index | NULL | idx_age_classid_name | 73 | NULL | 10 | 100.00 | NULL |
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+-------+
*/
#过程三:order by时顺序错误,索引失效
#创建索引age,classid,stuno
CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
#以下哪些索引失效?
EXPLAIN SELECT * FROM student ORDER BY classid LIMIT 10;#失效
EXPLAIN SELECT * FROM student ORDER BY classid,NAME LIMIT 10; #失效
EXPLAIN SELECT * FROM student ORDER BY age,classid,stuno LIMIT 10;#使用索引,使用了三个字段
EXPLAIN SELECT * FROM student ORDER BY age,classid LIMIT 10;##使用索引,使用了三个字段
EXPLAIN SELECT * FROM student ORDER BY age LIMIT 10;#使用索引,使用了三个字段
#过程四:order by时规则不一致, 索引失效 (顺序错,不索引;方向反,不索引)
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid ASC LIMIT 10;#失效,没用使用索引
EXPLAIN SELECT * FROM student ORDER BY classid DESC, NAME DESC LIMIT 10;#失效,没用使用索引
EXPLAIN SELECT * FROM student ORDER BY age ASC,classid DESC LIMIT 10; #失效,没用使用索引
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid DESC LIMIT 10;#使用了索引 Backward index scan
/*
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+---------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+---------------------+
| 1 | SIMPLE | student | NULL | index | NULL | idx_age_classid_name | 73 | NULL | 10 | 100.00 | Backward index scan |
+----+-------------+---------+------------+-------+---------------+----------------------+---------+------+------+----------+---------------------+
1 row in set, 1 warning (0.01 sec)
*/
#过程五:无过滤,不索引
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid;#使用了索引,仅age字段
/*
+----+-------------+---------+------------+------+--------------------------------------------+-----------------------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+--------------------------------------------+-----------------------+---------+-------+-------+----------+-------+
| 1 | SIMPLE | student | NULL | ref | idx_age_classid_name,idx_age_classid_stuno | idx_age_classid_stuno | 5 | const | 19184 | 100.00 | NULL |
+----+-------------+---------+------------+------+--------------------------------------------+-----------------------+---------+-------+-------+----------+-------+
*/
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid,NAME; #使用了索引,仅age字段
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age; #没使用了索引
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age LIMIT 10;#使用了索引,用了所有字段
CREATE INDEX idx_cid ON student(classid);
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age;#使用了索引idx_cid
小结
INDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
-
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c
-
不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/
3.3 案例
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序
执行案例前先清除student上的索引,只留主键:
# 实战:测试filesort和index排序
DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid_stuno ON student;
DROP INDEX idx_age_classid_name ON student;
#或者
call proc_drop_index('atguigudb2','student');
场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#0.121
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 3.33 | Using where; Using filesort |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
*/
结论:type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的
优化思路:
方案一: 为了去掉filesort我们可以把索引建成
#创建新索引
CREATE INDEX idx_age_name ON student(age,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#0.039
/*
+----+-------------+---------+------------+-------+---------------------------------+--------------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------------------------+--------------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | student | NULL | range | idx_age_stuno_name,idx_age_name | idx_age_stuno_name | 9 | NULL | 20 | 100.00 | Using index condition; Using filesort |
+----+-------------+---------+------------+-------+---------------------------------+--------------------+---------+------+------+----------+---------------------------------------+
*/
方案二: 尽量让where的过滤条件和排序使用上索引
建一个三个字段的组合索引:
DROP INDEX idx_age_name ON student;
CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#时间更短了 0.002
/*
+----+-------------+---------+------------+-------+--------------------+--------------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+--------------------+--------------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | student | NULL | range | idx_age_stuno_name | idx_age_stuno_name | 9 | NULL | 20 | 100.00 | Using index condition; Using filesort |
+----+-------------+---------+------------+-------+--------------------+--------------------+---------+
*/
结果竟然有filesort的sql运行速度,超过了已经优化掉filesort的 sql,而且快了很多,几乎一瞬间就出现了结果。
原因:
所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。相对的stuno<101000这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择。
结论:
两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的 。
当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
3.4filesort算法:双路排序和单路排序
排序的字段如果不在索引列上,则filesort会有两种算法:双路排序和单路排序
双路排序 (慢)
MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by列对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序
单路排序 (快)
从磁盘读取查询需要的 所有列
,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。
结论及引申出的问题
- 由于单路是后出的,总体而言好过双路
- 但是用单路有问题
- 在sort_buffer中,单路比多路要多占用很多空间,因为单路是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排…从而多次I/O。
单路本来想省一次l/o操作,反而导致了大量的I/O操作,反而得不偿失。
order by 优化:
- 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则
- 尽量使用覆盖索引
多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC) - 如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)
优化策略
1.尝试提高 sort_buffer_size
- 不管用哪种算法。提高这个参数都会提高效率,要根据系统的能力去提高,因为这个参数是针对每个进程(connection)的1M-8M之间调整。MySQL5.7,InnoDB存储引擎默认值是1048576字节,1MB
SHOW VARIABLES LIKE '%sort_buffer_size%';
/*
+-------------------------+---------+
| Variable_name | Value |
+-------------------------+---------+
| innodb_sort_buffer_size | 1048576 |
| myisam_sort_buffer_size | 8388608 |
| sort_buffer_size | 262144 |
+-------------------------+---------+
*/
2.尝试提高 max_length_for_sort_data
- 提高这个参数,会增加用改进算法的概率
SHOW VARIABLES LIKE '%max_length_for_sort_data%';#默认1024字节
/*
SHOW VARIABLES LIKE '%max_length_for_sort_data%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| max_length_for_sort_data | 4096 |
+--------------------------+-------+
*/
- 但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort_data,使用双路算法,否则使用单路算法。1024-8192字节之间调整
3.Order by 时select * 是一个大忌。最好只Query需要的字段
当Query的字段大小总和小于max_length_for_sort_data,而且排序字段不是TEXT|BLOB类型时,会用改进后的算法―-单路排序,否则用老算法――多路排序
两种算法的数据都有可能超出sort_buffer_size的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size。
四.GROUP BY优化
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
- group by 先排序再分组,遵照索引建的最佳左前缀法则
- 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
- where效率高于having,能写在where限定的条件就不要写在having中了
- 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
- 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢
五.分页查询优化(limit优化)
一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是limit 2000000,10,此时需要MySQL排序前2000010记录,仅仅返回2000000-2000010的记录,其他记录丢弃,查询排序的代价非常大。
在大数据量的分页查询时,limit后的起始位置越靠后,耗时越长
EXPLAIN select * from student limit 2000000,10;
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | NULL |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------+
*/
优化思路一
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;
/*
+----+-------------+------------+------------+--------+---------------+---------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+---------+---------+------+--------+----------+-------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 100.00 | NULL |
| 1 | PRIMARY | t | NULL | eq_ref | PRIMARY | PRIMARY | 4 | a.id | 1 | 100.00 | NULL |
| 2 | DERIVED | student | NULL | index | NULL | PRIMARY | 4 | NULL | 498858 | 100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+---------+---------+------+--------+----------+-------------+
*/
优化思路二
该方案适用于主键自增的表,可以把Limit查询转换成某个位置的查询
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
/*
+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | student | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 1 | 100.00 | Using where |
+----+-------------+---------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
*/
limit优化思路:通过覆盖索引+子查询的方式来优化
六.优先考虑覆盖索引
尽量使用覆盖索引(覆盖索引:查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到),减少使用selecl *.
1.什么是覆盖索引?
覆盖索引: SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据理解方式一: 索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。
理解方式二: 非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键
包含 SELECT 到 FROM之间查询的列
。
覆盖索引演示
#6. 覆盖索引
#删除之前的索引
#举例1:
DROP INDEX idx_age_stuno ON student;
CREATE INDEX idx_age_name ON student (age,NAME);
EXPLAIN SELECT * FROM student WHERE age <> 20;
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | student | NULL | ALL | idx_age_name | NULL | NULL | NULL | 498858 | 100.00 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
*/
EXPLAIN SELECT age,NAME FROM student WHERE age <> 20;
/*使用了索引,打破了前面说的“不等于”的查询索引会失效的原则
原因:查询优化器发现使用索引时,不会回表,开销更小,故使用了索引
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
| 1 | SIMPLE | student | NULL | index | idx_age_name | idx_age_name | 68 | NULL | 498858 | 100.00 | Using where; Using index |
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
*/
#举例2:
EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc';
/*
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | student | NULL | ALL | NULL | NULL | NULL | NULL | 498858 | 11.11 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
*/
EXPLAIN SELECT id,age FROM student WHERE NAME LIKE '%abc';
/*
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
| 1 | SIMPLE | student | NULL | index | NULL | idx_age_name | 68 | NULL | 498858 | 11.11 | Using where; Using index |
+----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+
*/
2 覆盖索引的利弊
好处:
1. 避免Innodb表进行索引的二次查询(回表)
lnnodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据,在查找到相应的键值后,还需通过主键进行二次查询才能获取我们真实所需要的数据。
在覆盖索引中,二级索引的键值中可以获取所要的据,避免了对主键的二次查询,减少了IO操作,提升了查询效率。
2. 可以把随机IO变成顺序IO加快查询效率
由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO
转变成索引查找的顺序IO
。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段
弊端:
索引字段的维护
总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。
七.如何给字符串添加索引
有一张教师表,表定义如下:
create table teacher(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
教师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:
select col1, col2 from teacher where email='xxx';
如果email这个字段上没有索引,那么这个语句就只能做 全表扫描 。
1.前缀索引
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
alter table teacher add index index1(email);
#或
alter table teacher add index index2(email(6))
这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图:
如果使用的是index1(即email整个字符串的索引结构),执行顺序是这样的:
1.从index1索引树找到满足索引值是’ zhangssxyz@xxx.com ’的这条记录,取得ID2的值;
2.到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;
3.取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=’ zhangssxyz@xxx.com ’的条件了,循环结束。这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行
如果使用的是index2(即email(6)索引结构),执行顺序是这样的:
1.从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;
2.到主键上查到主键值是ID1的行,判断出email的值不是’ zhangssxyz@xxx.com ’,这行记录丢弃;
3.取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然后判断,这次值对了,将这行记录加入结果集;
4.重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少
2.前缀索引对覆盖索引的影响
结论:
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。