目录
一、索引是什么?
二、索引的种类
2.1、通过主键索引查询
2.2、通过二级索引查询
2.3、为什么 MySQL InnoDB 选择 B + Tree 作为索引的数据结构?
2.4、部分索引规则
三、什么时候不需要索引
3.1、索引的缺点
3.2、什么适合用索引
3.3、什么时候不适合用索引
四、如何优化索引
4.1、前缀索引优化
4.2、覆盖索引优化
4.3、主键索引最好是自增的
4.4、索引最好设置为 not null
4.5、索引失效
4.6、防止索引失效
五、从数据页的角度看 B + Tree
5.1、InnoDB 是如何存储数据的?
5.2、重点说下数据页中的用户记录(User Records)
5.3、页目录创建过程如下:
六、为什么有人说MySQL单表记录不能超过2000w呢?
先举个:在我们看书的时候,找到我们想看的章节,是直接翻书更快还是先找目录更快呢?当然是查看目录更加快速啦~同样的我们书中的目录是不是多用了几张纸?所以索引是以空间换时间的设计思想。切换到数据库中,索引就是帮助存储引擎快速获取数据的一种数据结构(索引就是数据的目录)。
在创建表时, InnoDB 会根据不同的场景选择不同的列作为索引:
其他索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B + Tree 索引。
B + Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按照主键的顺序存放的。所有节点按照索引键大小排序,构成双向链表,便于范围查询。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成双向链表。下面我直接举个:
当我们需要查询 id 号为 5 的物品,查询的过程是这样子的, B + Tree 会自顶向下逐层查找:
数据库的索引和数据都是存储在硬盘之中的,我们把读取一个节点当作一次磁盘的 I / O 操作。上述的过程中一共经历了 3 个节点,也就是进行了 3 次 I / O 操作。而 B + Tree 存储千万级的数据只需要 3 - 4 层高度就可以满足,这也就是说即使在千万级的查询目标数据也只需要 3 - 4 次磁盘 I/O操作。所以, B + Tree 相比于B 树和二叉树来说,最大的优势在于查询的效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I / O 依然维持在 3 - 4 次。
我们在这里将上图除 id 节点之外的任一节点设置为二级索引,那么通过二级索引进行查找,会先检索二级索引中的 B + Tree 的索引值,找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B + Tree 才能查找到数据,这个操作也叫做回表,也就说要查找两个 B + Tree 才能找到数据。如果此时我们所想要查找的数据在二级索引的 B + Tree 的叶子节点里面能够查询到,就不用再去主键索引查了(也就是不用回表操作)。
①、B + Tree VS B Tree
②、B + Tree VS 二叉树
③、B + Tree VS Hash
(这一套组合PK下来发现还是 B + Tree 要更加腻害呀~)
联合索引的最左匹配原则:在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询的字段后面的字段无法用到联合索引。注意,对于 >=、<=、between、like 前缀匹配的范围查询,并不会停止匹配。
索引下推:在 MySQL 5.6 中引入了索引下推优化,可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回标次数。
索引区分度:建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,在建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。(区分度 = 某个字段不同值的个数 / 表的总行数)在MySQL中有一个查询优化器,当它发现某个值出现在表的数据行中的百分比(一般百分比界限是 30%)很高的时候,一般会忽略索引,进行全表扫描。
联合索引排序:
select * from order where status = 1 order by create_time asc
如何提高效率呢?一个方式就是将 status 和 create_time 建立索引,如果只对 status 建立索引这条语句还是要对 create_time 进行文件排序(filesort),此时我们利用索引的有序性建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序,提高查询效率。
使用某个字段中字符串的前几个字符建立索引,使用前缀索引是为了减少索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些长字符串的字段作为索引的时,使用前缀索引可以帮助我们减小索引项的大小。
局限性:order by 就无法使用前缀索引,无法把前缀索引用作覆盖索引
覆盖索引是指 SQL 中 query 的所有字段,在索引 B + Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表操作(innodb二级索引(非聚簇索引)除了存储id外还是存储了对应字段的数据的,所以覆盖索引不需要回表)
索引值存在 null 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可以为 null 的列会使索引、索引统计和值都更加复杂,比如进行索引统计的时候,count 会忽略值为 null 的行
null 值是一个没有意义的值,会占用物理空间,带来存储空间的问题。
对索引使用左或者左右模糊匹配:也就是使用 like %xx 或者 like %xx% 都会造成索引失效。
原因:B + Tree 是按照【索引值】进行有序排列存储的,只能根据前缀进行比较,如使用左或者左右模糊匹配就会导致前缀不明,无法进行比较。
对索引使用函数或者表达式:使用 length 等函数或者是表达式(num + 1 = 9)
原因:索引保存的是索引字段的原始值,而不是函数或者表达式计算的值,所以无法走索引
对索引隐式类型转换:索引字段是字符串类型,但是在条件查询中输入的参数是整形,就会发现索引失效
原因:MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。
联合索引非最左匹配:多个普通字段需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,如果不遵守会导致索引失效
原因:在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中最左边开始连续的列。
where 子句中的 or :在 where 子句中,如果在 or 前面的条件列是索引列,而在 or 后的条件列不是索引列,那么索引会失效
原因:or 的含义是两个只要满足一个条件即可,因此只有一个条件列是索引列是没有意义的,只要有条件不是索引列,就会进行全表扫描
常见扫描类型的执行效率从低到高的顺序为:
从 range 级别开始,索引的作用会越来越明显,因此应尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
InnoDB 的数据是按数据页为单位来读写的,也就是说,当需要读一条记录时,并不是将这个记录本身从磁盘中读出来,而是以页为单位,将其整体读入内存中。数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16 KB ,意味着数据库每次读写都是以 16KB 为单位的。
数据页长这样子,如图:
各自作用如下:
在文件头中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下:
数据页之间采用了链表的结构,不需要是物理上的连续,而是逻辑上的连续。
数据页中的记录按照【主键】顺序组成单向链表,单向链表的特点就是插入删除非常方便,但是查找效率不高,最差的情况需要遍历链表上的所有节点才能完成查询。因此数据页中有一个页目录(Page Directory),起到记录的索引作用,就像我们书一样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应章节的页数,而数据页中的页目录就是为了能快速找到记录。
页目录的与记录的关系如下:
页目录就是由多个槽组成的,槽相当于分组记录的索引。由于记录是按照从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽,定位到槽之后,再遍历槽中所有记录,找到相应的记录,无需从最小记录开始遍历整个页中的记录链表。
以上图举个:5个槽的编号分别是0,1,2,3,4,我想查找到主键为 11 的用户记录:
如果某个槽的记录很多,且都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是O(n)了嘛?
原因是在MySQL中,索引是存放在内存之中的,如果索引占的内存超过了 Buffer Pool Size(默认为128 MB),会导致之后的查询产生大量 I/O 操作导致性能下降,如果能够增加硬件比如128T的内存,就能够把索引全部加载进内存,带来立竿见影的效果。