MySQL的索引

2023-11-09

MySQL 的索引

1 索引的使用

1.1 什么是索引

  索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。数据库使用索引以找到特定值,然后顺指针找到包含该值的行。在表中建立索引,然后在索引中找到符合查询条件的索引值,最后通过保存在索引中的 ROWID(相当于页码)快速找到表中对应的记录。索引的建立是表中比较有指向性的字段,相当于目录,比如说行政区域代码,同一个地域的行政区域代码都是相同的,那么给这一列加上索引,避免让它重复扫描,从而达到优化的目的。

1.2 如何创建索引

  在执行 CREATE TABLE 语句时可以创建索引,也可以单独用 CREATE INDEX 或 ALTER TABLE 来为表增加索引。

1.2.1 ALTER TABLE

  ALTER TABLE用来创建普通索引、UNIQUE 索引或 PRIMARY KEY 索引。

ALTER TABLE table_name ADD INDEX index_name (column_list)
ALTER TABLE table_name ADD UNIQUE (column_list)
ALTER TABLE table_name ADD PRIMARY KEY (column_list)

  其中 table_name 是要增加索引的表名,column_list 指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名 index_name 可选,缺省时,MySQL 将根据第一个索引列赋一个名称。另外,ALTER TABLE 允许在单个语句中更改多个表,因此可以在同时创建多个索引。

1.2.2 CREATE INDEX

  CREATE INDEX 可对表增加普通索引或 UNIQUE 索引。

CREATE INDEX index_name ON table_name (column_list)
CREATE UNIQUE INDEX index_name ON table_name (column_list)

  table_name 、index_name 和 column_list 具有与 ALTER TABLE 语句中相同的含义,索引名不可选。另外,不能用 CREATE INDEX 语句创建 PRIMARY KEY 索引。

1.2.3 CREATE TABLE

create table T(
    id int primary key, 
    k int not null, 
    name varchar(16),
    index (k)
)engine=InnoDB;

1.2.4 创建索引的注意事项

  只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL 值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。
  MySQL 查询只使用一个索引,因此如果 where 子句中已经使用了索引的话,那么 order by 中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

1.3 索引类型

  在创建索引时,可以规定索引能否包含重复值。如果不包含,则索引应该创建为 PRIMARY KEY 或 UNIQUE 索引。对于单列唯一性索引,这保证单列不包含重复的值。对于多列唯一性索引,保证多个值的组合不重复。
  PRIMARY KEY 索引和 UNIQUE 索引非常类似。事实上,PRIMARY KEY 索引仅是一个具有名称 PRIMARY 的 UNIQUE 索引。这表示一个表只能包含一个 PRIMARY KEY,因为一个表中不可能具有两个同名的索引。

1.4 删除索引

  可利用 ALTER TABLE 或 DROP INDEX 语句来删除索引。类似于 CREATE INDEX 语句,DROP INDEX 可以在 ALTER TABLE 内部作为一条语句处理,语法如下。

DROP INDEX index_name ON talbe_name;
ALTER TABLE table_name DROP INDEX index_name;
ALTER TABLE table_name DROP PRIMARY KEY; 

  其中,前两条语句是等价的,删除掉 table_name 中的索引 index_name。第3条语句只在删除 PRIMARY KEY 索引时使用,因为一个表只可能有一个 PRIMARY KEY 索引,因此不需要指定索引名。如果没有创建 PRIMARY KEY 索引,但表具有一个或多个 UNIQUE 索引,则 MySQL 将删除第一个 UNIQUE 索引。
  如果从表中删除了某列,则索引会受到影响。对于多列组合的索引,如果删除其中的某列,则该列也会从索引中删除。如果删除组成索引的所有列,则整个索引将被删除。

1.5 查看索引

show index from table_name;

1.6 如何选择合适的列建立索引

  1. 在 where 从句,group by 从句,order by 从句,on 从句中虚线的列添加索引。
  2. 索引字段越小越好(因为数据库数据存储单位是以“页”为单位的,数据存储的越多,IO 也会越大)。
  3. 查询中与其它表关联的字段需要添加索引。
  4. 对一些经常处理的业务表应在查询允许的情况下尽量减少索引。
  5. 假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。

2 InnoDB 的索引模型

  在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。
  每一个索引在 InnoDB 里面对应一棵 B+ 树。
  假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引:如下

create table T(
    id int primary key, 
    k int not null, 
    name varchar(16),
    index (k)
)engine=InnoDB;
insert into T (id,k) values (100,1);
insert into T (id,k) values (200,2);
insert into T (id,k) values (300,3);
insert into T (id,k) values (500,5);
insert into T (id,k) values (600,6);

在这里插入图片描述

  从图中不难看出,根据叶子节点的内容,索引类型分为主键索引非主键索引。主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引。非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引

2.1 基于主键索引和普通索引的查询有什么区别。

  如上图,如果要查询 id = 500 的数据,即主键查询方式,则只需要搜索 id 这棵 B+ 树。但是如果要查询 k = 5 的数据,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表
  也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。

3 索引维护

  B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值 为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值 为400,就相对麻烦了,需要逻辑上挪动后面的数据空出位置。而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

3.1 为什么要使用自增主键?

  自增主键是指自增列上定义的主键,插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
  除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
  由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型则是 8 个字节。显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
  所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。

3.2 业务主键在什么情况下可以使用?

  当业务所在的场景为 KV 场景时,也就是整个表只有一个索引,并且是唯一索引。这时候我们就要优先考虑“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。

3.3 索引的优化

  增加索引会有利于查询效率,但会降低 insert,update,delete 的效率,但实际上往往不是这样的,过多的索引会不但会影响使用效率,同时会影响查询效率,这是由于数据库进行查询分析时,首先要选择使用哪一个索引进行查询,如果索引过多,分析过程就会越慢,这样同样的减少查询的效率,因此我们要知道如何增加,有时候要知道维护和删除不需要的索引。

3.3.1 重复和冗余的索引

重复索引:重复索引是指相同的列以相同的顺序建立的同类型的索引。
冗余索引:冗余索引是指多个索引的前缀列相同,或是在联合索引中包含了主键的索引。因为普通索引的叶子节点存储的是主键索引,如果建立的联合索引,又人为的把主键包含进去,那么这个时候就是一个冗余索引。

3.3.2 索引的维护删除

  由于业务变更,某些索引是后续不需要使用的,就要进行删除。在 MySQL 中,目前只能通过慢查询日志配合pt-index-usage工具来进行索引使用情况的分析。具体方式请查看这篇文章

4 覆盖索引

  先看如下示例:

create table T (
    ID int primary key,
    k int NOT NULL DEFAULT 0, 
    s varchar(16) NOT NULL DEFAULT '',
    index k(k)
)engine=InnoDB;
insert into T (id,k,s) values (100,1,'a');
insert into T (id,k,s) values (200,2,'b');
insert into T (id,k,s) values (300,3,'c');
insert into T (id,k,s) values (500,5,'d');
insert into T (id,k,s) values (600,6,'e');

  在上面这个表中如果要查询 SELECT * FROM T WHERE k BETWEEN 3 AND 5;这个 SQL 语句的话,索引树和上面的相同。SQL 的执行流程是怎么样的?
  1. 在 k 索引树上找到 k = 3 的记录,取得 ID = 300。
  2. 再到 ID 索引树查到 ID = 300 对应的 R3。
  3. 在 k 索引树取下一个值 k = 5,取得 ID = 500。
  4. 再回到 ID 索引树查到 ID = 500 对应的 R4。
  5. 在 k 索引树取下一个值 k = 6,不满足条件,循环结束。
  在这个过程中,回到主键索引树搜索的过程,我们称为回表。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤1、3 和 5),回表了两次(步骤 2 和 4)。在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。那么,有没有可能经过索引优化,避免回表过程呢?
  如果执行的语句是SELECT ID FROM T WHERE K BETWEEN 3 AND 5;,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引
  由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
  需要注意的是,在引擎内部使用覆盖索引在索引 k 上其实读了三个记录,R3~R5(对应的索引 k 上的记录项),但是对于 MySQL 的 Server 层来说,它就是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2。

5 最左前缀原则

在这里插入图片描述

  为了直观地说明这个概念,我们用(name,age)这个联合索引来分析。如上图,可以看到,索引项是按照索引定义里面出现的字段顺序排序的。当你的逻辑需求是查到所有名是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。
  如果你要查的是所有名字第一个字是“张”的人,你的 SQL 语句的条件是" where name like ‘张%’"。这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

5.1 在建立联合索引的时候,如何安排索引内的字段顺序?

  这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了(a,b)这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
  那么,如果既有联合查询,又有基于a、b各自的查询呢?查询条件里面只有b的语句,是无法使用(a,b)这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护(a,b)、(b) 这两个索引。这时候,我们要考虑的原则就是空间了。比如上面这个示例,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age)的联合索引和一个(age)的单字段索引。

5.2 前缀索引的影响

5.2.1 前缀索引对扫描行的影响

  先看如下示例:

create table SUser(
    ID bigint unsigned primary key,
    email varchar(64), 
    ... 
)engine=innodb; 
-- 有如上的一张表,如果要根据email进行查询。MySQL 是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
 alter table SUser add index index1(email);
-- 如果指定索引长度,那么索引就不会包含整个字符串。
alter table SUser add index index2(email(6));

  第一个语句创建的 index1 索引里面,包含了每个记录的整个字符串。而第二个语句创建的 index2 索引里面,对于每个记录都是只取前 6 个字节。那么,这两种不同的定义在数据结构和存储上有什么区别呢?

在这里插入图片描述
在这里插入图片描述

  如上图,由于 email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势。但这同时带来的损失是,可能会增加额外的记录扫描次数。
  如果有如下SELECT id,name,email FROM SUser WHERE email='zhangssxyz@xxx.com';SQL,这个 SQL 在这两个索引定义下分别是怎么执行的?
  index1
    1. 从 index1 索引树找到满足索引值是’zhangssxyz@xxxx.com’的这条记录,取得 ID2 的值;
    2. 到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
    3. 取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足email=‘zhangssxyz@xxxx.com’的条件了,循环结束。
    这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
  index2
    1. 从 index2 索引树找到满足索引值是’zhangs’的记录,找到的第一个是 ID1;
    2. 到主键上查到主键值是 ID1 的行,判断出 email 的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
    3. 取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;
    4. 重复上一步,直到在 index2 上取到的值不是’zhangs’时,循环结束。
    在这个过程中,要回主键索引取 4 次数据,也就是扫描了 4 行。
  通过这个对比,你很容易就可以发现,使用前缀索引后,可能会导致查询语句读数据的次数变多。但是,对于这个查询语句来说,如果你定义的 index2 不是 email(6) 而是 email(7),也就是说取 email 字段的前 7 个字节来构建索引的话,即满足前缀’zhangss’的记录只有一个,也能够直接查到 ID2,只扫描一行就结束了。
  也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

5.2.1.1 给字符串创建前缀索引时,怎么能够确定使用多长的前缀呢?

  实际上,我们在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。

-- 1. 首先,你可以使用下面这个语句,算出这个列上有多少个不同的值。
select count(distinct email) as L from SUser;
-- 2. 然后,依次选取不同长度的前缀来看这个值,比如我们要看一下4~7个字节的前缀索引,可以用这个语句:
select 
   count(distinct left(email,4)as L4,
   count(distinct left(email,5)as L5,
   count(distinct left(email,6)as L6,
   count(distinct left(email,7)as L7,
from SUser;

  使用前缀索引很可能会损失区分度,所以你需要预先设定一个可以接受的损失比例,比如 5%。然后,在返回的 L4 ~ L7 中,找出不小于 L * 95% 的值,假设这里 L6、L7 都满足,你就可以选择前缀长度为 6。

5.2.1.2 其他方式

  如果前缀字符串的区分度很低,可以使用如下的方式进行索引的创建。

5.2.1.2.1 倒序存储

  比如要创建索引的是身份证号,身份证号前 6 个是地址码,如果要创建区分度高的索引,可能需要创建 12 个字符以上的索引,但是如果存储的时候把身份证号倒过来存储,每次查询的时候通过 reverse 函数转换一下。由于身份证号的最后 6 位没有地址码这样的重复逻辑,所以最后这 6 位很可能就提供了足够的区分度。

5.2.1.2.2 Hash字段

  你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。然后每次插入新记录的时候,都同时用 crc32() 这个函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以你的查询语句 where 部分要判断 id_card 的值是否精确相同。这样,索引的长度变成了4个字节,比原来小了很多。

-- 增加一个字段并在这个字段上添加索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);
-- 使用新创建的字段进行索引查询并且再加上精确查询。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string';
5.2.1.2.3 使用倒序存储和使用 Hash 字段这两种方法的异同点

  首先,它们的相同点是,都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在[ID_X, ID_Y]的所有市民了。同样地,Hash 字段的方式也只能支持等值查询。
  它们的区别,主要体现在以下三个方面:
  1. 从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 Hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 Hash 字段也差不多抵消了。
  2. 在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 Hash字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
  3. 从查询效率上看,使用 Hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。

5.2.2 前缀索引对覆盖索引的影响

  先看看如下的SELECT id,email FROM SUser WHERE email='zhangssxyz@xxx.com';,这个 SQL 如果使用 index1 的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2,就不得不回到 ID 索引再去判断 email 字段的值。即使你将 index2 的定义修改为 email(18) 的前缀索引,这时候虽然 index2 已经包含了所有的信息,但 InnoDB 还是要回到 id 索引再查一下,因为系统并不确定前缀索引的定义是否截断了完整信息。也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。

6 索引下推

CREATE TABLE t_user (
    id int(11) NOT NULL,
    id_card varchar(32) DEFAULT NULL,
    name varchar(32) DEFAULT NULL,
    age int(11) DEFAULT NULL,
    ismale tinyint(1) DEFAULT NULL,
    PRIMARY KEY (id),
    KEY id_card_index (id_card),
    KEY name_age_index (name,age)
) ENGINE = InnoDB

  如上面的表,如果要查询 SELECT * FROM t_user WHERE name like '张%' AND age=10 AND ismale=1;这个 SQL 的加话,根据上面提到的最左前缀原则,它会使用name_age_index的索引信息进行查询,那是否是查询出所有的张姓姓名之后在主键索引上进行条件筛查呢?
  在 MySQL5.6 之前,只能是把性别为张的所有数据查询出来,到主键索引上找出数据行,再对比字段值。
  而 MySQL 5.6 引入的索引下推优化, 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
  如果在这个表中有如下信息

INSERT INTO t_user (id,id_card,name,age,ismale) values(1,101,'张三',10,1);
INSERT INTO t_user (id,id_card,name,age,ismale) values(2,201,'张六',30,1);
INSERT INTO t_user (id,id_card,name,age,ismale) values(3,301,'张四',10,2);
INSERT INTO t_user (id,id_card,name,age,ismale) values(4,401,'张五',10,1);

  那么根据分析会有如下情况:
在这里插入图片描述

  在上面两个图里面,每一条虚线箭头表示回表一次。
  在 MySQL5.6 之前,在(name,age)索引里面我特意去掉了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把" name 第一个字是’张’"的记录一条条取出来回表。因此,需要回表 4 次。
  在 MySQL5.6 开始,InnoDB 在(name,age)索引内部就判断了 age 是否等于 10,对于不等于 10 的
记录,直接判断并跳过。在我们的这个例子中,只需要对R1、R3、R4这三条记录回表取数据判断,就只需要回表 3 次。

7 普通索引和唯一索引在业务中应该如何选择?

7.1 示例

-- 假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。如果市民系统需要按照身份证号查姓名,就会执行类似这样的SQL语句:
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
-- 所以,你一定会考虑在id_card字段上建索引。
-- 由于身份证号字段比较大,不建议把身份证号当做主键,那么现在你有两个选择,要么给id_card字段创建唯一索引,要么创建一个普通索引。如果业务代码已经保证了不会写入重复的身份证号,那么这两个选择逻辑上都是正确的。那么应该怎么选择呢?

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

7.2 查询过程

  1. 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足 k = 5 条件的记录。
  2. 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
  那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微
  InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。
  因为引擎是按页读写的,所以说,当找到 k = 5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。当然,如果 k = 5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。但是,对于整型字段,一个数据页可以放近千个 key,因此出现这种情况的概率会很低。所以,我们计算平均性能差异时,仍可以认为这个操作成本对于现在的 CPU 来说可以忽略不计。

7.3 更新过程

7.3.1 change buffer

  当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
  需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
  将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭的过程中,也会执行 merge 操作。
  显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。

7.3.2 什么条件下可以使用 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%。

7.3.2 更新 InnoDB 的处理流程

  更新的目标页在内存中:
  1. 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
  2. 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
  这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。
  要更新的目标页不在内存中:
  1. 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
  2. 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
  将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。 change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

7.3.3 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 尽量开大,以确保这个“历史数据”表的数据写入速度。

7.3.4 change buffer 和 redo log的区别

  redo log相关的内容请查看这篇文章
  假设要执行下面这个 SQL。

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

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

在这里插入图片描述

  分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib
_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。这条更新语句做了如下的操作(按照图中的数字顺序):
  1. Page 1 在内存中,直接更新内存;
  2. Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息。
  3. 将上述两个动作记入redo log中(图中3和4)。
  做完上面这些,事务就可以完成了。所以,你会看到,执行这条更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),而且还是顺序写的。同时,图中的两个虚线箭头,是后台操作,不影响更新的响应时间。
  下面是在发生更新语句不久之后的读请求:

在这里插入图片描述

  1. 读 Page 1 的时候,直接从内存返回。
  2. 要读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。可以看到,直到需要读 Page 2 的时候,这个数据页才会被读入内存。
  所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

MySQL的索引 的相关文章

  • 使用 Python 2.7 和 MySQLdb 将二进制数据插入 MySQL 中的 blob 列时如何避免编码警告

    我在将二进制数据插入到longblob使用 Python 2 7 中的 MySQLdb 在 MySQL 中的列 但我收到一个编码警告 我不知道如何解决 test py 11 Warning Invalid utf8 character st
  • 使用 Hibernate 和 Apache DBCP 的 MySQL 连接池问题

    看来我的应用程序有问题 当应用程序在启动后闲置很长时间 我不确定确切的时间 时 我会在日志中收到以下错误消息 我使用 Spring Hibernate MySQL 和 ApacheDBCP 进行连接池 ERROR org hibernate
  • 如何在 MariaDB 10 中启用大索引?

    在 Debian Jessie 中 我安装了 MariaDB 服务器 10 0 30 并尝试增加最大密钥长度 AFAIU 这取决于配置参数innodb large prefix正在启用 根据docs https mariadb com kb
  • mySQL 返回可能有重复项的随机行

    我正在尝试随机化一定数量的行 但假设数据库中只有 4 行 而我需要获得 6 个随机行 我希望有可能 即使表中有超过 6 行 产生重复的行行 这在 mySQL 中很容易实现吗 我当前的查询是这样的 SELECT FROM winners OR
  • WooCommerce:在数据库中查找产品

    我正在使用 WooCommerce 创建一个网站 我想根据用户在主页搜索表单中输入的邮政编码来限制用户可用的产品 为了能够实现这一目标 我必须在 phpMyAdmin 的数据库中指定每个产品的条件 但我似乎找不到它 有谁知道 phpmyAd
  • MySQL 错误 1290 (HY000) --secure-file-priv 选项

    我试图在我的脚本中使用以下代码将 MySQL 脚本的结果写入文本文件 SELECT p title p content c name FROM post p LEFT JOIN category c ON p category id c i
  • MySQL:你能指定一个随机限制吗?

    有没有办法在 SQL MySQL 中随机化限制数字 我希望能够做的是在查询中获取随机数量的结果以在插入子查询中使用 而无需任何服务器端脚本 我希望能够作为假设说明运行的查询是 SELECT id FROM users ORDER BY RA
  • 如何在 WP_Query 中按日期排序?

    我已经尝试过这种方式但是orderby and order不适用于 WP Query 类 posts new WP Query array post type gt block code orderby gt post date order
  • PHP 中的异步数据库/服务调用:Gearman 与 pthreads

    在我们的 LAMP 站点上 我们遇到一些服务必须多次调用数据库才能提取数据的问题 通常在 PHP 中完成此操作的方式 至少我的经验 是串行的 这显然是低效的 我们可以通过使用缓存和聚合一些查询来缓解一些低效率的问题 但在某些情况下我们仍然需
  • 使用 EXPLAIN 进行 MYSQL 存储过程调用

    如何分析和使用 EXPLAIN 来调用我的存储过程 我需要优化查询时间 但是似乎没有地方可以执行 EXPLAIN 调用 proc name 你可以试试 set profiling 1 call proc name show profiles
  • 是否可以使用“WHERE”子句来选择SQL语句中的所有记录?

    晚上好 我很好奇是否可以在 SQL 语句中创建一个 WHERE 子句来显示所有记录 下面一些解释 随机 SQL 语句 Java JSP示例 正常情况 String SqlStatement SELECT FROM table example
  • MySQL 触发器和 SUM()

    我有两张桌子 学生桌和家庭桌 在学生中 我有列 st venue 和total venue 家里我有收入 Total Revenue 是学生 st 收入与家庭收入之和 其中 family id student student id stud
  • 在 MySQL 中将行转置为列

    如何在 MySQL 查询中将行转换为列 您可以将行变成a列与GROUP CONCAT 但您无法以任何自动方式转置整个结果集 您可以编写手动生成每一列的查询 也可以在应用程序中执行此操作 以下是有关编写复杂查询来模拟转置的教程 http ww
  • Laravel Sum 列数据库 Eloquent

    尝试获取我的一个表中 int 字段的总和应该非常容易 不幸的是 事实并非如此 因为无论我使用 Laravel MySQL 还是 Excel 我都会得到不同的结果 Laravel 5 4 给了我20506 Table sum field na
  • PHP/MySQL:如何在网站中创建评论部分[关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我不会问 如何使用 PHP MySQ
  • SQL Server 相当于 MySQL 的 USING

    在 MySQL 中 当您连接不同表中具有相同名称的列时 可以在连接中使用关键字 USING 例如 这些查询产生相同的结果 SELECT FROM user INNER JOIN perm USING uid SELECT FROM user
  • 数据表 - 服务器端处理 - 数据库列合并

    我目前正在使用 DataTables 1 10 5 服务器端处理功能 http www datatables net examples data sources server side html http www datatables ne
  • #1045 - 用户“root”@“localhost”的访问被拒绝(使用密码:YES)

    这可能看起来多余 但我无法找到正确的解决方案 我无法使用 mysql 控制台登录 mysql 它要求输入密码 但我不知道我实际输入的内容 有办法获取密码或更改密码吗 这就是我的 config inc 的样子 当我尝试打开 phpmyadmi
  • 如何使用 PHP 从 MySQL 检索特定值?

    好吧 我已经厌倦了 过去一周我花了大部分空闲时间试图解决这个问题 我知道 SQL 中的查询已更改 但我无法弄清楚 我能找到的所有其他帖子似乎都已经过时了 如果有人能帮助我 我将非常感激 我想做的就是使用手动输入数据库的唯一 密码 来检索行的
  • SQL:查找每个跑步者跑步之间的平均天数

    因此 如果我们给出下表 runner ran Carol 2011 02 01 Alice 2011 02 01 Bob 2011 02 01 Carol 2011 02 02 Bob 2011 02 02 Bob 2011 02 03 B

随机推荐

  • 嵌入式物联网毕业设计选题智能图像识别项目-stm32mp157 linux开发板

    stm32mp157开发板FS MP1A是华清远见自主研发的一款高品质 高性价比的Linux 单片机二合一的嵌入式教学级开发板 开发板搭载ST的STM32MP157高性能微处理器 集成2个Cortex A7核和1个Cortex M4 核 A
  • R语言第八次课堂小测 rattle的应用(包括rattle的安装)

    题目 安装rattle 并使用rattle 用三种聚类方法对鸢尾花数据集进行聚类 步骤一 修改镜像源 首先 在Rstudio上打开如下界面 进入后 找到packages 再点击change 下图是已经更换了的截图 选择中国的任意一个镜像 最
  • ./configure之后报错

    首先要看报的错误是什么 一般从第一条开始解决 因为有可能下面的错误是由上面的导致的
  • js 判断变量类型(完整版),包括ES6 新类型Symbol

    欢迎来到Altaba的博客 相信大家在开发中遇到需要判断变量类型的问题 js变量按存储类型可分为值类型和引用类型 值类型包括Undefined String Number Boolean 引用类型包括object Array Functio
  • 股票和期货的区别(股指期货1个点赚多少钱)

    股票和期货的辨别 股票的最后含意即是说不妨表明你购置了这家公司的股子 而期货 则是买卖两边按照各自对目标物的将来价钱预期 以此刻的价钱签署的合约 观念既是仍旧领会了 那咱们就再领会一下这几个的辨别 1 目标物 目标物也即是买卖东西 菜商场里
  • 【深度学习】SETR:基于视觉 Transformer 的语义分割模型

    Visual Transformer Author louwill Machine Learning Lab 自从Transformer在视觉领域大火之后 一系列下游视觉任务应用研究也随之多了起来 基于视觉Transformer的语义分割正
  • OpenMMLab AI实战营第二期(2)MMPose初体验

    根据MMPose的官方文档学习一下 MMPose文档地址 https mmpose readthedocs io zh CN latest index html 文章目录 1 概述 2 安装 2 1 创建conda环境并激活 2 2 安装p
  • mysql锁

    想要了解锁 必须要知道mysql事务 以及mysql事务产生的并发问题 数据库中的事务 隔离级别 以及数据展示 華同学 的博客 CSDN博客 1 Mysql锁的介绍 锁是计算机协调多个线程或进程并发访问某一资源的机制 除传统的计算机资源 C
  • sentencepiece原理与实践

    1 前言 前段时间在看到XLNET Transformer XL等预训练模式时 看到源代码都用到sentencepiece模型 当时不清楚 经过这段时间实践和应用 觉得这个方法和工具值得NLP领域推广和应用 今天就分享下sentencepi
  • Vscode + php + xdebug 单步调试

    1 确认xdebug已打开 php ini xdebug remote enable 1 xdebug remote autostart 1 xdebug remote host localhost xdebug remote port 9
  • win32汇编基础概念

    一 关于寄存器 寄存器有EAX EBX ECX EDX EDI ESI ESP EBP等 似乎IP也是寄存器 但只有在CALL RET在中会默认使用它 其它情况很少使用到 暂时可以不用理会 EAX是WIN32 API 默认的返回值存放处 E
  • 深入理解机器学习与极大似然之间的联系

    似然函数 事件A的发生含着有许多其它事件的发生 所以我就把这些其它事件发生的联合概率来作为事件A的概率 也就是似然函数 数据类型的不同 离散型和连续性 就有不同的似然函数 极大似然极大似然估计方法 Maximum Likelihood Es
  • sqli-labs:less-27(过滤select和union)

    div div
  • eosjs v20 如何通过jsonrpc连接到主网节点

    用eosjs连接主网节点很简单 只需要在创建JsonRpc对象时 指定要连接主网节点的地址 就可以了 例如 下面的代码将创建一个使用eosnewyork io节点RPC旳JsonRpc 对象 然后使用get info 方法获取网络总体信息
  • 拆分Nim游戏

    拆分Nim游戏 给定n堆石子 两位玩家轮流操作 每次操作可以取走其中的一堆石子 然后放入两堆规模更小的石子 新堆规模可以为0 且两个新堆的石子总数可以大于取走的那堆石子数 最后无法进行操作的人视为失败 问如果两人都采用最优策略 先手是否必胜
  • 顺序主子式

    设有矩阵 a b c d e f g h i 则 一阶顺序主子式 a 二阶顺序主子式 a b d e 三阶顺序主子式 a b c d e f g h i 若n阶方阵A的顺序主子式均 0 则A的LU分解A LU存在且唯一 转载于 https
  • 微软Xbox One无线手柄控制机器人

    ROS中的joy包提供了游戏手柄的驱动 并且包含joy node节点 这一节点可以发布包含手柄按钮和轴信息的Joy消息 在终端中输入下面命令 安装joy包 sudo apt get install ros kinetic joy Confi
  • python: FileHelper

    encoding utf 8 版权所有 2023 涂聚文有限公司 许可信息查看 描述 Author geovindu Geovin Du 涂聚文 IDE PyCharm 2023 1 python 311 Datetime 2023 7 9
  • A卡2023最新AI画图教程:3A主机安装ROCm运行Stable Diffusion画图

    硬件平台 3A主机 内存16G 显卡 AMD RX6700 XT 12GB 华擎幻影电竞 处理器 AMD R5 3500X 6C6T 主板 华硕TUF B450M PRO GAMING 安装Ubuntu22 04 2 LTS系统 更换系统源
  • MySQL的索引

    MySQL 的索引 1 索引的使用 1 1 什么是索引 1 2 如何创建索引 1 2 1 ALTER TABLE 1 2 2 CREATE INDEX 1 2 3 CREATE TABLE 1 2 4 创建索引的注意事项 1 3 索引类型