首页 > 其他分享 >第八讲:普通索引和唯一索引,应该怎么选择?

第八讲:普通索引和唯一索引,应该怎么选择?

时间:2024-07-25 15:44:00浏览次数:7  
标签:buffer 第八 更新 唯一 索引 内存 数据 change

目录

第八讲:普通索引和唯一索引,应该怎么选择?

img

日常

日常开头:

​ 在前面的基础篇文章中,我给你介绍过索引的基本概念,相信你已经了解了唯一索引和普通索引的区别。今天我们就继续来谈谈,在不同的业务场景下,应该选择普通索引,还是唯一索引?

​ 假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。如果市民系统需要按照身份证号查姓名,就会执行类似这样的 SQL 语句:

select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';

​ 所以,你一定会考虑在 id_card 字段上建索引

​ 由于身份证号字段比较大,我不建议你把身份证号当做主键,那么现在你有两个选择,要么给 id_card 字段创建唯一索引,要么创建一个普通索引。如果业务代码已经保证了不会写入重复的身份证号,那么这两个选择逻辑上都是正确的。

日常提问:

​ 现在我要问你的是,从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?

​ 简单起见,我们还是用第 4 篇文章《深入浅出索引(上)》中的例子来说明,假设字段 k 上的值都不重复。

img

​ 接下来,我们就从这两种索引对查询语句和更新语句的性能影响来进行分析。

查询过程:

​ 假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。

(批注:先通过B+树找到树根,再在叶子结点通过二分查找定位记录)

  • 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

日常答案

​ 那么,这个不同带来的性能差距会有多少呢?

​ 答案是,微乎其微。

​ 你知道的,InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。

​ 因为引擎是按页读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。

​ 当然,如果 k=5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。

​ 但是,我们之前计算过,对于整型字段,一个数据页可以放近千个 key,因此出现这种情况的概率会很低。所以,我们计算平均性能差异时,仍可以认为这个操作成本对于现在的 CPU 来说可以忽略不计。

更新过程

​ 为了说明普通索引和唯一索引对更新语句性能的影响这个问题,我需要先跟你介绍一下 change buffer。

​ 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。

​ 需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。

​ 将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。

​ 显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。

​ 那么,什么条件下可以使用 change buffer 呢?

​ 对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。

​ 因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。

​ change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

​ 现在,你已经理解了 change buffer 的机制,那么我们再一起来看看如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。

不同索引的更新

第一种情况是,这个记录要更新的目标页在内存中。

​ 这时,InnoDB 的处理流程如下:

  • 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。

​ 这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。

​ 但,这不是我们关注的重点。

第二种情况是,这个记录要更新的目标页不在内存中。

​ 这时,InnoDB 的处理流程如下:

​ 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。

​ 将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

一个更新事故:

​ 之前我就碰到过一件事儿,有个 DBA 的同学跟我反馈说,他负责的某个业务的库内存命中率突然从 99% 降低到了 75%,整个系统处于阻塞状态,更新语句全部堵住。而探究其原因后,我发现这个业务有大量插入数据的操作,而他在前一天把其中的某个普通索引改成了唯一索引。

change buffer 的使用场景

​ 通过上面的分析,你已经清楚了使用 change buffer 对更新过程的加速作用,也清楚了 change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。那么,现在有一个问题就是:普通索引的所有场景,使用 change buffer 都可以起到加速作用吗?

​ 因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大

​ 因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。

​ 反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。

索引选择和实践

​ 回到我们文章开头的问题,普通索引和唯一索引应该怎么选择。其实,这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,我建议你尽量选择普通索引

​ 如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。

​ 在实际使用中,你会发现,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。

​ 特别地,在使用机械硬盘时,change buffer 这个机制的收效是非常显著的。所以,当你有一个类似“历史数据”的库,并且出于成本考虑用的是机械硬盘时,那你应该特别关注这些表里的索引,尽量使用普通索引,然后把 change buffer 尽量开大,以确保这个“历史数据”表的数据写入速度。

change buffer 和 redo log

​ 理解了 change buffer 的原理,你可能会联想到我在前面文章中和你介绍过的 redo log 和 WAL。

​ 在前面文章的评论中,我发现有同学混淆了 redo log 和 change buffer。WAL 提升性能的核心机制,也的确是尽量减少随机读写,这两个概念确实容易混淆。所以,这里我把它们放到了同一个流程里来说明,便于你区分这两个概念。

深入分析:

​ 现在,我们要在表上执行这个插入语句:

mysql> insert into t(id,k) values(id1,k1),(id2,k2);

​ 这里,我们假设当前 k 索引树的状态,查找到位置后,k1 所在的数据页在内存 (InnoDB buffer pool) 中,k2 所在的数据页不在内存中。如图 2 所示是带 change buffer 的更新状态图。

img

​ 分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。

​ 这条更新语句做了如下的操作(按照图中的数字顺序):

  • Page 1 在内存中,直接更新内存;
  • Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息
  • 将上述两个动作记入 redo log 中(图中 3 和 4)。

​ 做完上面这些,事务就可以完成了。所以,你会看到,执行这条更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),而且还是顺序写的。

​ 同时,图中的两个虚线箭头,是后台操作,不影响更新的响应时间。那在这之后的读请求,要怎么处理呢?

​ 比如,我们现在要执行 select * from t where k in (k1, k2)。这里,我画了这两个读请求的流程图。

​ 如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。所以,我在图中就没画出这两部分。

img

​ 从图中可以看到:

​ 读 Page 1 的时候,直接从内存返回。有几位同学在前面文章的评论中问到,WAL 之后如果读数据,是不是一定要读盘,是不是一定要从 redo log 里面把数据更新以后才可以返回?其实是不用的。你可以看一下图 3 的这个状态,虽然磁盘上还是之前的数据,但是这里直接从内存返回结果,结果是正确的。

​ 要读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。

​ 可以看到,直到需要读 Page 2 的时候,这个数据页才会被读入内存

​ 所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

小结

​ 今天,我从普通索引和唯一索引的选择开始,和你分享了数据的查询和更新过程,然后说明了 change buffer 的机制以及应用场景,最后讲到了索引选择的实践。由于唯一索引用不上 change buffer 的优化机制,因此如果业务可以接受,从性能角度出发我建议你优先考虑非唯一索引。

补充:

​ 评论区大家对“是否使用唯一索引”有比较多的讨论,主要是纠结在“业务可能无法确保”的情况。这里,我再说明一下:

  • 首先,业务正确性优先。咱们这篇文章的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本篇文章的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,可以给你多提供一个排查思路。
  • 然后,在一些“归档库”的场景,你是可以考虑使用普通索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。

深入:

NO1:

​ 普通索引跟唯一索引执行上的区别: 普通索引的等值查询,会继续遍历到第一个不相等的值才会结束,而唯一索引等值查询,命中则结束(性能差距微乎其微)

No2:

​ 因为遍历的链表是有序的,所以往后一个不满足,就都不满足了,没必要往下继续遍历到最后一个啦,普通索引也是排序的,如果不是唯一索引,那么可能会出现多个重复的索引值,所以找到第一个满足条件的数据后,顺着往后找(排序了嘛),直到找到第一个不等的值为止。

No3:

​ 普通索引存放的索引是可重复的,因此查找到满足的第一个记录的时候还是要继续往下查找;而对于唯一索引,因为是唯一的,不重复,所以找到满足的就可以结束了。 俩者的性能差距主要体现在,如果要查找的正好处于一个数据页的末尾上。具体来说,如果是唯一索引,那么就直接结束,因为后面肯定不会在出现满足条件的索引了;而对于普通索引来说,就还得从磁盘中取一个数据页到内存当中,多了一次IO操作,性能也自然就下去了。另一个方面,唯一索引只需判断一次,普通索引还需要向后继续寻找,直到不满足才结束。当然这对于CPU来说影响较小,主要问题还是在一个数据页的末尾的情况下。 (如果不在数据页的末尾,就不需要进行IO取下一页的数据,此时性能差距微乎其微,所以一般来说性能差距很小) 因为遍历的链表是有序的,所以往后一个不满足,就都不满足了,没必要往下继续遍历到最后一个啦

No4:

​ 普通索引和唯一索引的查询性能几乎一样, 但是写性能是普通索引快, 因为可以用到change buffer, 唯一索引会导致内存命中率下降

No5:

​ 理解change buffer,首先要明确概念: (1)mysql数据存储在主键索引树的叶子节点。 (2)普通索引和唯一索引也都有自己的索引树,树的叶子节点存储的是主键ID。 (3)做更新操作(插入,更新,删除)会同时更新所有的索引树结构。---------insert:主键索引树和唯一建索引树的肯定都要更新,肯定是无法用到change buffer的;但是普通索引树的更新,是可以使用change buffer的。 update:只要涉及到相关字段更新,就要同时更新相应的索引树。道理同上。 【显然,insert操作的影响更大,如果有多个唯一索引,insert对内存命中率会有极大影响】 1、减少读磁盘:仅仅是减少的是对二级普通索引页的读磁盘操作,而对于其他类型的页(唯一索引,主键索引)还是要读磁盘的。 2、减少内存占用:change buffer虽然还是需要内存占用(记录数据更新动作),但相比于数据页来说(默认16K),所占的内存还是小了很多的。

No6:

​ 数据表空间:就是一个个的表数据文件,对应的磁盘文件就是“表名.ibd”; 系统表空间:用来放系统信息,如数据字典等,对应的磁盘文件是“ibdata1”

​ 数据的更新(数据与主键聚簇索引-数据结构为B+树)落地到磁盘,就是写入到ibd数据文件 更新数据的同时 更新二级索引(B+树)落地到磁盘,是写入ibdata1文件

No7:

​ 更新语句主要做这几件事情:

  1. k1所在Page1已在内存中,那么直接更新内存

  2. k2所在Page2不在内存中,则在change-bufffer记录”add k2 to page2“这样的信息

  3. 上述两个动作记入到redo log中

  4. 在适当的时机写入到磁盘。

    (总结:在内存直接更新内存,否则更新change buffer,操作内存或者操作change buffer都需要记录到redo log,方便后台将数据同步到内存)


​ 在此之后,若读取这两条数据应是如下操作:

  1. 因为Page1数据已在内存,那么读取k1数据直接从内存返回
  2. 因为Page2不在内存,那么要先将page2读入内存,再将change_buffer所更改信息合并到page2,生成一个正确结构再返回。

问题

​ change buffer 一开始是写内存的,那么如果这个时候机器掉电重启,会不会导致 change buffer 丢失呢?change buffer 丢失可不是小事儿,再从磁盘读入数据可就没有了 merge 过程,就等于是数据丢失了。会不会出现这种情况呢?

答案:

​ 这个问题的答案是不会丢失,

​ 留言区的很多同学都回答对了。

​ 虽然是只更新内存,但是在事务提交的时候,我们把 change buffer 的操作也记录到 redo log 里了,所以崩溃恢复的时候,change buffer 也能找回来。

​ 在评论区有同学问到,merge 的过程是否会把数据直接写回磁盘,这是个好问题。这里,我再为你分析一下。

  • merge 的执行流程是这样的:从磁盘读入数据页到内存(老版本的数据页);

  • 从 change buffer 里找出这个数据页的 change buffer 记录 (可能有多个),依次应用,得到新版数据页;

  • 写 redo log。这个 redo log 包含了数据的变更和 change buffer 的变更。

​ 到这里 merge 过程就结束了。这时候,数据页和内存中 change buffer 对应的磁盘位置都还没有修改,属于脏页,之后各自刷回自己的物理数据,就是另外一个过程了。

标签:buffer,第八,更新,唯一,索引,内存,数据,change
From: https://www.cnblogs.com/guixiangyyds/p/18323276/mysql8

相关文章

  • 一文了解MySQL索引机制
    接触MySQL数据库的小伙伴一定避不开索引,索引的出现是为了提高数据查询的效率,就像书的目录一样。某一个SQL查询比较慢,你第一时间想到的就是“给某个字段加个索引吧”,那么索引是什么?是如何工作的呢?一起静下心来,耐心看完这篇文章吧,干货不啰嗦,相信你一定会有所收获。一、索引模型模......
  • 是否可以从列中删除唯一约束?
    我尝试简单地删除unique=True约束并在命令行中运行flaskdbmigrateflaskdbupgrade,但是当我运行我的烧瓶应用程序时,我仍然收到(sqlite3.IntegrityError)UNIQUEconstraintfailed错误。有没有一种简单的方法可以做到这一点Flask-migrate还是我......
  • SQL-----索引
    ●索引概述介绍:索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这数据结构上实现高级查找算法,这种数据结构就是索引。·优点:提高数据检索的效率,降低数据库的io......
  • Python获取list中指定元素索引的两种方法
    在平时开发过程中,经常遇到需要在数据中获取特定的元素的信息,如到达目的地最近的车站,橱窗里面最贵的物品等等。怎么办?看下面方法一:利用数组自身的特性list.index(target),其中a是你的目标list,target是你需要的下标对应的值li=[10,8,9,26,72,6,28]print(li.index(8))但是,......
  • 力扣高频SQL 50题(基础版)第八题
    文章目录力扣高频SQL50题(基础版)第八题1581.进店却未进行过交易的顾客题目说明思路分析实现过程准备数据:实现方式:结果截图:总结:力扣高频SQL50题(基础版)第八题1581.进店却未进行过交易的顾客题目说明表:Visits±------------±--------+|ColumnName|Type|......
  • 在线客服系统二次开发:访客聊天前端页面删减部分功能【唯一客服】
    前端部分想要修改前端界面,删除某些功能,或修改某些文字,可以查看下面介绍我们的前端是传统的开发模式,不是node编译后部署形式,前端没有独立的项目所有展示的页面地址,都是后端的地址,后端把前端html模板页渲染出来展示渲染前端页面的原理Gin框架引入并渲染前端页面后,前端页面就是......
  • PART1-Oracle关系数据结构-索引和索引组织表
    3.索引组织表3.1.索引概述索引是与表或表簇关联的可选结构,有时可以加快数据访问速度。通过在表的一个或多个列上创建索引,在某些情况下,您可以从表中检索一小部分随机分布的行。索引是减少磁盘I/O的众多方法之一。如果堆组织表没有索引,那么数据库必须执行全表扫描才能找到一个......
  • 获取一行中字符串中第一个字符(五个正确字符中的第一个字符)的索引
    我正在尝试压缩我的代码(与朋友进行最少行数和字符挑战),但我遇到了问题。outputWord=""deftranslate(inp):output=[]words=inp.split()foriinrange(len(words)):outputWord=""curWord=words[i]ifcurWord[0]in("a","o",......
  • 代码随想录算法训练营第八天|● 344.反转字符串● 541. 反转字符串II● 卡码网:54.替换
    题目:344.反转字符串编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用O(1)的额外空间解决这一问题。示例1:输入:s=["h","e","l","l","o"]输出:["o","l","l","e",&......
  • 如何解决 TypeError:列表索引必须是整数或切片,而不是 str
    我想建造一个刮刀。我写了这段代码,但它不断出错。如果有人可以提供帮助,我将不胜感激。deffetch_json(api_link,timeout=10):try:response=requests.get(api_link,timeout=timeout)ifresponse.status_code==200:returnresponse.js......