本节内容如下:
索引是一种提升查询效率得数据结构。
索引数据结构
底层用的有hash表、有序数组、搜索树等。
hash表就是以键值对存储数据,适合等值查找,并不适合范围查找。时间复杂度为O(1)。
啥叫等值查找,就是根据给定得key找出对应的value得过程。
有序数组是把数据按照一定顺序排序好,咱们去查询数据直接用二分法就可以确定值以及范围了。时间复杂度为O(log n)。适合等值查询和范围查询。但是去增加数据就比较麻烦了。
有序数组索引只适用于静态存储引擎,适合那类不会再修改的数据。
搜索树就是二叉搜索平衡树,就解决上述更新数据麻烦得情况,这个是O(log n)。
MySQL得innodb用的是B+树做索引得底层结构。B+树是二叉搜索树的一种。
B+树做底层的索引,分为两种,一种是聚集索引,另一种是非聚集索引。
聚集索引得叶子节点是整行数据(列+行定位信息+行版本控制信息),非聚集索引得叶子节点是主键值。
在聚集索引(Clustered Index)中,非叶子节点(也称为内部节点)的主要作用是构建索引树的层次结构,以便高效地进行数据查找。非叶子节点并不存储完整的数据行记录,而是存储用于导航到叶子节点的信息。(索引键值/主键值+指向子节点指针+边界值)。
非聚集索引得非叶子节点存放啥?给咱聚集索引一样,索引键值/主键值+指向子节点指针+边界值。
B树和B+树
B树,即平衡多路查找树,也称为B-树或B_树,是一种自平衡的树状数据结构。它能够对存储的数据进行高效的查找、插入和删除操作,时间复杂度为O(log n)。 B+树是B树的一种变体,它同样是一种自平衡的树状数据结构,但在结构和用途上与B树有所不同。
B树特点:
子多路查找:B树可以看作是二叉查找树的一种扩展,它允许每个节点有多个子节点。具体来说,一个m阶的B树(即m路B树)的节点至多有m个孩子。
自平衡性:B树通过特定的操作(如分裂和合并)来保持树的平衡,确保所有叶子节点都位于同一层或相邻两层,从而确保查找、插入和删除操作都能在对数时间内完成。
关键字分布:对于非根节点和非叶子节点,其关键字个数k满足ceil(m/2)-1 ≤ k ≤ m-1,即关键字个数至少为m/2的向上取整减1,至多为m-1。叶子节点则包含指向记录(或数据)的指针,以及可能包含的关键字(取决于具体实现)。
应用场景:B树特别适用于读写大块数据的环境,如数据库和文件系统的索引结构。
B+树特点:
所有数据在叶子节点:与B树不同,B+树的所有数据(或记录的指针)都存储在叶子节点中,而非叶子节点仅作为索引使用,不保存实际数据。这使得B+树在查找数据时更加高效,因为所有数据都位于同一层(叶子层)。
叶子节点相连:B+树的叶子节点通过指针相连,形成了一个有序链表。这使得B+树非常适合进行范围查询和遍历操作。
非叶子节点关键字数:对于m阶的B+树,非叶子节点的关键字个数k满足ceil(m/2) ≤ k ≤ m,且这些关键字仅用于索引,指向相应的子树或叶子节点。
插入与删除:B+树的插入和删除操作主要在叶子节点上进行,并通过分裂和合并操作来保持树的平衡。
适用范围
B树适合于需要频繁进行随机访问的场景。
B+树更适合于范围查询和顺序访问,因为它的叶子节点形成了一个有序的链表,便于遍历。
索引维护
B+树为了维持数据得有序性,在插入新值时就要维护。 维护分两种,页分裂和页合并。
页分裂是当前物理页已经满了,要是放咱新插入得值,就得去开辟新的物理页,这样的话,空间利用率就低了,这就是页分裂。
页合并就是相邻两个页删除得值多了,空间利用率低了,然后就合并物理页,让它得空间利用率增大。
索引如何使用?
咱建表是为啥要有自增主键呢?
自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。
也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
什么时候用业务字段做主键呢?
只有一个索引或者该索引是唯一索引。
覆盖索引就是保存了查询所需要的全部数据的索引。
咱通过覆盖索引久能查询到所需要的数据,就不用去回表了,就减少树的搜索次数,从而提升性能了。
最左前缀原则
我理解是在一个字段上建立索引,包含的数据范围太大了,我们需要查询的数据只是其中的一部分,所查询的数据满足某种规律,咱就用最左匹配原则去缩小这个范围。这样咱就不用就扫描整张表了,减少了查询量。也算是优化。
B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
在建立联合索引的时候,如何安排索引内的字段顺序?
第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
比如要查(a,b),同时也要对a,b查询,那就要维护(a,b)、(b)、(a)索引了。
第二原则是空间,对空间较小的单独做一个索引。
比如(a,b),空间a>b,那抹对b做一个单字段索引。
索引下推
在MySQL5.6没索引下推前,咱查询时,从第一个满足条件的记录开始一个个回表,到主键索引上找到数据行,咱去对比字段值,找符合条件的数据。
之后有索引下推了,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
普通索引和唯一索引咋选择呢?
这两类索引在查询能 力上是没差别的,主要考虑的是对更新性能的影响。所以,我建议你尽量选择普通索引。
如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。 而在其他情况下,change buffer 都能提升更新性能。
change buffer 是什么
hange Buffer(变更缓冲区)是InnoDB存储引擎中的一个特性,它是为了解决磁盘I/O性能问题而设计的一种优化机制。在InnoDB中,所有的数据修改(插入、更新和删除操作)都会被记录到日志文件中,同时也会尝试直接修改索引页。然而,如果这些索引页并不在内存中(即不在缓冲池中),那么就需要从磁盘加载这些页到内存,这会带来额外的I/O开销。
Change Buffer的作用就是推迟这种I/O操作。当对非聚集索引(secondary indexes)进行修改时,如果对应的索引页没有在缓冲池中,InnoDB不会立刻将这些页读入内存,而是将这个变更记录到Change Buffer中。Change Buffer是一个位于缓冲池中的结构,它保存了对非聚集索引页的变更信息。等到后来这些页因为其他原因需要加载到内存时,或者是在后台线程有空闲时,InnoDB会合并这些变更到实际的索引页中,从而减少不必要的随机I/O操作。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内 存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的 时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方 式就能保证这个数据逻辑的正确性。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问 这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭 (shutdown)的过程中,也会执行 merge 操作。 显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到 明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占 用内存,提高内存利用率
什么条件下可以使用 change buffer 呢?
唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插 入一个记录,就要先判断现在表中是否已经存在该记录,而这必须要将数据 页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
普通索引和 change buffer 的配合使用,对于数据量大的表的 更新优化还是很明显的。
使用普通索引和唯一索引在插入数据时区别是什么
如果要在这张表中插 入一个新记录 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
对于唯一索引来说,找到适合插入的位置,判断到没有冲突,插入这个值,语句执行 结束; 对于普通索引来说,找到适合插入的位置,插入这个值,语句执行结束
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下: 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结 束; 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。 将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
MySQL选择错索引,导致执行速度变得很慢?你遇到过吗?
在 MySQL 中一张表其实是可以支持多个索引的。写 SQL 语句的时候,并没有主动指定使用哪个索引。也就是说,使用哪个索引是由 MySQL 来确定的。
具体原因是选择索引是在优化器选择的。优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。优化器会结合扫描行数、是否使用临时表、是否排序等因素进行综合判断。
扫描行数是怎么判断的?
MySQL 在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根据统计信息来估算记录数。
这个统计信息就是索引的“区分度”。显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
MySQL 是怎样得到索引的基数的呢?
通过MySQL 采样统计。采样统计的时候,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过 1/M 的时候,会自动触发重新做一次索引统计。
在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择:
- 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。
- 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。
优化器会估算这几个选择的代价,从结果看来,优化器认为直接扫描主键索引更快。当然,从执行时间看来,这个选择并不是最优的。
使用普通索引需要把回表的代价算进去,
如何解决MySQL选择错索引,导致执行速度变得很慢这个问题呢?
一些问题
如果某次写入使用了 change buffer 机制,之后主机异常重启,是否会丢失 change buffer 和数据?
虽然是只更新内存,但是在事务提交的时候,我们把 change buffer 的操作也记录到 redo log 里了,所以崩溃恢复的时候,change buffer 也能找回来。
merge 的过程是否会把数据直接写回磁盘?
不会的
merge 的执行流程是这样的:
- 从磁盘读入数据页到内存(老版本的数据页);
- 从 change buffer 里找出这个数据页的 change buffer 记录 (可能有多个),依次应用,得到新版数据页;
- 写 redo log。这个 redo log 包含了数据的变更和 change buffer 的变更。
怎末给字符串字段加索引呢?
可以使用前缀索引,遇到前缀的区分度不够好的情况时,使用倒叙存储或hash字段
倒叙存储或hash字段区别是什么?
- 从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。
- 在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
- 从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
答:
- 直接创建完整索引,这样可能比较占用空间;
- 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
- 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
- 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。
索引失效
什么是索引失效?
索引失效指在某些情况下,数据库查询优化器选择不使用已有的索引来加速查询操作,而是采用全表扫描或其他方式来执行查询。
什么时候会发生索引失效?
- 没有遵循最左匹配法则
- 进行模糊查询时,如果%号在前面也会导致索引失效
- 在添加索引的字段上进行了运算操作或者类型转换
- 使用了复合索引,中间使用了范围查询,右边的条件索引也会失效
如何检查是否发生了索引失效?
可以使用mysql自动的执行计划 explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检 查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况。
索引如何优化呢?
- 选择合适的索引类型 B树/B+树索引:适用于大多数场景,特别是等值查询、范围查询。。
- 创建复合索引 将多个经常一起使用的列创建为复合索引,以提高查询效率。注意遵循最左前缀原则,即查询条件中应包含索引的最左边列。
- 覆盖索引 确保查询所需的所有列都在索引中,这样可以直接从索引中获取数据,而无需访问表中的行数据,从而加快查询速度。
- 避免索引失效 注意避免在索引列上使用函数或进行隐式类型转换,以及避免使用LIKE '%value'这样的通配符查询,防止索引失效。
- 合理选择索引列 对于频繁作为查询条件的列,应该考虑建立索引;但对于更新频繁的列,需要权衡索引带来的维护成本。
- 定期重建索引 随着数据的增删改,索引可能会变得不再紧凑,导致性能下降。定期对索引进行重建或重组,可以帮助维持其性能。
- 使用适当的数据类型 尽量使用较小的数据类型,因为更小的数据类型通常占用更少的空间,这有助于减少索引大小,提高I/O性能。
- 限制索引数量 每个额外的索引都会增加插入、更新和删除操作的成本。因此,只应在确实能带来性能提升的情况下创建索引。
- 索引合并在某些情况下,数据库优化器可以将多个单列索引合并起来使用,但这通常不如一个设计良好的复合索引有效。