5.4 filesort算法:双路排序和单路排序
排序的字段若如果不在索引列上,则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。
6.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会很慢
7.分页查询优化(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优化思路:通过覆盖索引+子查询的方式来优化
8.优先考虑覆盖索引
尽量使用覆盖索引(覆盖索引:查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到),减少使用selecl *.
8.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 | +----+-------------+---------+------------+-------+---------------+--------------+---------+------+--------+----------+--------------------------+ */
8.2 覆盖索引的利弊
好处:
1. 避免Innodb表进行索引的二次查询(回表)
lnnodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据,在查找到相应的键值后,还需通过主键进行二次查询才能获取我们真实所需要的数据。
在覆盖索引中,二级索引的键值中可以获取所要的据,避免了对主键的二次查询,减少了IO操作,提升了查询效率。
2. 可以把随机IO变成顺序IO加快查询效率
由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段
弊端:
索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作
9. 如何给字符串添加索引
有一张教师表,表定义如下:
create table teacher( ID bigint unsigned primary key, email varchar(64), ... )engine=innodb;
讲师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:
select col1, col2 from teacher where email='xxx';
如果email这个字段上没有索引,那么这个语句就只能做 全表扫描 。
9.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’时,循环结束。
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少
9.2 前缀索引对覆盖索引的影响
结论:
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
9.3 拓展内容
从查询效率上看,使用hash字段方式的查询性能相对更稳定一些。因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
10. 索引下推
10.1 使用前后的对比
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。
ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE后面的条件是否保留行
启用ICP后,如果部分WHERE 条件可以仅使用索引中的列进行筛选,则
MySQL服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。
好处:ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数
但是:ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例
10.2 ICP的开启/关闭
默认情况下启用索引条件下推。可以通过设置系统变量optimizer_switch 控制:index_condition_pushdown
#关闭索引下推 SET optimizer_switch = 'index_condition_pushdown=off' ; #打开索引下推 SET optimizer_switch = 'index_condition_pushdown=on' ;
- 当使用索引条件下推时,
EXPLAIN语句输出结果中Extra列内容显示为Using index condition。
SET optimizer_switch = 'index_condition_pushdown=off'; #Using where EXPLAIN SELECT * FROM people WHERE zipcode= '000001' AND lastname LIKE '%张%' AND address LIKE '%北京市%'; SET optimizer_switch = 'index_condition_pushdown=on' ; #Using index condition; Using where
10.3 ICP使用案例
#7.索引条件下推(ICP) #举例一: USE atguigudb1; #Using index condition #先key1 > 'z' 再 key1 LIKE '%a' 最后回表 #优于 先key1 > 'z' 再 回表 最后 key1 LIKE '%a' EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
建表
CREATE TABLE `people`( `id ` int NOT NULL AUTO_INCREMENT, `zipcode` varchar (20)COLLATE utf8_bin DEFAULT NULL, `firstname` varchar(20)COLLATE utf8_bin DEFAULT NULL, `lastname` varchar(2) COLLATE utf8_bin DEFAULT NULL, `address` varchar(50) cOLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`), KEY `zip_last_first`( `zipcode ', 'lastname ` , `firstname `) )ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
插入数据
INSERT INTO `people` VALUES INSERT INTO `people` VALUES ( '1', '000001','三','张','北京市'), ( '2', '000002','四','李','南京市'), ( '3', '000003','五','王','上海市'), ( '4', '000001','六','赵','天津市');
为该表定义联合索引zip_last_first (zipcode,lastname,firstname)。如果我们知道了一个人的邮编,但是不确定这个人的姓氏,我们可以进行如下检索:
SELECT * FROM people WHERE zipcode= '008881' AND lastname LIKE '%张%' AND address LIKE '%北京市%'; #Using index condition; Using where
执行查看SQL的查询计划,Extra中显示了Using index condition,这表示使用了索引下推。另外,Using where表示条件中包含需要过滤的非索引列的数据,即address LIKE '%北京市%'这个条件并不是索引列,需要在服务端过滤掉。
如果不想出现Using where,把address LlKE’%北京市%'去掉即可
EXPLAIN SELECT * FROM people WHERE zipcode= '000001' AND lastname LIKE '%张%'; #Using index condition
这个表中存在两个索引,分别是:
- 主键索引(简图)
二级索引zip_last_first(简图,这里省略了数据页等信息)
下面我们关闭ICP查看执行计划
mysql> SET optimizer_switch = 'index_condition_pushdown=off '; Query oK, rows affected (0.02秒)
查看执行计划,已经没有了Using index condition,表示没有使ICP
mysql> explain SELECT * FROM people WHERE zipcode='080801 ' AND lastname LIKE'%张%'AND address LIKE '%北京市%';
10.4 开启和关闭ICP的性能对比
创建存储过程,主要目的就是插入很多0000001的数据,这样查询的时候为了在存储引擎层做过滤,减少IO,也为了减少缓冲池(缓存数据页,没有lO)的作用。
#创建存储过程,向people表中添加1000000条数据,测试ICP开启和关闭状态下的性能 DELIMITER // CREATE PROCEDURE insert_people( max_num INT ) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO people ( zipcode , firstname, lastname, address ) VALUES ( '000001', '六','赵','天津市'); UNTIL i = max_num END REPEAT; COMMIT; END // DELIMITER ;
调用存储过程
call insert_people(1000000);
SELECT COUNT(*) FROM people;#1000004
首先打开profiling。
set profiling=1;
执行sQL语句,此时默认打开索引下推。
SELECT * FROM people WHERE zipcode='000001' AND lastname LIKE '%张%';
再次执行sQL语句,不使用索引下推
SELECT /*+ no_icp (people)*/ * FROM people WHERE zipcode='000001' AND lastname LIKE '%张%';
查看当前会话所产生的所有profiles
show profiles \G;
结果如下。
这个是SET optimizer_switch = 'index_condition_pushdown=off' 条件下执行的;
多次测试效率对比来看,使用ICP优化的查询效率会好一些。这里建议多存储一些数据效果更明显。
10.5 ICP的使用条件
1.如果表访问的类型为rapge、ref、eq_ref和ref_or_null可以使用ICP
2.ICP可以用于InnoDB和MyISAM表,包括分区表InnoDB和MyISAM表
3.对于InnoDB表,ICP仅用于二级索引。ICP的目标是减少全行读取次数,从而减少I/o操作。
4.当sQL使用覆盖索引时,不支持lCP。因为这种情况下使用ICP不会减少I/O。
5.相关子查询的条件不能使用ICP
ICP的使用条件:
① 只能用于二级索引(secondary index)
② explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法
11. 普通索引 vs 唯一索引
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引,假设字段 k 上的值都不重复。
这个表的建表语句是:
create table test( id int primary key, k int not null, name varchar(16), index (k) )engine=InnoDB;
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6)
11.1 查询过程
假设,执行查询的语句是 select id from test where k=5。
- 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录
- 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索
那么,这个不同带来的性能差距会有多少呢?答案是, 微乎其微
11.2 更新过程
为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下change buffer。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中 ,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性
将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。除了 访问这个数据页 会触发merge外,系统有后台线程会定期merge。在 数据库正常关闭(shutdown) 的过程中,也会执行merge操作。
如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存 ,提高内存利用率。
唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用
如果要在这张表中插入一个新记录(4,400)的话,InnoDB的处理流程是怎样的?








