10 索引
10.1 数据页存储结构
10.1.1 数据页的各个部分
在讲索引之前,让我们看看一个单独的数据页是什么样子的
去除掉一些我们不太需要那么关注的部分后,简化如下:
也就是说平时我们在一个表里插入的一行一行的数据会存储在数据页里,然后数据页里的每一行数据都会按照主键大小进行排序存储,同时每一行数据都有指针指向下一行数据的位置,组成单向链表。
10.1.2 页分裂
随着业务的发生,我们的数据页一般会越来越大,当大到一定程度的时候,就需要再搞一个数据页了,如下图所示
但是这一步骤并不是说简简单单多加一个数据页就 OK,还需要保证新加的数据页中的每一行数据的主键值都要比前面的大才行,所以数据行有可能会在数据页中挪动。具体如下图所示:
10.2 索引页存储结构
10.2.1 概念
上一节我们讲了数据页的存储结构,这一节我们继续学习索引页的存储结构。
我们先思考一个问题,如果我们只有一般的数据页,咱们怎么找到自己想要的数据呢?是不是要将数据页全部遍历,再在每一个页中,通过二分查找查询数据。这么做,实在是太慢了!所以 MySQL 抽象出了一个索引页的概念,它和一般的数据页差不多,只不过存放的是最小主键值和页号。然后后续你查询主键值,就可以在目录里二分查找直接定位到那条数据所属的数据页,接着到数据页里二分查找定位那条数据就可以了,如下图所示
但是随着数据页越来越多,索引页也变得越来越多,这个时候怎么办呢?这个时候 MySQL 会抽象出一个更高层级的索引页,它里面记录的是最小主键值和索引页号。
那么现在问题再次来了,假如你最顶层的那个索引页里存放的下层索引页的页号也太多了,怎么办呢?此时可以再次分裂,再加一层索引,最后不断的向上加,索引页看起来就像下面这个样子了,也就是一颗 B+树。
10.2.1 例子
最后我们以最简单最基础的主键索引来举例,当你为一个表的主键建立起来索引之后,其实这个主键的索引就是一颗 B+树,然后当你要根据主键来查数据的时候,直接就是从 B+树的顶层开始二分查找,一层一层往下定位,最终一直定位到一个数据页里,在数据页内部的目录里二分查找,找到那条数据。
10.3 聚簇索引
10.3.1 特点
我们上面介绍的B+树索引,它有两个特点:
-
使用记录主键值的大小进行记录和页的排序
-
B+树的叶子节点存储的是完整的用户记录
符合这两个特点的索引,就是聚簇索引。在 InnoDB 存储引擎中,聚簇索引就是数据的存储方式,InnoDB 存储引擎会自动的为我们创建聚簇索引。
10.4 二级索引
10.4.1 概念
聚簇索引,使用记录主键值的大小进行记录和页的排序,他是和主键强关联的。但是如果查询的条件不是主键,而是其他列呢?这个时候,就要请出咱们的二级索引了。
二级索引也是一颗B+树,但是它的数据页里存放的是主键+目标字段值。换句话说,将聚簇索引中的主键值替换成目标值段,且叶子节点仅存储主键+目标字段值这两个列的值,那么他就是二级索引了。
当你要根据目标字段来查数据的时候,直接就是从 B+树的顶层开始二分查找,一层一层往下定位,最终一直定位到一个数据页里,在数据页内部的目录里二分查找,找到那条数据。但是这条数据只有主键+目标字段值。
10.4.2 图解
10.4.3 回表
如果你的查询结果中还需要有其他值,那么你得再根据主键在聚簇索引这个B+树中,再查找一次,得到最终的结果,这个过程叫做回表。
10.5 联合索引
联合索引也是一颗B+树,但是它的数据页里存放的是主键+多个目标字段值。其他和二级索引类似。
10.6 覆盖索引与回表查询
10.6.1 概念
首先我们要明确一点,覆盖索引,并不是真正的索引,他其实是一种基于索引的查询方式。
不管是二级索引,还是联合索引,如果你的查询结果中没有其他值,只有索引值,那么你得不需要再在聚簇索引这个B+树中,再查找一次,只需要扫描当前索引的叶子节点,就能得到结果,这种就叫做覆盖索引。
10.6.2 例子
select xx1,xx2,xx3 from table order by xx1,xx2,xx3
基于xx1,xx2,xx3建立联合索引
这种情况下,你仅仅需要联合索引里的几个字段的值,那么其实就只要扫描联合索引的索引树就可以了,不需要回表去聚簇索引里找其他字段了。
10.7 如何更好的建立索引
通过前文我们知道了索引其实是一颗一颗的B+树,那么接下来我们介绍一下B+树索引适用的条件。
10.7.1 索引适用的条件
首先我们给出一张示例表,如下:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(32) NOT NULL COMMENT '用户名',
`age` int(3) NOT NULL COMMENT '年龄',
PRIMARY KEY (`id`),
KEY `index_name_age` (`name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user示例表';
这个例子中,有两个索引,一个是根据id排序的聚簇索引,另一个是跟据name和age排序的二级索引。
注意:二级索引,是先跟据name排序,如果name相同,再根据age排序的。
10.7.1.1 全值匹配
例子:
SELECT * FROM user WHERE name = '第七人格' AND age = '29';
解析:
-
因为B+树的数据页和记录先是按照name列的值进行排序的,所以先可以很快定位name列的值是“第七人格”的记录位置。
-
在name列相同的记录里又是按照age列的值进行排序的,所以在name列的值是“第七人格”的记录里又可以快速定位age列的值是’29’的记录。
101.7.1.2 匹配左边的列
【例子】:
SELECT * FROM user WHERE name = '第七人格';
解析:
-
因为B+树的数据页和记录先是按照name列的值进行排序的,所以先可以很快定位name列的值是“第七人格”的记录位置。
【反例】:
SELECT * FROM user WHERE age = '29';
解析:
-
因为B+树的数据页和记录先是按照name列的值进行排序的,所以先可以很快定位name列的值是“第七人格”的记录位置,但是现在用age去找,你想想能找到吗?当然找不到了,你都没找到第一层排序的name值,怎么能找到下层的age呢?
10.7.1.3 匹配列前缀
【例子】:
SELECT * FROM user WHERE name like '第七%';
解析:
-
因为B+树的数据页和记录先是按照name列的值进行排序的,那么的值是按照字符串排序的,字符串本质是按照字符排序的,这个例子中“第七”是被排好序了的,也可以很快定位name列的值是“第七…”的记录位置。
【反例】:
SELECT * FROM user WHERE name like '%第七%';
或者
SELECT * FROM user WHERE name like '%第七';
解析:
-
“第七”并没有排好序,所以无法使用索引。
10.7.1.4 匹配范围值
【例子】:
SELECT * FROM user WHERE name > 'Anna' AND name < 'Ziad';
解析:
-
name能用到索引。
【例子】:
SELECT * FROM user WHERE name > 'Anna' AND age < '35';
解析:
-
name能用到索引,age不能。
【思考】:
SELECT * FROM user WHERE name = 'Anna' AND age < '35';
请读者思考上面可以使用到索引吗?为什么?
10.7.1.5 排序
【例子】:
SELECT * FROM user ORDER BY name,age LIMIT 1;
解析:
-
这个是可以用到索引的,因为他是按照联合索引的字段顺序去进行order by排序的,这样就可以直接利用联合索引树里的数据有序性,到索引树里直接按照字段值的顺序去获取数据。
【反例】:
SELECT * FROM user ORDER BY name ASC,age DESC;
解析:
-
既有升序又有降序,没办法使用索引。
10.7.1.6 分组
【例子】:
SELECT name,age FROM user GROUP BY name , age;
解析:
-
这个是可以用到索引的,原因可以类比排序。
10.7.2 阿里巴巴索引规约
-
【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
-
【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。
-
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度。
-
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
-
【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
-
【推荐】利用覆盖索引来进行查询操作,避免回表。
-
【推荐】利用延迟关联或者子查询优化超多分页场景。
-
【推荐】建组合索引的时候,区分度最高的在最左边。
以上规约摘自阿里巴巴开发手册,通过前面的学习,我相信大家可以从更深层次理解这些规约了,而不是一味的死记硬背。
10.8 执行计划与性能优化浅谈
10.8.1 总览
在生产中我们判断一个SQL写的好不好,一般都是通过EXPLAIN关键字,看他的执行计划的。
首先我们看看执行计划长什么样子,我们执行以下SQL
EXPLAIN SELECT * FROM user WHERE name = '第七人格' AND age = '29';
得到的执行计划如下:
10.8.2 要素
10.8.2.1 id
这个字段对性能优化来说,不太重要,我们只需要知道,在一个大的查询语句中每个SELECT关键字都对应一个唯一的id 就行了。
10.8.2.2 select_type
SELECT关键字对应的那个查询的类型,也不重要。
10.8.2.3 table
表名,意思就是查的哪个表,也不是很重要。
10.8.2.4 partitions
匹配的分区信息,我们接触不到这个知识点,99.9%都是null。
10.8.2.5 type
针对单表的访问方法,这个就非常重要了。
完整的类型如下:system,const,eq_ref,ref,fulltext,ref_or_null,index_merge,unique_subquery,index_subquery,range,index,ALL 。
这里我们针对比较常见和重要的几种类型介绍一下。
const,常量级,一般出现这个,就表示,你写的SQL非常好,性能非常快。哪些属于const呢?比如select * from user where id=x,或者select * from user where name=x这样的的语句,直接就可以通过聚簇索引或者二级索引+聚簇索引回表,轻松查到你要的数据。
值的一提的是,这里的二级索引必须是唯一索引。如果是普通索引,那么就是ref了,就如我们总览中的执行计划一样。当然,ref也是一种非常快的查询方式。
range也是一种常见的查询方式,一般出现在范围查询中,比如我们前面举的一个例子select * from user where name>=x and name <=x,假设name就是一个普通索引,此时就必然利用索引来进行范围筛选,一旦利用索引做了范围筛选,那么这种方式就是range。
我们再回忆一下这个sql,select name,age from user where age = ‘29’,因为age不是索引最左边的值,所以它是没法从联合索引的根节点二分查找快速跳转的,但是因为他的结果和条件都在索引里,所以MySQL的优化器,会直接扫描这个联合索引,一个个的遍历。也就是说,针对这种只要遍历二级索引就可以拿到你想要的数据,而不需要回源到聚簇索引的访问方式,就叫做index访问方式,这种访问方式相对于前面几种,就要慢的多。
最后再看看ALL,顾名思义,全表扫描,一般属于我们要杜绝的情况。
10.8.2.6 possible_keys
可能用到的索引。
10.8.2.7 key
实际上使用的索引。
10.8.2.8 key_len
实际使用到的索引长度。
10.8.2.9 ref
当使用索引列等值查询时,与索引列进行等值匹配的对象信息。
10.8.2.10 rows
预估的需要读取的记录条数,这个值按道理来说,越小越好。
10.8.2.11 filtered
某个表经过搜索条件过滤后剩余记录条数的百分比,对于单表查询来说,这个值没什么意义,都是100%,但是对于多表查询就有意义了,越小越好。
10.8.2.12 Extra
一些额外的信息。比如Using where,Using index,Using filesort等等。我们用group by、union、distinct之类的语法的时候,要是没法直接利用索引来进行分组聚合,那么MySQL会直接基于临时表来完成,会有大量的磁盘操作,也就是会使用文件排序(Using filesort)。这种情况一般也是需要避免的。