考虑额外创建二级索引的代价

92 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天

创建二级索引的代价,主要表现在维护代价、空间代价和回表代价三个方面。接下来,我就与你仔细分析下吧。

首先是维护代价。 创建 N 个二级索引,就需要再创建 N 棵 B+ 树,新增数据时不仅要修改聚簇索引,还需要修改这 N 个二级索引。

我们通过实验测试一下创建索引的代价。假设有一个 person 表,有主键 ID,以及 name、score、create_time 三个字段:


CREATE TABLE `person` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `score` int(11) NOT NULL,
  `create_time` timestamp NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

通过下面的存储过程循环创建 10 万条测试数据,我的机器的耗时是 140 秒(本文的例子均在 MySQL 5.7.26 中执行):

如果再创建两个索引,一个是 name 和 score 构成的联合索引,另一个是单一列 create_time 的索引,那么创建 10 万条记录的耗时提高到 154 秒:


KEY `name_score` (`name`,`score`) USING BTREE,
KEY `create_time` (`create_time`) USING BTREE

这里,我再额外提一下,页中的记录都是按照索引值从小到大的顺序存放的,新增记录就需要往页中插入数据,现有的页满了就需要新创建一个页,把现有页的部分数据移过去,这就是页分裂;如果删除了许多数据使得页比较空闲,还需要进行页合并。页分裂和合并,都会有 IO 代价,并且可能在操作过程中产生死锁。

其次是空间代价。 虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间。比如,person 表创建了两个索引后,使用下面的 SQL 查看数据和索引占用的磁盘

SELECT DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES 

WHERE TABLE_NAME='person'

结果显示,数据本身只占用了 4.7M,而索引占用了 8.4M。

最后是回表的代价。二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们要的数据。比如,使用 SELECT * 按照 name 字段查询用户,使用 EXPLAIN 查看执行计划:

EXPLAIN SELECT * FROM person WHERE NAME='name1'

执行计划如下,可以发现:

image.png

  • key 字段代表实际走的是哪个索引,其值是 name_score,说明走的是 name_score 这个索引

  • type 字段代表了访问表的方式,其值 ref 说明是二级索引等值匹配,符合我们的查询。

把 SQL 中的 * 修改为 NAME 和 SCORE,也就是 SELECT name_score 联合索引包含的两列:

EXPLAIN SELECT NAME,SCORE FROM person WHERE NAME='name1'

再来看看执行计划:

image.png

可以看到,Extra 列多了一行 Using index 的提示,证明这次查询直接查的是二级索引,免去了回表。

原因很简单,联合索引中其实保存了多个索引列的值,对于页中的记录先按照字段 1 排序,如果相同再按照字段 2 排序,如图所示:

image.png

image.png

图中,叶子节点每一条记录的第一和第二个方块是索引列的数据,第三个方块是记录的主键。如果我们需要查询的是索引列索引或联合索引能覆盖的数据,那么查询索引本身已经“覆盖”了需要的数据,不再需要回表查询。因此,这种情况也叫作索引覆盖。我会在最后一小节介绍如何查看不同查询的成本,和你一起看看索引覆盖和索引查询后回表的代价差异。

最后,我和你总结下关于索引开销的最佳实践吧。

第一, 无需一开始就建立索引,可以等到业务场景明确后,或者是数据量超过 1 万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用 EXPLAIN 命令,确认查询是否可以使用索引。我会在下一小节展开说明。

第二, 尽量索引轻量级的字段,比如能索引 int 字段就不要索引 varchar 字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用 Elasticsearch 等专门用于文本搜索的索引数据库。

第三, 尽量不要在 SQL 语句中 SELECT *,而是 SELECT 必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段,既能实现索引加速,又可以避免回表的开销。