MySQL索引篇

目录

一、索引是什么?

二、索引的种类

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呢?


一、索引是什么?

先举个:在我们看书的时候,找到我们想看的章节,是直接翻书更快还是先找目录更快呢?当然是查看目录更加快速啦~同样的我们书中的目录是不是多用了几张纸?所以索引是以空间换时间的设计思想。切换到数据库中,索引就是帮助存储引擎快速获取数据的一种数据结构(索引就是数据的目录)。

二、索引的种类

  1. 按【数据结构】分类:B + Tree 索引、Hash 索引、 Full - test 索引
  2. 按【物理存储】分类:聚簇索引(主键索引)、二级索引(辅助索引)
  3. 按【字段特性】分类:主键索引、唯一索引、普通索引、前缀索引
  4. 按【字段个数】分类:单列索引、联合索引

在创建表时, InnoDB 会根据不同的场景选择不同的列作为索引:

  1. 如果有主键,默认会使用逐渐作为聚簇索引的索引键(key)
  2. 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key)
  3. 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列(一行记录中的row_id)作为聚簇索引的索引键

其他索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B + Tree 索引。

B + Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按照主键的顺序存放的。所有节点按照索引键大小排序,构成双向链表,便于范围查询。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成双向链表。下面我直接举个:

MySQL索引篇_第1张图片

2.1、通过主键索引查询

当我们需要查询 id 号为 5 的物品,查询的过程是这样子的, B + Tree 会自顶向下逐层查找:

  1. 将 5 与根节点的索引数据(1,10,20)比较,5 在 1 和 10 之间,所以按照 B + Tree 的搜索逻辑,找到第二层的索引数据(1,4,7)。
  2. 在第二层的索引数据(1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6)。
  3. 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。

数据库的索引和数据都是存储在硬盘之中的,我们把读取一个节点当作一次磁盘的 I / O 操作。上述的过程中一共经历了 3 个节点,也就是进行了 3 次 I / O 操作。而 B + Tree 存储千万级的数据只需要 3 - 4 层高度就可以满足,这也就是说即使在千万级的查询目标数据也只需要 3 - 4 次磁盘 I/O操作。所以, B + Tree 相比于B 树和二叉树来说,最大的优势在于查询的效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I / O 依然维持在 3 - 4 次。

 2.2、通过二级索引查询

  1. 主键索引的 B + Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的    B + Tree 的叶子节点里。
  2. 二级索引的 B + Tree 的叶子节点存放的是主键值,而不是实际数据。

我们在这里将上图除 id 节点之外的任一节点设置为二级索引,那么通过二级索引进行查找,会先检索二级索引中的 B + Tree 的索引值,找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B + Tree 才能查找到数据,这个操作也叫做回表,也就说要查找两个 B + Tree 才能找到数据。如果此时我们所想要查找的数据在二级索引的 B + Tree 的叶子节点里面能够查询到,就不用再去主键索引查了(也就是不用回表操作)。

2.3、为什么 MySQL InnoDB 选择 B + Tree 作为索引的数据结构?

①、B + Tree VS B Tree

  1.         B + Tree 只在叶子节点存储数据,而 B 树的非叶子节点也要存储数据,所以 B + Tree的单个节点数据量更小,在相同的磁盘 I / O 次数下,能够查询到更多的节点。也由于 B + 树的非叶子节点可以存放更多的索引,因此 B + Tree 可以比 B Tree 更加矮胖,查询节点的磁盘 I/O 会更多
  2.         B + Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围查找的顺序查找,而 B Tree 只能通过遍历树完成范围查询,会涉及更多节点的磁盘 I/O 操作。

②、B + Tree VS 二叉树

  1.         二叉树的高度会随着数据量增加而增加,I / O操作会不断增加,并且可能退化为链表,查询复杂度会由 log n 降为 n ,而 B + Tree 即使在千万级数据量下也能保持在 3 ~ 4 层之中。
  2.         由于二叉树的父节点只能是两个,意味着其搜索复杂度为 O(log N),相比 B + Tree 高出不少。

③、B + Tree VS Hash

  1.         Hash 在做等值查询的效率很高,搜索时间复杂度为 O(1)
  2.         Hash 无法做范围查询
  3.         由于 Hash 不是按照索引值顺序存储的,就不能像 B + Tree 索引一样利用索引完成排序
  4.         如果 Hash 中有大量重复键值的情况下,会存在 Hash 碰撞的问题,导致 Hash 索引效率降低

(这一套组合PK下来发现还是 B + Tree 要更加腻害呀~)

2.4、部分索引规则

联合索引的最左匹配原则:在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询的字段后面的字段无法用到联合索引。注意,对于 >=、<=、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 排好序的,避免在文件排序,提高查询效率。

三、什么时候不需要索引

3.1、索引的缺点

  1. 需要占用物理空间,数量越大,占用空间越大
  2. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大
  3. 会降低表中 增删改 的效率,因为每次 增删改 时,B+树为了维护索引有效性都需要进行动态维护

3.2、什么适合用索引

  1. 字段有唯一性限制【比如身份证号,每个人只有一个吧?】
  2. 经常使用 where 查询条件的字段,这样就能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引
  3. 经常用于 group by 和 order by 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们已经知道建立索引之后在 B + Tree 中的记录是排好序的。

3.3、什么时候不适合用索引

  1. where 条件, group by ,order by 里用不到的字段,索引的价值是快速定位,如果起不到定位作用的字段通常是不需要创建索引的,因为索引是会占用物理内存空间的。
  2. 字段中存在大量重复的数据,不需要建立索引。比如性别字段,只有男女,无论怎么搜索都会得到一半数据
  3. 表数据太少的时候,不需要创建索引
  4. 经常更新的字段不用创建索引,比如我们使用的支付宝和微信的余额,会经常被修改,由于要维护 B + Tree 的有序性,就会需要频繁的重建索引,资源消耗大,对性能影响巨大。

四、如何优化索引

4.1、前缀索引优化

使用某个字段中字符串的前几个字符建立索引,使用前缀索引是为了减少索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些长字符串的字段作为索引的时,使用前缀索引可以帮助我们减小索引项的大小。

局限性:order by 就无法使用前缀索引,无法把前缀索引用作覆盖索引

4.2、覆盖索引优化

覆盖索引是指 SQL 中 query 的所有字段,在索引 B + Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表操作(innodb二级索引(非聚簇索引)除了存储id外还是存储了对应字段的数据的,所以覆盖索引不需要回表)

4.3、主键索引最好是自增的

  1. InnoDB 创建主键索引默认为聚簇索引,数据被存放在了 B + Tree 的叶子节点中,同一个叶子节点内的各个数据都是按主键顺序存放的,因此当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。
  2. 如果使用自增主键,每次插入数据的时候就会按顺序添加到当前索引节点的位置,不需要移动现有数据,当页面写满的时候,会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此此种插入效率非常高。
  3. 如果使用非自增主键,每次插入数据的主键都是随机的,因此每次插入数据时可能会插入到现有数据页中间的某个位置,从而不得不移动其他数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,通常称这种现象为 页分裂 。页分裂还有可能会造成大量的内存碎片,导致结构不紧凑,从而影响查询效率。另外,主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。

4.4、索引最好设置为 not null

索引值存在 null 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可以为 null 的列会使索引、索引统计和值都更加复杂,比如进行索引统计的时候,count 会忽略值为 null 的行

null 值是一个没有意义的值,会占用物理空间,带来存储空间的问题。

4.5、索引失效

对索引使用左或者左右模糊匹配:也就是使用 like %xx 或者 like %xx% 都会造成索引失效。

原因:B + Tree 是按照【索引值】进行有序排列存储的,只能根据前缀进行比较,如使用左或者左右模糊匹配就会导致前缀不明,无法进行比较。

对索引使用函数或者表达式:使用 length 等函数或者是表达式(num + 1 = 9)

原因:索引保存的是索引字段的原始值,而不是函数或者表达式计算的值,所以无法走索引

对索引隐式类型转换:索引字段是字符串类型,但是在条件查询中输入的参数是整形,就会发现索引失效

原因:MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。

联合索引非最左匹配:多个普通字段需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,如果不遵守会导致索引失效

原因:在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中最左边开始连续的列。

where 子句中的 or :在 where 子句中,如果在 or 前面的条件列是索引列,而在 or 后的条件列不是索引列,那么索引会失效

原因:or 的含义是两个只要满足一个条件即可,因此只有一个条件列是索引列是没有意义的,只要有条件不是索引列,就会进行全表扫描

4.6、防止索引失效

  1. 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者like %xx% 这两种方式都会造成索引失效
  2. 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况都会造成索引失效
  3. 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效
  4. 在 where 子句中,如果在 or 前的条件列是索引列,而在 or 后面的条件列不是索引列,那么索引就会失效

常见扫描类型的执行效率从低到高的顺序为:

  1. All(全表扫描)
  2. index(全索引扫描)
  3. range(索引范围扫描)
  4. ref(非唯一索引扫描)
  5. eq_ref(唯一索引扫描)
  6. const(结果只有一条的主键或唯一索引扫描)

从 range 级别开始,索引的作用会越来越明显,因此应尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。

五、从数据页的角度看 B + Tree

5.1、InnoDB 是如何存储数据的?

InnoDB 的数据是按数据页为单位来读写的,也就是说,当需要读一条记录时,并不是将这个记录本身从磁盘中读出来,而是以页为单位,将其整体读入内存中。数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16 KB ,意味着数据库每次读写都是以 16KB 为单位的。

数据页长这样子,如图:

MySQL索引篇_第2张图片

各自作用如下:

  1. 文件头:表示页的信息
  2. 页头:表示页的状态信息
  3. 最小和最大记录:两个虚拟的伪记录,分别表示页中的最小记录和最大记录
  4. 用户记录:存储行记录内容
  5. 空闲空间:页中还没被使用的空间
  6. 页目录:存储用户记录的相对位置,对记录起到索引作用
  7. 文件尾:校验页是否完整

在文件头中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下:

MySQL索引篇_第3张图片

数据页之间采用了链表的结构,不需要是物理上的连续,而是逻辑上的连续。

5.2、重点说下数据页中的用户记录(User Records)

数据页中的记录按照【主键】顺序组成单向链表,单向链表的特点就是插入删除非常方便,但是查找效率不高,最差的情况需要遍历链表上的所有节点才能完成查询。因此数据页中有一个页目录(Page Directory),起到记录的索引作用,就像我们书一样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应章节的页数,而数据页中的页目录就是为了能快速找到记录。

页目录的与记录的关系如下:

MySQL索引篇_第4张图片

5.3、页目录创建过程如下:

  1. 先将所有的记录分组,这些记录的最小记录和最大记录,但是不包括标记为“已删除”的记录
  2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为  n_owned 字段(图中粉红色字段)
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称为槽(slot),每个槽相当于指针指向了不同组的最后一个记录

页目录就是由多个槽组成的,槽相当于分组记录的索引。由于记录是按照从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽,定位到槽之后,再遍历槽中所有记录,找到相应的记录,无需从最小记录开始遍历整个页中的记录链表。

以上图举个:5个槽的编号分别是0,1,2,3,4,我想查找到主键为 11 的用户记录:

  1. 先二分得出槽中间位是(0 + 4)/ 2 = 2,而二号槽中最大记录为 8 ,因为 11 > 8,所以需要从 2 号槽后继续搜索记录
  2. 再使用二分搜索(2 + 4)/ 2 = 3,3 号槽最大的记录是12,因为11 < 12 所以主键为 11 的记录在3号槽中
  3. 槽对应的值都是这个组的主键最大记录,如何找到组里最小的记录呢?解决办法是找到上一个槽,然后通过上一个槽找到本组最小记录。

如果某个槽的记录很多,且都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是O(n)了嘛?

  1. InnoDB 规定了第一个分组只能有一条记录
  2. 最后一个分组的记录条数范围只能在 1 - 8 条之间
  3. 剩下分组中记录条数只能在 4 - 8 条

六、为什么有人说MySQL单表记录不能超过2000w呢?

原因是在MySQL中,索引是存放在内存之中的,如果索引占的内存超过了 Buffer Pool Size(默认为128 MB),会导致之后的查询产生大量 I/O 操作导致性能下降,如果能够增加硬件比如128T的内存,就能够把索引全部加载进内存,带来立竿见影的效果。

你可能感兴趣的:(mysql,数据库)