- 索引是怎么提高查询效率的?可以为了提高查询效率增加索引么?
- mysql索引系统采用的数据结构是什么?
- 为什么要使用B+树?
- 聚集索引相对于非聚集索引的区别?
- 什么是回表?
- 什么是索引覆盖?
- 什么是最左匹配原则?
- 索引失效场景有哪些,如何避免?
索引原理探究
什么是数据库索引?先来个官方一些的定义吧。
官方解释:在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
索引不是mysql特有的,涉及到任何文件、数据查找的系统都有,比如windows操作系统也有索引的存在,帮助我们更加快速的找到对应的文件。
为了可以让查找的速度提升,那树就是一种很好数据结构。mysql在索引的设计上采用的就是采用B+树的结构来对数据进行排序存储,不管是聚簇索引 辅助索引 联合索引,本质上都是在b+树的结构上进行查找。
既然提到了B+树,还是要拓展一下,为啥B+树会作为主流数据结构。树的类型有这么多,为啥用B+树呢?那下面列举几种常见的 树 。
数据结构
普通二叉树
它是一颗普普通通的树,特点:
- 左边的字节点一定小于右边的子节点;
- 每个节点最多有2个叶子结点。
用了这种结构,再也不用顺序io了,可以通过折半查找来查询我们想要的数据,插入和查找的时间复杂度变成了O(log(N)),确实一定程度上减缓了io次数。 缺点:但是容易失衡,就像这样。
随着数据量越来越大,树阶越来越高,查询次数和io大幅提升啊。所以这种 肯定不适合mysql。那就需要优化一下,来个平衡二叉树,每次写入的时候判断左右两个节点的层级差不能超过1层不就行了,否则就左旋或者右旋自动平衡。
平衡二叉树
这样看起来是平衡了,但是如果节点增多了,树阶还是会增多,io次数又要上去了。所以这也不适合mysql。 缺点:每个节点下的子节点只能有两个,树阶还是高。
B树
基于平衡二叉树继续优化一下,让每个节点下可以放的叶子节点多一些,树高降低,io也就降下来了。
这个时候就需要B树(也叫平衡多路查找树),这里要注意这个(多路),我们在描述一个B书的时候,一般需要指定树阶,也就是一个节点下最多能存在几个孩子节点,下面看一下三阶的。 如下图:
这样看来树已经平衡,而且多路也可以降低树的高度。你可能有个大胆的想法,把数据都存在一个叶子节点上,是不是1亿数据最多也只要io三次就够了。
这时候要先引出一下mysql是如何于磁盘交互来读取数据的。
上面提到的存储引擎(innodb、myIsam),它的基础存储单位是页。 当我们发送查询指令,mysql会从磁盘读取数据,但是磁盘io效率极差,所以这里有一些小的优化措施,mysql在做一次读取的时候除了会加载需要读取数据,还会将地址附近的数据一起加载出来,这个动作叫预读。而mysql一次可以加载出来的数据就是16KB,所以一页的大小就是16K(注意看我上图画的,每一个大方块代表一页,一页里面的内容就是一次io读取出来的数据),这样就可以根据你每个key的大小推算出来一个节点能放多少路的数据了。 但是除了key,B树的行数据也是存在每节点上的**,看看一条行数据的大小,就能推算出一个节点的数据有多大了,数据大了,那一页上能存放的数据就少了,那节点就会多,节点多了,树又高了(套娃警告)。
B+树
所以为了让每个节点上尽可能的多存放数据,就要缩衣节食。B+树就在节点的数据存储上继续优化,他们唯一的区别就是,B+树在非叶子节点上不再存在数据,只存放指针地址。如下图:
从B树和B+树的结构图可以看出,两者在树结构上的差别不大,都是多路平衡查找树,区别在于以下两点:
- B+树所有的非叶子节点上就只有key了,而data只存放在叶子节点,每个非页子节点上的数据占用的空间就少了,再算下16KB能放多少数据了。B+树三层的高度就可以存放2000万的数据,每条数据如果在1kb左右。
首先我们算出非叶子节点能放多少指针,一个指针的大小是6bit,id如果是integer类型是8bit,一共14bit,一页是16kb,也就是16384bit,所以一层非叶子节点最多可以存在16384/14=1170个指针,三阶的树这一层有16个叶子节点,就可以存放1770 * 16个指针,每个指针又指向一页,刚刚算出来一页放1170个指针,那就再乘以1770,所以最终可以存放1170 * 1170 * 16=21902400条记录。也就是说在一棵三阶的B+树就可以放2000万的数据量级以内,3次io即可查询到。B树同理,只不过B树非叶子节点不止有id,还有数据,一页就不止是8+6=14了,可能会更大,所以相同树高存的东西也就少了。 拓展点: 如果超过这个数量,io次数又会增加,所以我们在设计表的时候需要清楚的知道每一行的数据有多大,来保证每张表在三层树高的情况下最多能存放多少数据,超过这个量级,可能就需要引入一些分表、或者数据迁移的工作,这也是大表优化的一些手段。
- 同时B树还存在一个缺点,范围查询效率不高,因为当你查到子节点的时候发现没路了,那需要重新返回根节点再次遍历,而在B+树中,可以看到每个叶子节点之间用双向指针连接,这样当进行范围查询,就可以通过叶子节点的双向指针来进行快速的查找了。
拓展点:为啥平时我们都要要求主键是自增的呢?(不管是数据库主键自增还是分布式算法的自增)。这也和树的结构有关。 看下下面这个图。
这颗B+树的数据按顺序写入,但是跳过了4,如果后面插进来一个4的key,那树为了维持节点的平衡,就会进行左旋或者右旋,在数据量大或者写入密集的情况下,自平衡的动作是非常消耗资源的,所以为了避免这种情况,我们就要让写入的key按顺序自增!(知其然知其所以然哟)
索引的基础结构B+树以及B+树的好处介绍完了,索引快(利用树形结构和折半查找)的原因就大致清楚了吧。接下来再来看看具体的索引类型。
拓展点:除了树形结构,索引比较常见的还有hash表结构,和java中的hashMap类似,以键值对的方式存储数据,key存放索引字段,value就存储行数据或地址。hash结构通过index获取地址的查询效率是非常之高的,时间复杂度是O(1),但是缺点就是无法支持范围查询,所以适用的场景不了,了解一下就好了。
索引类型
首先索引分为两大块
- 聚集索引(也叫主键索引或聚簇索引)
- 非聚集索引(辅助索引、联合索引、全文索引等都是非聚集索引)
为啥这么这么分,且往下看
InnoDB的索引
聚簇索引
#查看一张表中的索引情况
SHOW INDEX FROM table_name;
聚集索引就是我们所说的主键索引,如果没有主键,innodb就会自动创建一个rowId来构建聚簇索引,平时也许我们都知道主键索引查询效率最高,但是这又是为啥呢?首先innodb才有聚集索引(myisam没有真正意义上的聚集索引,具体后面会说到)。看这张图。
当我们要查询36的时候,首先进行两次io,查找到了这个id所在的位置,B+树的叶子节点可以直接存放数据,所以就直接定位到id所对应的行数据。
IO总次数:3次
辅助索引
#创建辅助索引
ALTER TABLE table_name ADD INDEX index_name (column);
这次用非主键字段来查,可以看到辅助索引的叶子节点只会存放id,然后根据指针再去主键索引中查到对应的行数据(这里的步骤和上面的主键索引查询步骤一样了)。 IO次数:最多6次
拓展点: 这个重新根据主键id去查询行数据的行为被称为回表查询。所以知道为啥叫辅助了,辅助就是先找到大哥(主键),再根据主键去查到最终想要的数据。(不准抢人头)。当然这个也有优化的方案,后面会说。
说到这里可以再点一下题,上面这样分的原因。方便大家记住,innodb下主键索引和非主键索引的主要区别就在于这里,主键索引直接可以拿到行数据,而非主键索引都需要回表,所以会增加Io次数,这就说明了为什么主键索引的速度最快。
联合索引
#创建联合索引
ALTER TABLE table_name ADD INDEX index_name(column1,column2, column3);
当我们的where语句后面跟着的条件有多个的时候,就要用到联合索引了,多个查询字段组合在一起的索引被称为联合索引,先看下联合索引的结构和查询路径
IO次数:最多6次。
拓展点:最左匹配原则。 联合索引是怎么进行排序的呢?根据你联合索引的数量,比如abc三个索引,先根据a排序,在a相等的情况下,再根据b排序,a、b都相等的情况下,再根据c排序,当你建立联合索引(a、b、c)的同时,等价于建立了(a)(a、b)(a、b、c)三个索引,所以当where后面的条件要使联合索引生效,条件必须根据这个顺序来,即必须要有a,再有b,再有c,如果哪个索引漏了,那后面的条件就都失效了。为什么直接来个b、c就不生效了呢?你想啊,我的排序是要从a开始的,只有保障了a的有序才能查b,单看b的排序是杂乱无章,那你直接给我塞个我应该从哪里走?只能全表扫描了呀。所以索引也就失效了。 但是你说我a、b换下位置可不可以,比如select * from table where a=xx and b=xx或者select * from table where b=xx and a=xx,这样是可以的,mysql没这么傻这样就认不出来了,查询优化器会帮你把位置调整过来的.
覆盖索引
上面说到,非主键索引的查询都会有一次回表,那有办法让他不回表吗,可以,那就是查询的同时将索引的值带入select后面的字段中,这样你查询的行数据就直接再你的条件中了呀,还回啥表。
注意,这种场景其实不常用,很多查询场景下其实我们需要很多字段的,不可能将所有的字段都设置索引,所以还是要分场景,不要为了覆盖而搞了一大堆索引,索引也是有成本的,有写入成本,存储成本。只有所需字段比较少且是热点数据的情况下,才可能需要这么设置。
以上,索引的基本内容就介绍完了,下面再说一下和索引有密切关系的一个内容。
myisam的索引
刚刚在说主键索引的时候,提到了innodb和myiSam。 它们的索引区别就在于myIsam可以理解为只有辅助索引,因为它的叶子节点是不存放数据的,而是只存放数据的指针。 看图:
IO次数:4次。 这里和innodb的辅助索引还是有一丢丢区别!它拿到指针后直接去磁盘拿到对应的数据,没有回表了,但是还是多一次io。所以查询的时候效率会相对来说低一些。(但是他们都是B+树,有些人会搞混,觉得myiSam不存在数据就不是B+树,是不是B+树取决于你的叶子节点是否只存放指针)。
它的其他索引就不画了,因为不管是主键索引还是辅助索引。查询路径和这个一样,都是找到地址后再去查一遍。
myisam不支持主键索引不代表它就不能设置主键索引了!myisam的也有主键索引,但是主要是用来标示字段唯一,刚刚也提到,它没有真正意义上的聚集索引,是因为它除了唯一性以外查询路径和结构基本一样,不要搞浑说它没主键索。
存储引擎
本期内容我们只关注存储文件区别,这和我们的上面说到的他们的索引区别有关系
先说myIsam和innodb索引文件上的区别。 myIsam的表有三个文件: - tableName.frm:表结构文件 - tableName.MYD:数据文件(MyISAM Data) - tableName.MYI:索引文件(MyISAM Index)
Innodb的表有两个文件: - tableName.frm:表结构文件 - tableName.ibd:索引和数据文件(InnoDB Data)。
从这个物理文件的区别也就可以看出为什么myIsam的没有主键索引,因为它的索引和数据物理文件就是分开的,拿到地址必须再去数据文件中获取具体的行数据内容。
索引的管理
索引的原理探究到此结束,这部分内容堪称最难啃的骨头。不过,能坚持读下来的朋友,你的收获也一定良多。接下来的内容就轻松愉悦多了。
索引的优化
上面的索引基本都介绍完了,那是不是无脑上索引就可以了,万事大吉。万万不可! 首先索引会带来两个缺陷:
- 刚刚提到不同存储引擎的索引文件不一样的,但是他们都是需要占用物磁盘理空间的,每一条数据的增加都是增加一个索引,随意的添加索引导致磁盘空间的过度占用。
- 因为每次的写入和删除都会调整索引,所以在删写频繁的时候也会影响到吞吐量,提升db的负荷。
其次:索引也会存在很多建立了之后不生效的情况,除了上述说到的最左匹配,还有诸如以下
- 隐式转换-查询条件的数据类型和字段的数据类型不匹配。
- 查询条件有is null、is not null 不走索引。
- 查询条件是用函数或计算操作。比如concat(‘jingxi’,1)不走索引;
- or条件,任意一个or连接的条件没有索引,就会失效。
- 查询优化器的成本计算导致不走索引。
- 这些原因有部分通过上面的分析应该可以推断出原因,这里就不细说了比较固定,可以通过explan去查看这条sql是否走了索引,走了什么索引。
下面是关于索引优化的几条建议
- 避免过度索引:过多的索引会增加数据库的存储和维护成本,同时也会影响数据库的性能。因此,在建立索引时,应该根据实际查询场景进行决定,尽量避免过度索引。
- 建立复合索引:对于经常需要同时查询多个列的语句,建立复合索引可以有效地提高查询效率。需要注意的是,复合索引的建立顺序需要根据实际查询场景进行决定。
- 使用覆盖索引:覆盖索引是指查询语句只需要使用索引就可以获取需要的数据,而不需要回表操作。覆盖索引可以减少MySQL的I/O操作,从而提高查询效率。
- 避免使用函数在索引列上进行计算:在查询语句中,应尽量避免在索引列上使用函数进行计算,因为这样会使MySQL无法使用索引,而需要进行全表扫描。
- 定期维护索引:定期维护索引可以清理无用的索引,以保证数据库的正常运行。例如,可以使用OPTIMIZE TABLE语句来对表进行优化,以清理无用的索引和碎片。
索引优化案例
下面,我将通过一个具体案例来说明如何优化MySQL索引。
假设我们有一个用户表,包含用户的ID、姓名、年龄、性别、所在城市、注册时间等信息。现在,我们需要查询年龄大于20岁、所在城市为北京、注册时间在2019年以后的用户。
首先,我们可以通过以下语句来查询这些用户:
SELECT * FROM users WHERE age > 20 AND city = '北京' AND reg_time >= '2019-01-01';
接下来,我们需要对这个查询语句进行优化。首先,我们可以对age和city列建立复合索引,以避免回表操作。同时,由于我们需要查询注册时间在2019年以后的用户,因此我们还需要对reg_time列建立单独的索引。
ALTER TABLE users ADD INDEX idx_age_city (age, city);
ALTER TABLE users ADD INDEX idx_reg_time (reg_time);
建立完索引后,我们可以再次执行查询语句,查看查询性能的提升情况:
SELECT * FROM users WHERE age > 20 AND city = '北京' AND reg_time >= '2019-01-01';
在执行上述查询语句时,MySQL将使用idx_age_city和idx_reg_time索引进行查询,并避免回表操作,从而大大提高查询效率。希望这个具体案例能够帮助你更好地理解MySQL索引优化。
索引的查看
查看索引的语法格式如下:
SHOW INDEX FROM <表名>
查询结果说明如下:
创建索引的方式有三种:
create index直接创建
可以使用专门用于创建索引的 CREATE INDEX 语句在一个已有的表上创建索引,但该语句不能创建主键。
CREATE <索引名> ON <表名> (<列名> [<长度>] [ ASC | DESC])
语法说明如下:
- <索引名>:指定索引名。一个表可以创建多个索引,但每个索引在该表中的名称是唯一的。
- <表名>:指定要创建索引的表名。
- <列名>:指定要创建索引的列名。通常可以考虑将查询语句中在 JOIN 子句和 WHERE 子句里经常出现的列作为索引列。
- <长度>:可选项。指定使用列前的 length 个字符来创建索引。使用列的一部分创建索引有利于减小索引文件的大小,节省索引列所占的空间。在某些情况下,只能对列的前缀进行索引。索引列的长度有一个最大上限 255 个字节(MyISAM 和 InnoDB 表的最大上限为 1000 个字节),如果索引列的长度超过了这个上限,就只能用列的前缀进行索引。另外,BLOB 或 TEXT 类型的列也必须使用前缀索引。
- ASC|DESC:可选项。ASC指定索引按照升序来排列,DESC指定索引按照降序来排列,默认为ASC。
例如,在student表name字段上创建索引:
#普通索引
CREATE INDEX index_name ON student (name);
#唯一索引
CREATE UNIQUE index_name ON student (name);
创建普通索引使用的关键字,例如在student表name字段上创建一个普通索引index_name
#建表创建
CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));
#ALTER TABLE
ALTER student ADD INDEX index_name (name)
CREATE TABLE时创建
索引也可以在创建表(CREATE TABLE)的同时创建。在 CREATE TABLE 语句中添加以下语句。例如创建student表时在name字段添加索引:
#主键索引
CREATE TABLE student(name CHAR(45) PRIMARY KEY);
#唯一索引
CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,UNIQUE INDEX(name));
#普通索引
CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));
ALTER TABLE时创建
ALTER TABLE 语句也可以在一个已有的表上创建索引。例如在student表name字段上创建一个普通索引index_name:
#主键索引
ALTER TABLE student ADD PRIMARY KEY (name);
#唯一索引
ALTER TABLE student ADD UNIQUE INDEX index_name(name);
#普通索引
ALTER TABLE student ADD INDEX index_name(name);