1.mysql索引调优
1.1 讲解
在进行优化讲解之前,先请大家记住不要听信你看到的关于优化的“绝对真理”,而应该是在实际的业务场景下通过测试来验证你关于执行计划以及响应时间的假设。
1.2 优化方向
从上图中可以看出,我们把数据库优化分为四个纬度:硬件,系统配置,数据库表结构,SQL及索引
**硬件: ** CPU、内存、存储、网络设备等
系统配置: 服务器系统、数据库服务参数等
数据库表结构: 高可用、分库分表、读写分离、存储引擎、表设计等
Sql及索引: sql语句、索引使用等
- 从优化成本进行考虑:硬件>系统配置>数据库表结构>SQL及索引
- 从优化效果进行考虑:硬件<系统配置<数据库表结构<SQL及索引
本文就从优化成本最低,效果最好的SQL及索引来讲解
1.3 数据页
B+Tree是为磁盘等外存储设备设计的一种平衡查找树,InnoDB存储引擎就是用B+Tree实现其索引结构
在了解B+Tree结构之前先解了这2个概念
**磁盘:**系统从磁盘读取数据到内存是以磁盘块(block)为基本单位的,位于同一磁盘块种的数据会被一次性读取出来。
数据页:InnoDB是使用页来作为管理存储空间的基本单位,InnoDB在把磁盘数据读入到内存时也是以页为基本单位,InnoDB存储引擎中默认每个页的大小为16KB,可以修改为4K、8K、16K。
1.3.1数据页结构
一个数据页新建,到插入数据的过程
当Free Space
全部用完后,还有新的记录插入则申请新页面。
**User Records:**一条记录的结构如图所示
名称 | 大小(bit) | 描述 |
---|---|---|
预留位 |
1 | 先不用 |
delete_mask |
1 | 标记该记录是否删除 |
min_rec_mask |
1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 | 表示当前记录拥有的记录数 |
heap_no |
13 | 表示当前记录在记录堆的位置信息 |
record_type |
3 | 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶节点记录,2 表示最小记录,3 表示最大记录 |
next_record |
16 | 表示下一条记录的相对位置 |
Page Directory(页目录):是对多个User Records进行管理的一个目录
- 将所有的正常记录(Infimum键值最小记录及Supermum键值最大记录)进行分组
- 每个组最后一条记录(组内键值最大的那条记录)的头信息中n_owned该记录所在的组共有几条记录
- 每个组最后一条记录的地址偏移量按顺序存储在一个目录,这个目录就是Page Directory,目录中的这些地址偏移量被称为
槽
(slot)
Infimum + Supermum + User Records + Page Directory结构图如下
用上图数据模拟一个数据页中,数据查找过程:(二分法查找slot,再遍历slot对)
-
计算中间槽的位置:(0 + 3) / 2 = 1,查看slot1对应的主键值是4,因为主键4小于主键6。
设low = 1,high = 3不变;
-
重新计算中间槽位置:(1 + 3)/ 2 = 2,查看slot2对应的之间值是8,因为主键8大于主键6
low = 1不变,设high = 2
-
因为high - low = 1, 所以确定主键6记录再slot2位置。通过slot1找到该组最大主键4,该记录的next_recode记录了slot2主键值5的地址偏移量,遍历slot2对应的组,找到主键6的记录。
1.4 B+Tree数据结构
各个数据页可以组成一个双向链表
,而每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表
,每个数据页都会为存储在它里边儿的记录生成一个页目录(Page Directory)
,在通过主键查找某条记录的时候可以在页目录
中使用二分法快速定位到对应的槽,再遍历该槽对应分组中的记录即可快速找到指定的记录。
简略图:
1.4.1 无索引查找
- 以主键为搜索条件,假设要找记录为2的数据,在一个数据页中,可以在Page Directory中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。但是假如有1000个数据页,要找记录10000的在page500,那么就得500次IO加载到page0到page500才能拿到数据。 (解决方案:主索引)
- 以非主键为搜索条件,因为在数据页中并没有对非主键列建立所谓的Page Directory,所以我们无法通过二分法快速定位相应的
槽
。这种情况下只能从最小记录
开始依次遍历单链表中的每条记录,然后对比每条记录是不是符合搜索条件。很显然,这种查找的效率是非常低的。 (解决方案:辅助索引)
1.4.2 主索引
**主索引:**键值是主键id,data是一行数据
例sql:select * from table where id = 20;
查找过程:
1)读取根节点page0,将数据从磁盘加载到内存中,根据二分查找槽及遍历组。找到p1.
2)读取page1,加载到内存中,根据二分查找槽及遍历组,找到p5.
- 读取page5,加载到内存中,根据二分查找槽及遍历组,找到key = 20的记录。
mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,对于高度为3的b+tree ,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。
2.一个数据页默认16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16B * 1024 / (8B+8B)=1024个键值。
深度为3的B+tree主索引:1024x1024x100约等于1亿条数据
深度为3的B+tree辅助索引:1024x1024x1024约等于10亿条数据
1.4.3 辅助索引
**辅助索引:**键值是非主键字段,data是行数据的主键id
例sql:select id from table where key= 4;
- 读取根节点page0,将数据从磁盘加载到内存中,根据二分查找槽及遍历组。找到p1.
- 读取page1,加载到内存中,在页目录(page directory)通过二分查找法找出槽及遍历组,找到p4.但由于键值没有唯一约束,所以key4可能存在多个数据页中,又因为1<4<20,所以具体数据存在p3和p4中
- 读取page3,page4记载到内存,基于同上的查找规则找出key=4的记录。
为什么辅助索引的记录数据是主键id呢?
1.数据页大小有限制,当data数据过大,会导致一个数据页存储的key值数量小,也就意味着要查找等量的数据,要加载的数据页也更多,IO次数越多。
2.如果也记录行数据,相当于每建立一棵B+
树都需要把所有的用户记录再都拷贝一遍,太浪费存储空间了。
1.5 MySql优化实战
1.5.1 limit关键字优化
现有user表 有500w条数据。有个功能是最简单分页查询,sql如下:
select * from user where age > 45 limit page, size;
当page越大的时候,这条sql查询越慢,比如当page= 3000000,size = 10. 这条sql已经是秒级了,你有办法优化吗?
原因:
- 假设表中只有主键索引:数据最先是在磁盘上,我只需要第3000000到3000000 + 10的数据,但是执行引擎并不知道第3000000数据是哪条记录。所有这条sql会全表查询,把记录和条件匹配,直到符合条件的3000000 + 10的数据都加载进内存,再舍去前面条,才停止执行。
- 假设表中有主键索引及age建立的二级索引:数据最先是在磁盘上,这条sql会基于age索引进行查询主键id,因为b+tree数据页(节点)都是有序的key值记录,我们很容易直到age>45的数据页的位置,然后加载3000000 + 10条id数据。因为我们要查的是 * 而不id,所有这3000000 + 10id数据会再次基于主键索引拿到3000000 + 10条记录数据。然后再执行limit语句。(PS: 我们可以看到limit语句是最后截取,和sql 关键字执行顺序有关,可以了解下)
**优化方向:**考虑使用二级索引,减少io。
select * from user u1 right join (select id from user where age > 45 limit 3000000, 10);
驱动表语句单独拿出来
select id from user where age > 45 limit 3000000, 10;
虽然这条sql会基于非主键索引也就是age建立的索引,读取3000000 + 10条数据。和上面sql相比:我们知道非主键索引叶子节点能存储的数据比主键索引叶子节点能存储的数据多,也就是加载同样的数据,非主键索引要加载的数据页比主键索引要少。接下来拿到10条id再基于join连接查询效率是非常快的。
1.5.2 in关键子优化
现有user表 有500w条数据。table表就三条数据(uid: 1,2,3)现有如下sql:
select * from user where id in (select uid from table1), 它的执行效率如何,你有办法优化吗?
主观意识上我们会认为先执行 in里面的语句, 拿到三条uid(1,2,3)数据再对user表进行主键索引查询.这是非常快的,也是我们想要的查询方式。
在mysql5.5版本:先explain extended分析语句,再执行SHOW WARNINGS; 得到真实的sql如下
SELECT `数据库名`.`user`.`id` AS `id`,`数据库名`.`user `.`name` AS `name`,`数据库名`.`user`.`age` AS `age`
FROM `数据库名`.`user` WHERE <in_optimizer>(`数据库名`.`user`.`id`,<EXISTS>(<primary_index_lookup>(<CACHE>(`数据库名`.`user`.`id`) IN table1 ON PRIMARY)))
也就是说,执行引擎将in语句优化成exists语句。再对这条sql分析:先执行user表全表扫描,加载500w条数据,然后拿user表的id去table1表进行匹配。user表的500w条数据 导致table1表匹配就得500w次。这条sql在5.5版本是非常慢的。
在mysql5.7版本
同样先执行 explain extended select * from user where id in (select uid from table1) ;
再执行SHOW WARNINGS;
得到真实的sql如下
也就是说,在5.7版本执行引擎将in子查询语句优化成join连接,从这条sql可以看出,in里面的表被优化成驱动表,in的外表被优化成被驱动表,这种连接方式也是符合我们查询意愿的。
1.5.4 范围查寻解析
现有如下sql
- SELECT * FROM t_class WHERE id <= 6,它的执行计划是?
首先检索主键索引从最小下id=1拿取记录,然后通过一个数据页中有着单向链表关系依次找出‘2,3,4,5,6,7’记录数返回给server,其中判断id=7不符合条件。终止查找。得到结果集合。\ - SELECT * FROM t_class WHERE id >= 6,它的执行计划是?
首先检索出主键索引id=6或者大于6且最接近6的id记录。然后感觉这条记录依次往下寻找出所有记录数。