本文来自网易云社区。mysql
Innodb是Mysql最经常使用的存储引擎,了解Innodb存储引擎的索引对于平常工做有很大的益处,索引的存在即是为了加速数据库行记录的检索。如下是我对最近学习的知识的一些总结,以及对碰到的以及别人提到过的问题的一些分析,若有错误,请指正,我会及时更正。算法
1. Innodb表结构sql
此小结与索引其实没有太多的关联,可是为了便于理解索引的内容,添加此小结做为铺垫知识。数据库
Mysql表中的全部数据被存储在一个空间内,称之为表空间,表空间内部又能够分为段(segment)、区(extent)、页(page)、行(row),逻辑结构以下图:编程
表空间是由不一样的段组成的,常见的段有:数据段,索引段,回滚段等等,在Mysql中,数据是按照B+树来存储,所以数据即索引,所以数据段即为B+树的叶子节点,索引段为B+树的非叶子节点,回滚段用于存储undo日志,用于事务失败后数据回滚以及在事务未提交以前经过undo日志获取以前版本的数据,在Innodb1.1版本以前一个Innodb,只支持一个回滚段,支持1023个并发修改事务同时进行,在Innodb1.2版本,将回滚段数量提升到了128个,也就是说能够同时进行128*1023个并发修改事务。缓存
区是由连续页组成的空间,每一个区的固定大小为1MB,为保证区中页的连续性,Innodb会一次从磁盘中申请4~5个区,在默认不压缩的状况下,一个区能够容纳64个连续的页。可是在开始新建表的时候,空表的默认大小为96KB,是因为为了高效的利用磁盘空间,在开始插入数据时表会先利用32个页大小的碎片页来存储数据,当这些碎片使用完后,表大小才会按照MB倍数来增长。服务器
页是Innodb存储引擎的最小管理单位,每页大小默认是16KB,从Innodb 1.2.x版本开始,能够利用innodb_page_size
来改变页size,可是改变只能在初始化Innodb实例前进行修改,以后便没法进行修改,除非mysqldump导出建立新库,常见的页类型有:数据页、undo页、系统页、事务数据页、插入缓冲位图页、插入缓冲空闲列表页、未压缩的二进制大对象页、压缩的二进制大对象页。并发
行对应的是表中的行记录,每页存储最多的行记录也是有硬性规定的最多16KB/2-200,即7992行(16KB是页大小,我也不明白为何要这么算,听说是内核定义)函数
1.2 Innodb行记录格式高并发
Innodb提供了两种格式来存储行记录:Redundant格式、Compact格式、Dynamic格式、Compressed格式,Redudant格式是为了兼容保留的。
隐藏列:事务id和回滚列id,分别占用六、7字节,若此表没有主键,还会增长6字节的rowid列。
Dynamic格式和Compressed格式与Compact的不一样之处在于对于行溢出只会在该列处存放20字节的指针,指向该字符串的实际存储位置,不会存储768字节前缀,并且Compressed格式在存储BLOB、TEXT、VARCHAR等类型会利用zlib算法进行压缩,可以以很高的存储效率来存储字符串。
《Mysql技术内幕-Innodb存储引擎》书中对此有描述,可是应该不是太准确,书中有以下描述,此处不作详细介绍,如有兴趣请看此神书。
B树(B-TREE)知足以下条件,便可称之为m阶B树:
B+树知足以下条件,便可称之为m阶B+树:
B+树的插入必须保证插入后叶节点中的记录依然排序,同时须要考虑插入B+树的三种状况,每种状况均可能会致使不一样的插入算法,插入算法入下图:
插入举例(未加入双向链表):
一、 插入28这个键值,发现当前Leaf Page和Index Page都没有满,直接插入。
二、 插入70这个键值,Leaf Page已经满了,可是Index Page尚未满,根据中间的值60拆分叶节点。
三、 插入记录95,Leaf Page和Index Page都满了,这时须要作两次拆分
四、 B+树老是会保持平衡。可是为了保持平衡,对于新插入的键值可能须要作大量的拆分页(split)操做,而B+树主要用于磁盘,所以页的拆分意味着磁盘数据移动,应该在可能的状况下尽可能减小页的拆分。所以,B+树提供了旋转(rotation)的功能。旋转发生在Leaf Page已经满了、可是其左右兄弟节点没有满的状况下。这时B+树并不会急于去作拆分页的操做,而是将记录移到所在页的兄弟节点上。一般状况下,左兄弟被首先检查用来作旋转操做,在第一张图状况下,插入键值70,其实B+树并不会急于去拆分叶节点,而是作旋转,50,55,55旋转。
B+树使用填充因子(fill factor)来控制树的删除变化,50%是填充因子可设的最小值。B+树的删除操做一样必须保证删除后叶节点中的记录依然排序,同插入同样,B+树的删除操做一样须要考虑下图所示的三种状况,与插入不一样的是,删除根据填充因子的变化来衡量。
删除示例(未加入双向链表): 一、删除键值为70的这条记录,直接删除(在插入第三点基础上的图)。
二、接着咱们删除键值为25的记录,该值仍是Index Page中的值,所以在删除Leaf Page中25的值后,还应将25的右兄弟节点的28更新到Page Index中。
三、删除键值为60的状况,删除Leaf Page中键值为60的记录后,填充因子小于50%,这时须要作合并操做,一样,在删除Index Page中相关记录后须要作Index Page的合并操做。
以m阶树为例:
每一个Innodb的表都拥有一个索引,称之为聚簇索引,此索引中存储着行记录,通常来讲,聚簇索引是根据主键生成的。为了可以得到高性能的查询、插入和其余数据库操做,理解Innodb聚簇索引是颇有必要的。
聚簇索引按照以下规则建立:
Note: 对于选择惟一索引的顺序是按照定义惟一索引的顺序,而非表中列的顺序, 同时选中的惟一索引字段会充当为主键,或者Innodb隐式建立的自增列也能够看作主键。
聚簇索引总体是一个b+树,非叶子节点存放的是键值,叶子节点存放的是行数据,称之为数据页,这就决定了表中的数据也是聚簇索引中的一部分,数据页之间是经过一个双向链表来连接的,上文说到B+树是一棵平衡查找树,也就是聚簇索引的数据存储是有序的,可是这个是逻辑上的有序,可是在实际在数据的物理存储上是,由于数据页之间是经过双向链表来链接,假如物理存储是顺序的话,那维护聚簇索引的成本很是的高。
除了聚簇索引以外的索引均可以称之为辅助索引,与聚簇索引的区别在于辅助索引的叶子节点中存放的是主键的键值。一张表能够存在多个辅助索引,可是只能有一个聚簇索引,经过辅助索引来查找对应的航记录的话,须要进行两步,第一步经过辅助索引来肯定对应的主键,第二步经过相应的主键值在聚簇索引中查询到对应的行记录,也就是进行两次B+树搜索。相反经过辅助索引来查询主键的话,遍历一次辅助索引就能够肯定主键了,也就是所谓的索引覆盖,不用回表(查询聚簇索引)。
建立辅助索引,能够建立单列的索引,也就是用一个字段来建立索引,也能够用多个字段来建立副主索引称为联合索引,建立联合索引后,B+树的节点存储的键值数量不是1个,而是多个,以下图:
order by
对某个字段进行排序时,能够减小复杂度,加速进行查询;select * from table where a=? and ?
可使用索引(a,b)来加速查询,可是在查询时有一个原则,sql的where条件的顺序必须和二级索引一致,并且还遵循索引最左原则,select * from table where b=?
则没法利用(a,b)索引来加速查询。
如下的每一步操做都会生成一个虚拟表,做为下一个处理的输入,在这个过程当中,这些虚拟表对于用户都是透明的,只用最后一步执行完的虚拟表返回给用户,在处理过程当中,没有的步骤会直接跳过。
如下为逻辑上的执行顺序:
from
:对左表left-table
和右表right-table
执行笛卡尔积(a*b),造成虚拟表VT1;on
: 对虚拟表VT1进行on
条件进行筛选,只有符合条件的记录才会插入到虚拟表VT2中;join
: 指定out join
会将未匹配行添加到VT2产生VT3,如有多张表,则会重复(1)~(3);where
: 对VT3进行条件过滤,造成VT4, where
条件是从左向右执行的;group by
: 对VT4进行分组操做获得VT5;cube | rollup
: 对VT5进行cube | rollup
操做获得VT6;having
: 对VT6进行过滤获得VT7;select
: 执行选择操做获得VT8,本人看来VT7和VT8应该是同样的;distinct
: 对VT8进行去重,获得VT9;order by
: 对VT9进行排序,获得VT10;limit
: 对记录进行截取,获得VT11返回给用户。Note: on
条件应用于连表过滤,where
应用于on过滤后的结果(有on
的话),having
应用于分组过滤
5. sql优化建议
索引有以下有点:减小服务器扫描的数据量、避免排序和临时表、将随机I/O变为顺序I/O。
like 'xx%'
能够走索引;>
,like
等;select
的字段为主键;范围查询后的条件不会走索引,具体缘由会在下一节进行介绍。
选择性(区分度)是指不重复的列值个数/列值的总个数,通常意义上建索引的字段要区分度高,并且在建联合索引的时候区分度高的列字段要放在前边,这样能够在第一个条件就过滤掉大量的数据,有利用性能的提高,对于如何计算列的区分度,有以下两种方法:
show index from <table_name>
来查看 解释一下此处的carlinality并非准确值,并且mysql在B+树种选择了8个数据页来抽样统计的值,也就是说carlinality=每一个数据页记录总和/8*全部的数据页,所以也说明这个值是不许确的,由于在插入/更新记录时,实时的去更新carlinality对于Mysql的负载是很高的,若是数据量很大的话,触发mysql从新统计该值得条件是当表中的1/16数据发生变化时。可是选择区分度高的列做为索引也不是百试百灵的,某些状况仍是不合适的,下节会进行介绍。
Mysql查询过程
当但愿Mysql可以高性能运行的时候,最好的办法就是明白Mysql是如何优化和执行的,一旦理解了这一点,不少查询优化工做实际上就是遵循了一些原则让优化器可以按照预想的合理的方式运行————《引用自高性能Mysql》
当想Mysql实例发送一个请求时,Mysql按照以下图的方式进行查询:
in
操做将范围查询转换成多个等值查询;in
操做至关于多个等值操做,可是要注意的是对于order by
来讲,这至关于范围查询,所以例如select * from t1 where c1 in (x,x) order by c2
的sql是不走索引的;inner join
,好比说分页时候
6. 一些问题分析
这个部分是我在学习过程当中产生的一些疑问,以及在工做中碰到的或者同事提起的一些问题,对此我作了些调研,总结了一下并添加了些本身的理解,若有错误还请指正。
此处提一下索引分裂,就我我的理解,在Mysql插入记录的同时会更新配置的相应索引文件,根据以上的了解,在插入索引时,可能会存在索引的页的分裂,所以会致使磁盘数据的移动。当插入的主键是随机字符串时,每次插入不会是在B+树的最后插入,每次插入位置都是随机的,每次均可能致使数据页的移动,并且字符串的存储空间占用也很大,这样重建索引不只仅效率低并且Mysql的负载也会很高,同时还会致使大量的磁盘碎片,磁盘碎片多了也会对查询形成必定的性能开销,由于存储位置不连续致使更多的磁盘I/O,这就是为何推荐定义主键为递增整型的一个缘由,Mysql索引页默认大小是16KB,当有新纪录插入的时候,Mysql会留下每页空间的1/16用于将来索引记录增加,避免过多的磁盘数据移动。
对于高并发的场景,在Innodb中按照主键的顺序插入可能会形成明显的争用,主键的上界会成为“热点”,由于全部的插入都发生在此处,索引并发的插入可能会形成间隙锁竞争,何为间隙锁竞争,下个会详细介绍;另一个缘由多是Auto_increment的锁机制,在Mysql处理自增主键时,当innodb_autoinc_lock_mode
为0或1时,在不知道插入有多少行时,好比insert t1 xx select xx from t2
,对于这个statement的执行会进行锁表,只有这个statement执行完之后才会释放锁,而后别的插入才可以继续执行,,可是在innodb_autoinc_lock_mode=2
时,这种状况不会存在表锁,可是只能保证全部并发执行的statement插入的记录是惟一而且自增的,可是每一个statement作的多行插入之间是不链接的。
好比一张order表中有联合索引(order_id, goods_id),在此例子上来讲明 这个问题是从两个方面来讲:
select order_id from order where order_id > 1000
,若是查看其执行计划的话,发现是用use index condition,走的是索引覆盖
select * from order where order_id > 1000
, 此条语句查询的是该表全部字段,有一部分字段并未在此联合索引中,所以走联合索引查询会走两步,首先经过联合索引肯定符合条件的主键id,而后利用这些主键id再去聚簇索引中去查询,而后获得全部记录,利用主键id在聚簇索引中查询记录的过程是无序的,在磁盘上就变成了离散读取的操做,假如当读取的记录不少时(通常是整个表的20%左右),这个时候优化器会选择直接使用聚簇索引,也就是扫全表,由于顺序读取要快于离散读取,这也就是为什么通常不用区分度不大的字段单独作索引,注意是单独由于利用此字段查出来的数据会不少,有很大几率走全表扫描。
根据Mysql的查询原理的话,当处理到where
的范围查询条件后,会将查询到的行所有返回到服务器端(查询执行引擎),接下来的条件操做在服务器端进行处理,这也就是为何范围条件不走索引的缘由了,由于以后的条件过滤已经不在存储引擎完成了。可是在Mysql 5.6之后假如了一个新的功能index condition pushdown(ICP),这个功能容许范围查询条件以后的条件继续走索引,可是须要有几个前提条件:
select * from xx where c1=x and c2>x and c3<x
,这样c3是能够走到索引的;where
条件的字段须要在索引中;set @@optimizer_switch = "index_condition_pushdown=on" 开启ICP set @@optimizer_switch = "index_condition_pushdown=off"
关闭ICP
好比建立一个表:
create table `person`( `id` int not null auto_increment primary key, `uid` int not null, `name` varchar(60) not null, `time` date not null, key `idx_uid_date` (uid, time) )engine=innodb default charset=utf8mb4;
当执行select count(*) from person where time > '2018-03-11'
and time < '2018-03-16'时,time是能够用到
idx_uid_date`的索引的,看以下的执行计划:
其中extra标识use index说明是走索引覆盖的,通常意义来讲是Mysql是没法支持松散索引的,可是对于统计函数,是可使用索引覆盖的,所以Mysql的优化器选择利用该索引。
在Mysql中,分页当offset值很大的时候,性能会很是的差,好比limit 100000, 20
,须要查询100020条数据,而后取20条,抛弃前100000条,在这个过程当中产生了大量的随机I/O,这是性能不好的缘由,为了解决这个问题,切入点即是减小无用数据的查询,减小随机I/O。 解决的方法是利用索引覆盖,也就是扫描索引获得id而后再从聚簇索引中查询行记录,我知道有两种方式:
好比从表t1中分页查询limit 1000000,5
inner join
select * from t1 inner join (select id from t1 where xxx order by xx limit 1000000,5) as t2 using(id)
,子查询先走索引覆盖查得id,而后根据获得的id直接取5条得数据。
select * from t1 where id > 1000000 order by id limit 0, 5
,即利用条件id > 1000000
在扫描索引是跳过1000000条记录,而后取5条便可,这种处理方式的offset值便成为0了,但此种方式一般分页不能用,可是能够用来分批取数据。
SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20; SELECT * FROM tbl_name WHERE (key1 = 10 OR key2 = 20) AND non_key=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);
对于如上的sql在mysql 5.0版本以前,假如没有创建相应的联合索引,是要走全表扫描的,可是在Mysql 5.1后引入了一种优化策略为索引合并,能够在必定程度上利用表上的多个单列索引来定位指定行,其原理是将对每一个索引的扫描结果作运算,总共有:交集、并集以及他们的组合,可是索引合并并不是是一种合适的选择,由于在作索引合并时可能会消耗大量的cpu和内存资源,通常用到索引合并的状况也从侧面反映了该表的索引须要优化。
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易云社区,经做者范鹏程受权发布。