目录
概念
官方定义:索引(Index)是帮助MySQL高效获取数据的数据结构。
在数据库中,索引被定义为一种特殊的数据结构,由数据库中的一列或多列组合而成,可以用来快速查询数据表中某一特定值的记录,就像一本书的目录一样。索引是在表的字段的基础上建立的一种数据库对象。
优缺点
1、优点
1)减少磁盘的IO次数,提高查找效率,这是索引的核心优势
2)通过创建唯一索引,可以保证表中每行数据记录的唯一性
3)在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。
2、缺点
1)占用磁盘空间:创建索引需要占据存储空间,如果表中数据量巨大,索引数量多,索引文件可能比数据本身的文件会更快到达硬件限制。
2)降低表数据更新速度:增删改数据场景,索引会影响更新效率,因为在增删改数据时,不仅要修改数据本身,而且要对数据关联的索引进行重建,会影响更新表的整体速度
索引并不是越多越好,我们要根据具体业务场景合理创建索引,当我们更新超大表数据时,最好的办法是先删除表中的索引,然后插入数据,插入成功后重建索引。
索引的数据结构
能作为索引的数据结构可以有很多,但是mysql中选择了B+树,那为什么使用B+树而不是二叉查找树、平衡二叉树或者B树等其他数据结构呢?下面一一看下各个数据结构的特点就知道了。
Hash索引
哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的键即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的有很多方法,工程上最常用的是,拉出一个链表。假设,你现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这时对应的哈希索引的示意图如下所示:
所以,哈希表这种结构适用于只有等值查询的场景。
问题:不支持范围查询,有时候会有hash冲突问题。
有序数组索引
而有序数组在等值查询和范围查询场景中的性能就都非常优秀。还是上面这个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示:
这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。这时候如果你要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。
如果只看查询效率,有序数组就是最好的选择。
问题:当需要进行更新数据时,需要将后面的数据全部后移,效率低,有序数组只适合做静态查询。
二叉搜索树
二叉搜索树是二叉树的一种,根节点/父节点的左子树上的节点都比根节点/父节点小,而右子树上的节点都比根节点/父节点要大。
整棵树根据ID进行排序,左子树的ID值 < 节点ID值 < 右子树的ID值,这样当我们查找 id:450 时,就可以得到对比路径
500 -> 400 -> 450 比较三次即可,时间复杂度为O(log(N)); 插入操作,也只需要查找到叶子节点根据规则,插入一个节点即可,非常方便。 但是这种结构也不是最优的,当我们节点插入顺序不合理的时候,就会出现一边倒的情况,比如下面这种情况,此时的查找就和普通的顺序查找基本一样了。
因为每个节点有两个子节点,数据量的增大必然导致高度的快速增加,对那种逐渐增大的数据查询相当于链表查询,效率低下,显然这个不适合作为大量数据存储的基础结构。
问题:数据库的索引不止存在于内存中,还要写在磁盘上,每次查询都要去访问数据块,进行一次磁盘I/O,当存储大数据量时,树的高度很高,查询还是慢;有些情况可能退化为链表。
平衡二叉树
平衡二叉树是是二叉搜索树的一种拓展,要求左右两颗子树树高差值不能超过1。 因此当新插入的数据破坏了树的平衡(左右两颗子树树高差值大于1),就要通过内部算法LL操作,LR操作等,对树的节点进行重新排列进而达到新的平衡,这里具体的实现细节我们就不再深究了。
平衡二叉树解决上面二叉树可能退化为链表的问题,但是没有解决在大数据量时,树的高度很高的问题。
B树
为了降低树的高度,减少访问磁盘的次数,人们又提出了新的数据结构,B树,也叫B-树,还叫多路平衡查找树,通过名字就能够看出来,B树和平衡二叉树,就是树的分叉数不一样而已,其他的都是一样的。
B树特点:
- B树的节点中存储着多个元素,每个内节点有多个分叉。
- 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据。
- 父节点当中的元素不会出现在子节点中。
- 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。
B树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,高度也会很低,子节点的个数一般称为阶。B树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。
问题:
- 范围查询非常缓慢。B树不支持范围查询的快速查找,你想想这么一个情况如果我们想要查找10和35之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
- 因为所以节点都存储数据,所有当单行数据比较大时,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大。
B+树
B+树,是B树的升级版,改进点在于,B树的非叶子节点和叶子节点都会存储数据,但是B+树只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。
因为B+Tree只在叶子节点存储数据,所以B+Tree的单个节点的数据量更小,只存储索引,可以放更多的索引,在相同的磁盘I/O次数下,就能查询更多的节点。另外,B+Tree叶子节点采用的是双指针连接,适合MySQL中常见的基于范围的顺序查找。
一个节点可以放16kb,1170个元素,1170117016=2千多万,高度为3,即使是2千万数据,B+树的高度为3,这样我们查找数据的效率会比较高,高度是由非叶子节点能放多少个索引元素决定的。
没有有特别说明,默认都是使用B+树结构组织的索引。
索引的物理结构
索引的物理存储结构,在不同的存储引擎中有着不同的实现方式,这里我们重点介绍两个存储引擎的存储方式,也是最常用的两个存储引擎,一个是MyISAM存储引擎,一个是InnoDB存储引擎。
MyISAM存储引擎
MyISAM存储引擎对于一个表的存储是分为三个文件存储在硬盘上,对于一个user表,分别是
-
- user.frm : 表结构文件
- user.MYD : 数据文件
- user.MYI : 索引文件
可以发现,MyISAM存储引擎,将一个表的索引和数据分开存在了不同的文件中。那么具体的情况是什么样的呢?
- 主键索引
如下图,ID列为主键索引。
可以看到非叶子节点存储键值,叶子节点存储键值以及指向该主键所对应的数据的地址指针,这也是为什么MyISAM存储引擎,将索引和数据存储在不同文件的原因。每次索引查询,当MyISAM查询,找到对应的叶子节点时,根据该对应的地址指针,再去数据文件找到对应的数据。
- 非主键索引
MySIAM的非主键索引的存储结构我们可以发现,是和主键索引结构完全一样的,没有什么区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而非主键索引的键值可以重复。
查询数据时,由于非主键索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在非主键索引树中检索数据。
InnoDB存储引擎
InnoDB存储引擎对于一个表的存储是分为两个文件存储在硬盘上,对于一个user表,分别是
-
- user.frm : 表结构文件
- user.ibd : 索引和数据文件
可以发现,InnoDB存储引擎,将一个表的索引和数据分开存在了相同的文件中。那么具体的情况是什么样的呢?
- 主键索引
再InnoDB中,主键索引也就是聚簇索引。每个InnoDB表都有一个聚簇索引 ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下:
- 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引。
- 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。
- 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。
InnoDB存储引擎中,非叶子节点存储的是键值,也就是主键。而叶子节点,则存储的是键值和对应的该行数据!在InnoDB存储引擎的主键索引中,是将数据和键值放在一起的,这也是为什么索引和数据存在一个文件中的原因。
- 非主键索引
除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。
以表user的age列为例,age索引的索引结果如下图:
我们能够发现,非叶子节点一样的只是存储键值,到了叶子节点后,存储的是键值以及对应的主键的值. 所以,也正是基于这个原因,InnoDB存储引擎,在普通索引上查找到数据后,还要根据得到的主键值,到主键索引上再进行一次查找,才能找到对应的数据。
例如查询age=19的数据,第一步先从辅助索引查找主键,找到主键为47,然后根据主键47查找行数据,示意图如下:
上面这种根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询。
- 联合索引
联合索引又叫复合索引,例如下表:
CREATE TABLE `test` (`id` bigint NOT NULL AUTO_INCREMENT,`name` varchar(24) NOT NULL,`age` int NOT NULL,`position` varchar(32) NOT NULL,`address` varchar(128) NOT NULL,`birthday` date NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
如下索引就是一个联合索引。
`idx_name_age_position` (`name`,`age`,`position`) USING BTREE
联合索引底层数据结构长什么样?
比较相等时,先比较第一列的值,如果相等,再继续比较第二列,以此类推(这也就是最左前缀匹配原则)。
索引的分类
- 按照基本使用分类
- 主键索引: InnoDB 主键是默认的索引,数据列不允许重复,不允许为 NULL,⼀个表只能有⼀个主键。
- 唯⼀索引: 数据列不允许重复,允许为 NULL 值,⼀个表允许多个列创建唯⼀索引。
- 普通索引: 基本的索引类型,没有唯⼀性的限制,允许为 NULL 值。
- 全文索引:只能在文本类型CHAR,VARCHAR,TEXT类型字段上创建全文索引。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。
- 前缀索引:在文本类型如CHAR,VARCHAR,TEXT类列上创建索引时,可以指定索引列的长度,但是数值类型不能指定。
- 空间索引:MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型。MySQL在空间索引这方面遵循OpenGIS几何数据模型规则。
- 按照索引字段个数
- 单列索引:对单独的列 设置一个索引
- 联合索引:对 多个字段 一起建立索引
- 按照物理存储结构
- 聚簇索引:叶子节点上存储了完整的数据记录(完整的数据记录是指存储了单条记录中所有列的值),通过主键找到了索引也就找到了数据。
- 非聚簇索引:也叫二级索引或辅助索引,将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行。
页、区、段
- 页
页是InnoDB存储数据记录的基本单位,也是数据库IO操作的最小单位,页的大小默认是16KB,一个页中存放了多条数据记录。
InnoDB将数据划分为若干个页存放到磁盘上,并且以页
作为磁盘和内存之间交互的基本单位
,也就是说一次IO操作最少会从磁盘中读取16KB的内容到内存中,一次最少会把内存中的16KB内容刷新到磁盘中。从使用角度来说,在InnoDB引擎环境下,不论是读取一行还是多行记录,都是将这些数据记录所在的页进行加载。
- 区
区是比页大一级的存储结构,在InnoDB中,一个区内会分配64个连续的页,因此一个区的大小是64*16KB=1MB。
- 段
段是由至少一个区组成的,在段中不要求区与区之间连续。
段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。 当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
change buffer 和索引
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
整个 change buffer 和 索引有什么关系呢?
如果是唯一索引,在插入的时候,需要对插入的数据进行唯一性判定,就必须将数据从硬盘上读入内存,因此,唯一索引无法应用 change buffer。而对于普通索引,则可以应用 change buffer ,减少 磁盘I/O,这在数据量很大的时候,对性能的影响还是很明显的。因此,在能够使用普通索引的情况下,尽量使用普通索引,不要使用唯一索引。
回表和覆盖索引
前面我们知道,在InnoDB存储引擎中,如果是非主键索引,需要先在非主键索引上查询主键,再去主键索引上查询整行数据,它⽐基于主键索引的查询多扫描了⼀棵索引树,这个过程就叫回表。
而如果执行的是 select ID from T where k between 3 and 5 这时只需要查 ID 的值,且 ID 是主键索引,即索引和值在一起,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 ID已经“覆盖了”我们的查询需求,我们称为覆盖索引。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
索引优化
- 关于建立索引
哪些需要建
1、主键自动建立唯一非空索引
2、频繁作为查询条件的字段
3、查询中与其他表建立关联,外键关系建立索引
4、单值/复合索引选择,在高并发下倾向复合索引
5、查询中排序的字段
6、查询中统计或分组的字段
哪些不建议建索引
1、表记录太少
2、经常增删改的表和更新的字段
3、某列数据种类太少,如性别、一些分类等
4、where条件里用不到的不建立索引
- 尽量不使用!=,not in ,not exists
因为这些操作无法使用索引可能会导致全表扫描 ,!=的结果集可能会很大,走索引也和全表扫描差不多,这样MySQL优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。
- 尽量不使用is null,is not null
然这个字段是索引字段,但是也可能不会走索引,对于null的字段,在索引树中会集中起来处理,统一在左端或者右端。
- like不要以%开头
like以通配符开头(’%Lei’)MySQL索引失效会变成全表扫描操作,%号在前就意味着前面还有很多其他的字符串,跳过这些字符串在整个索引树里面就不是有序的了,定位不到,没办法用索引;%号在后等于是用了%号前面的字符串,这些字符串在整个索引树里面是有序的,所以能走索引。
- 尽量使用覆盖索引
减少 select * 语句。
查询的时候指明具体的字段,尽量被联合索引覆盖掉,如果要查的是全部列的话,数据量又特别大,可以考虑使用搜索引擎。
- 少用or或in
因为用它查询时,MySQL不一定使用索引,MySQL内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。
- 使用最左前缀原则
如果索引了多列,要遵守最左前缀法则,指的是查询从索引的最左前列开始并且不跳过索引中的列。
最左前缀原则是针对联合索引的,它的底层是一个B+树,但键值数是大于1的,而构建一个B+树就只能根据一个键值来进行,所以数据库依据联合索引最左的字段来构建B+树,是按照联合索引从左到右排好序的,如果跳过前面的,这样后面的就不是有序的了,查询就需要全表扫描。
- 不在索引列上做操作
计算、函数等操作可能会导致索引失效而转向全表扫描,比如SQL中截取name左边的三位,这样就不是有序了,走不了索引。
- 不能使用索引中范围条件右边的列
在联合索引中,如果前面索引列使用了范围,那么后面的索引列就走不了索引,这是因为在索引树中前面的是范围,后面的就不一定是有序的。
- order by 优化
order by满足两种情况会使用Using index,order by语句使用索引最左前列;使用where子句与order by子句条件列组合满足索引最左前列,如果order by的条件不在索引列上,就会产生Using filesort。
另外,where与order by冲突时优先where。因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。
面试题
索引哪些情况下会失效
查询条件包含 or,可能导致索引失效
如果字段类型是字符串,where 时⼀定⽤引号括起来,否则会因为隐式类型转换,索引失效
like 通配符可能导致索引失效。
联合索引,查询时的条件列不是联合索引中的第⼀个列,索引失效。
在索引列上使⽤ mysql 的内置函数,索引失效。
对索引列运算(如,+、-、*、/),索引失效。
索引字段上使⽤(!= 或者 < >,not in)时,可能会导致索引失效。
索引字段上使⽤ is null, is not null,可能导致索引失效。
左连接查询或者右连接查询查询关联的字段编码格式不⼀样,可能导致索引失效。
MySQL 优化器估计使⽤全表扫描要⽐使⽤索引快,则不使⽤索引
什么是索引下推
索引条件下推优化 (Index Condition Pushdown (ICP) ) 是 MySQL5.6 添加的,⽤于优化数据查询。
-
不使⽤索引条件下推优化时存储引擎通过索引检索到数据,然后返回给 MySQL Server,MySQL Server 进⾏过滤条件的判断。
-
当使⽤索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL Server 将这⼀部分判断条件下推给存储引擎,然后由存储引擎通过判断索引是否符合 MySQL Server 传递的条件,只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器。
例如⼀张表,建了⼀个联合索引(name, age),查询语句:` select * from t_user where name like ‘张%’
and age=10;` ,由于 name 使⽤了范围查询,根据最左匹配原则:
- 不使⽤ ICP,引擎层查找到 name like ‘张%’ 的数据,再由 Server 层去过滤 age=10 这个条件,这样⼀来,就回表了两次,浪费了联合索引的另外⼀个字段 age 。
- ,使⽤了索引下推优化,把 where 的条件放到了引擎层执⾏,直接根据 name like ‘张%’ and age=10 的条件进⾏过滤,减少了回表的次数。
主键选择自增和uuid的区别
InnoDB存储引擎的最小存储单位是 页,一页有16KB,但插入的索引或者数据等在一个页中满了之后,要调整节点顺序,进行分页分为两个页面,这个过程是比较消耗性能的,而且页面的内存利用率也会下降。自增主键的插入数据模式,正符合了递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
因此一般情况下,我们推荐使用自增主键,但UUID主键也不是完全没有优点,UUID主键在数据非常庞大,而进行分库分表的时候,或者合并表的时候,就很方便,很容易做到一致性。