mysql的索引剖析
一、索引简介
1、概念:索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据。索引需要占用磁盘空间,因此在创建索引时要考虑到磁盘空间是否足够,并且创建索引时需要对表加锁,因此实际操作中需要在业务空闲期间进行。
2、索引的优缺点
优势: 可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
劣势: 索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表。
二、B+树
1、B+树特点: B+Tree是BTree的一个变种,B+Tree和BTree的不同主要在于:
-
B+Tree中的非叶子结点不存储数据,只存储key和指针;
-
所有键值都会出现在叶子结点上,叶子结点有一个链指针,方便区间范围查找;
-
B+Tree的每个非叶子节点由n个键值key和n个指针point组成;
2、B+Tree对比BTree的优点:
1)、磁盘读写代价更低
一般来说B+Tree比BTree更适合实现外存的索引结构,因为存储引擎的设计专家巧妙的利用了外存(磁盘)的存储结构,即磁盘的最小存储单位是扇区(sector),而操作系统的块(block)通常是整数倍的sector,操作系统以页(page)为单位管理内存,一页(page)通常默认为4K,数据库的页通常设置为操作系统页的整数倍,因此索引结构的节点被设计为一个页的大小,然后利用外存的“预读取”原则,每次读取的时候,把整个节点的数据读取到内存中,然后在内存中查找,已知内存的读取速度是外存读取I/O速度的几百倍,那么提升查找速度的关键就在于尽可能少的磁盘I/O,那么可以知道,每个节点中的key个数越多,那么树的高度越小,需要I/O的次数越少,因此一般来说B+Tree比BTree更快,因为B+Tree的非叶节点中不存储data,b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”,就可以存储更多的key。
2)、查询速度更稳定
由于B+Tree非叶子节点不存储数据(data),因此所有的数据都要查询至叶子节点,而叶子节点的高度都是相同的,因此所有数据的查询速度都是一样的。
3)、易于区间的范围查找
很多存储引擎在B+Tree的基础上进行了优化,添加了指向相邻叶节点的指针,形成了带有顺序访问指针的B+Tree,这样做是为了提高区间查找的效率,只要找到第一个值那么就可以顺序的查找后面的值。
三、MyISAM 索引实现 底层索引的实现
简介:在索引的分类中,我们可以按照索引的键是否为主键来分为“主索引”和“辅助索引”,使用主键键值建立的索引称为“主索引”,其它的称为“辅助索引”。因此主索引只能有一个,辅助索引可以有很多个。
1、主索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址。下图是 MyISAM 主索引(Primary key)的原理图:可以看出 MyISAM 的索引文件仅仅保存数据记录的地址。
2、辅助索引
在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复。同样也是一颗 B+Tree,data 域保存数据记录的地址。因此,MyISAM 中索引检索的算法为首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其data 域的值,然后以 data 域的值为地址,读取相应数据记录。
3、非聚集索引
MyISAM 的索引方式也叫做“非聚集索引”,非聚集(unclustered)索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。我们可以通过两次查询来找到所查找的结果,先找到目录中的结果地址,然后再通过地址找到数据行。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”,MyISAM的索引方式索引和数据存放是分开的,非聚集的,所以也叫做非聚集索引。。
四、InnoDB 底层索引的实现
1、主索引:InnoDB 也使用 B+Tree 作为索引结构,但具体实现方式却与 MyISAM 截然不同。 InnoDB 的数据文件本身就是索引文件。 从上文知道,MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这棵树的叶点data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。
2、辅助索引: InnoDB 的辅助索引 data 域存储相应记录主键的值而不是地址。换句话说,InnoDB 的所有辅助索引都引用主键作为 data 域。辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
3、聚集索引
由InnoDB 主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集。聚集索引这种实现方式使得按主键的搜索十分高效。聚簇索引的数据是根据主键的顺序保存。因此适合按主键索引的区间查找,可以有更少的磁盘I/O,加快查询速度。但是也是因为这个原因,聚簇索引的插入顺序最好按照主键单调的顺序插入,否则会频繁的引起页分裂,严重影响性能。
四、联合索引及最左原则
1、概念: 两个或更多个列上的索引被称作联合索引,联合索引又叫复合索引。对于复合索引Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。在Mysql建立多列索引(联合索引)有最左前缀的原则,即最左优先。
2、示例: 如果我们建立了一个2列的联合索引(col1,col2),实际上已经建立了两个联合索引(col1)、(col1,col2); 如果有一个3列索引(col1,col2,col3),实际上已经建立了三个联合索引(col1)、(col1,col2)、(col1,col2,col3)。
(A,B,C) 这样3列,mysql会首先匹配A,然后再B,C。如果用(B,C)这样的数据来检索的话,就会找不到A使得索引失效。如果使用(A,C)这样的数据来检索的话,就会先找到所有A的值然后匹配C,此时联合索引是失效的。把最常用的,筛选数据最多的字段放在左侧。
最左匹配原因: mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。
- 联合索引(A,B,C)是一棵B+Tree,其非叶子节点存储的是第一个关键字的索引,而叶节点存储的则是三个关键字A、B、C三个关键字的数据,且按照A、B、C的顺序进行排序。
- 当执行以下查询的时候,是无法使用这个联合索引的。
select * from STUDENT where B='b';
因为联合索引中是先根据A进行排序的。如果A没有先确定,直接对B和C进行查询的话,就相当于乱序查询一样,因此索引无法生效,查询是全表查询。
五、索引失效
索引失效的情况:
- 在组合索引中不能有列的值为NULL,如果有,那么这一列对组合索引就是无效的。
- LIKE操作中,like查询是以’%‘开头的不会使用索引,’%aaa%'不会使用索引,也就是索引会失效,但是‘aaa%’可以使用索引。
- 在索引的列上使用表达式或者函数会使索引失效,例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。其它通配符同样,也就是说,在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
- 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。
- 字符串不加单引号会导致索引失效。更准确的说是类型不一致会导致失效,比如字段email是字符串类型的,使用WHERE email=99999 则会导致失败,应该改为WHERE email=‘99999’。
- 在查询条件中使用OR连接多个条件会导致索引失效,除非OR链接的每个条件都加上索引,这时应该改为两次查询,然后用UNION ALL连接起来。
- 联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效
六、索引树的层数
假设:表的记录数是N、每一个BTREE节点平均有B个索引KEY、那么B+TREE索引树的高度就是logNB(等价于logN/logB)。
由于索引树每个节点的大小固定,所以索引KEY越小,B值就越大,那么每个B+TREE节点上可以保存更多的索引KEY,也就是B值越大,索引树的高度就越小,那么基于索引的查询的性能就越高。所以相同表记录数的情况下,索引KEY越小,索引树的高度就越小。
在计算机中磁盘存储数据最小单元是扇区,一个扇区的大小是512字节,而文件系统(例如XFS/EXT4)他的最小单元是块,一个块的大小是4k,而对于我们的InnoDB存储引擎也有自己的最小储存单元——页(Page),一个页的大小是16K。
1、通常一棵B+树可以存放多少行数据?
单个叶子节点(页)中的记录数=16K/1K=16。(这里假设一行记录的数据大小为1k,实际上现在很多互联网业务数据记录大小通常就是1K左右)。这里我们先假设B+树高为2,即存在一个根节点和若干个叶子节点,那么这棵B+树的存放总记录数为: 根节点指针数*单个叶子节点记录行数 。
上文我们已经说明单个叶子节点(页)中的记录数=16K/1K=16。(这里假设一行记录的数据大小为1k,实际上现在很多互联网业务数据记录大小通常就是1K左右)。
那么现在我们需要计算出非叶子节点能存放多少指针?
其实这也很好算,我们假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码中设置为6字节,这样一共14字节,我们一个页中能存放多少这样的单元,其实就代表有多少指针,即 16384/14=1170 。那么可以算出一棵高度为2的B+树,能存放 1170*16=18720 条这样的数据记录。
根据同样的原理我们可以算出一个高度为3的B+树可以存放: 1170*1170*16=21902400 条这样的记录。所以在InnoDB中B+树高度一般为1-3层,它就能满足千万级的数据存储。在查找数据时一次页的查找代表一次IO,所以通过主键索引查询通常只需要1-3次IO操作即可查找到数据。一个千万量级,且存储引擎是MyISAM或者InnoDB的表,其索引树的高度在3~5层左右。
七、索引的适用情形
哪些情况下适合建索引
- 在经常需要搜索的列上,可以加快搜索的速度;
- 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
- 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度;
- 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
- 在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
- 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
哪些情况下不适合建索引
一般来说,不应该创建索引的的这些列具有下列特点:
- 对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
- 对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
- 对于那些定义为text, image和bit、bool 枚举等数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
- 经常增删改的表的字段不适合建立索引。当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。尽管索引提供了一种快速访问数据的途径,但它们减缓了数据修改语句,因为当插入、修改和删除时,需要额外的负担来维护索引。 因此,当修改性能远远大于检索性能时,不应该创建索引。
八、数据表的单表最大数据量
Mysql 单表的最优最大数量的一个重要因素是磁盘空间,除了硬件之外,索引也是重要影响因素。 Mysql 的主要存储引擎 InnoDB 采用 B+树结构索引。
那么 B+树索引是如何影响 Mysql 单表数据量的呢?Mysql 的 B+树索引存储在磁盘上,Mysql 每次读取磁盘 Page 的大小是 16KB,为了保证每次查询的效率,需要保证每次查询访问磁盘的次数,一般设计为 2-3 次磁盘访问,再多性能将严重不足。Mysql B+树索引的每个节点需要存储一个指针(8Byte)和一个键值(8Byte)。因此计算16KB/(8B+8B)=1K, 16KB 可以存储 1K 个节点,3 次磁盘访问(即 B+树 3 的深度)可以存储 1K *1K *1K 即 10 亿数据。如果查询依赖非主键索引,那么还涉及二级索引。这样数据量将更小。
九、百万级数据分页的优化
方法一:直接使用数据库提供的SQL语句
语句样式:MySQL中可用如下方法:
select * from table_name limit m, n;
适用场景:适用于数据量较少的情况(元组百/千级)
原因/缺点: 全表扫描,速度会很慢 且 有的数据库结果集返回不稳定(如某次返回1,2,3,另外的一次返回2,1,3). limit限制的是从结果集的 m 位置处取出 n 条输出,其余抛弃.
方法二:建立主键或唯一索引,利用索引(假设每页10条)
语句样式: MySQL中,可用如下方法:
select * from table_name where id_pk > (pageNum*10) limit m;
适应场景: 适用于数据量多的情况(元组数上万)
原因: 索引扫描,速度会很快. 有朋友提出: 因为数据查询出来并不是按照pk_id排序的,所以会有漏掉数据的情况,只能方法3
方法三:基于索引再排序
语句样式,MySQL中可用如下方法:
select * from table_name where id_pk > (pageNum * 10) order by id_pk asc limit m;
适应场景: 适用于数据量多的情况(元组数上万). 最好 order by 后的列对象是主键或唯一所以,使得 order by 操作能利用索引被消除但结果集是稳定的(稳定的含义,参见方法1)
原因: 索引扫描,速度会很快. 但MySQL的排序操作,只有 asc 没有 desc ( desc 是假的,未来会做真正的 desc ,期待…).
方法四:基于索引使用prepare
第一个问号表示pageNum,第二个问号表示每页元组数
语句样式,MySQL中可用如下方法:
prepare stmt_name from select * from table_name where id_pk > (? * ?) order by id_pk asc limit m;
适应场景: 大数据量
原因: 索引扫描,速度会很快. prepare语句又比一般的查询语句快一点。
方法五:利用MySQL支持order操作可以利用索引快速定位部分元祖,避免全表扫描
比如:读第1000到1019行元组(pk是主键/唯一键).
select * from your_table where pa >= 1000 order by pk asc limit 0,20;
方法六:利用"子查询/连接+索引"快速定位元祖的位置,然后再读取元祖。
比如(id是主键/唯一键,蓝色字体时变量)
利用子查询示例:
select * from your_table where id <= (select id from your_table order by id desc limit ($page - 1) * $pagesize order by id desc limit $pagesize)
利用连接示例:
select * from your_table as t1 join (select id from your_table order by id desc limit ($page - 1) * $pagesize as t2 where t1.id <= t2.id order by t1.id desc limit $pagesize);
mysql大数据量使用limit分页,随着页码的增大,查询效率越低下。
十、锁表
如果使用针对InnoDB的表使用行锁,被锁定字段不是主键,也没有针对它建立索引的话。行锁锁定的也是整张表。锁整张表会造成程序的执行效率会很低。 InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
十一、索引下推和回表
回表:
MySQL
通过普通索引没法一次性将数据拿全的情况下,通过普通索引获取主键值,再通过主键值到聚集索引中定位到记录,这个过程就叫回表。可以通过建立覆盖索引来减少回表,比如现在要通过身份证号查姓名,那就建立身份证号和姓名的联合索引(id
,name
),当查询时可以通过这个索引直接拿到姓名name
得值,不再需要去聚集索引里查找了,这就是覆盖索引。
-
二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。
-
Mysql数据库,且使用的搜索引擎为innodb引擎,建立了主键索引和普通索引,通过主键索引可以查出来一条数据,通过辅助索引查询需要先查询到对应的主键索引的值,然后根据主键索引查询到相应的信息。如果通过辅助索引所要查询的信息在包含在辅助索引中,就不会再通过主键索引的值去数据库中查询,而是直接可以返回结果,如果辅助索引中的信息不完整,则会通过主键索引去获取数据信息,这种情况就被称为回表查询。
-
MySQL中的回表查询,二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后,再查询到想要的数据,这种通过二级索引查询出来的过程,就叫做回表。
索引下推:
MySQL服务层负责SQL语法解析、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。
索引下推
的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。索引条件下推ICP就是尽可量利用二级索引筛除不符合where条件的记录,如此一来减少需要回表继续判断的次数。索引下推可以减少存储引擎访问数据表的次数以及MySQL服务器访问存储引擎的次数。
- 在没有索引下推的情况下,MySQL通过存储引擎遍历索引来定位表中的数据行并将它们返回给MySQl服务器,服务器再进行WHERE条件的判断,确认是否将数据行加入结果集。
- 开启索引下推,且WHERE条件部分可以仅使用索引中的列来评估,这时MySQL服务器会将这部分WHERE条件下推到存储引擎,接着存储引擎使用索引条目评估推送的索引条件,仅当满足该条件时才从表中进行读取。
示例1: 表student中存在联合索引name 、age.
explain ``select` `* ``from` `student ``where` `name` `like` `'peng%'` `and` `age = 23;
-
在MySQL5.6之前,只能从name字段中找出符合条件的行然后开始回表,到聚集索引上找出数据行,再对age字段进行对比,把符合条件的数据加入到结果集中。需要回表多次获取多条记录,把联合索引的另一个字段
age
浪费了。 -
在MySQL5.6引入了索引下推优化,在索引的遍历过程中,对索引中包含字段先做判断,这里对age字段进行判断。直接将age字段不满足的数据行排除,从而减少回表的次数。
示例2: SELECT * FROM user_info WHERE name LIKE "大%" AND level = 1;
存在建立联合索引(name, level)
- 无索引下推:查询条件
name LIKE
不是等值匹配,根据最左匹配原则,在(name, level)
索引树上只用到name
去匹配,查找到两条记录(id为1和4),拿到这两条记录的id分别回表查询,然后将结果返回给MySQL server,在MySQL server层进行level
字段的判断。整个过程需要回表2次。
- 索引下推:在索引遍历过程中,对索引中的字段先做判断,过滤掉不符合条件的索引项,也就是判断level是否等于1,level不为1则直接跳过。因此在
(name, level)
索引树只匹配一个记录,之后拿着此记录对应的id(id=1)回表查询全部数据,整个过程回表1次。可以使用explain查看是否使用索引下推,当Extra
列的值为Using index condition
,则表示使用了索引下推。
索引下推只适用于二级索引(也叫辅助索引); 索引下推的目的是为了减少回表次数,也就是要减少IO操作。对于的聚簇索引来说,数据和索引是在一起的,不存在回表这一说。
- 引用了子查询的条件不能下推;
- 引用了存储函数的条件不能下推,因为存储引擎无法调用存储函数。