跳过正文
  1. 面试题库/

04|索引应用

·14354 字·29 分钟
目录
MySQL面试题库 - 这篇文章属于一个选集。
§ 4: 本文

1. MySQL 有哪些索引?
#

分析

主键索引、唯一索引、普通索引、前缀索引、联合索引。

  • 主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
  • 唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
  • 普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE
  • 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引。前缀索引可以建立在字段类型为 charvarcharbinaryvarbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
  • 联合索引:通过将多个字段组合成一个索引,该索引就被称为联合索引。

回答

我了解到 MySQL 有主键索引、唯一索引、普通索引、前缀索引、联合索引这几种索引。InnoDB 引擎会要求每一张数据库表都必须要有一个主键索引,比如表里的 id 字段就是主键索引。

然后针对查询比较频繁的字段,我们可以对这个字段建立普通索引;如果是多个字段的话,可以考虑建立联合索引,利用索引覆盖的特性提高查询效率。

对于长文本、字符串等类型的字段,比如文章标题、商品名称等,我们可以只对这些字段的前缀部分建立索引,也就是建立前缀索引,这样可以减少索引的存储空间。

2. MySQL 主键是聚簇索引吗?
#

分析

聚簇索引就是按照每张表的主键构造一棵 B+ 树,同时叶子节点中存放的是整张表的行记录数据,就好像把数据和索引聚集在了一棵 B+ 树上,所以这种数据组织形式的索引叫聚簇索引。

每张表只能拥有一个聚簇索引,因为数据库表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。

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

  • 如果定义了主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐藏自增 idrow_id)列作为聚簇索引的索引键。

回答

是的,主键是聚簇索引。我们创建数据库表的时候,通常都会对 id 字段设置为主键索引,InnoDB 在创建聚簇索引时,默认会使用主键作为聚簇索引的索引键。

3. 主键为什么不推荐有业务含义?
#

分析

因为任何有业务含义的列都有改变的可能性,主键一旦带上了业务含义,那么主键就有可能发生变更。主键一旦发生变更,该数据在磁盘上的存储位置就会发生变更,有可能会引发页分裂,产生空间碎片。

还有就是,带有业务含义的主键,不一定是顺序自增的。那么就会导致数据的插入顺序,并不能保证后面插入数据的主键一定比前面的数据大。如果出现了后面插入数据的主键比前面的小,就有可能引发页分裂,产生空间碎片。

回答

我觉得原因有两个:

  • 第一个是业务会有变动的可能性,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现,等需要变动的时候,去更改主键是成本很高的一件事情,不如设计阶段就规避不用有业务含义的主键。
  • 第二个是业务含义的主键可能不是顺序自增的,有可能会发生页分裂问题,从而影响性能。

4. 主键是用自增还是 UUID?
#

分析

在《阿里巴巴 Java 开发手册》第五章 MySQL 规约第九条中,强制规定了单表的主键 id 必须为无符号的 bigint 类型,且是自增的。

通常主键 id 的数据类型有两种选择:字符串或者整数。主键通常要求是唯一的,如果使用字符串类型,我们可以选择 UUID 或者具有业务含义的字符串来作为主键。

对于 UUID 而言,它由 32 个字符加 4 个 - 组成,长度为 36。虽然 UUID 能保证唯一性,但是它有两个致命的缺点:

  1. 不是递增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
  2. 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 I/O 次数越多,性能越差。

对于整数的数字类型,MySQL 中主要有 intbigint 类型。其中 int 占用 4 个字节,bigint 占用 8 个字节,这和 Java 中的 intlong 对应。

  • 如果使用无符号的 int 类型作为主键,那么主键的最大值为 \(2^{32}-1\),即 4294967295,这个值不到 43 亿,似乎有点太小了。虽然一张表的数据,我们不可能让其达到 43 亿条(太大会影响性能),但是对于频繁进行插入、删除的表来说,43 亿这个值是可以达到的。
  • 如果使用无符号的 bigint 类型的话,主键的最大值可以达到 \(2^{64}-1\),这个数足够大了,如果以每秒插入 100 万条数据计算,58 万年以后才能达到最大值。所以 bigint 作为主键的数据类型,完全不用担心超过最大值的问题。

而强制要求主键 id 是自增的,则是为了在数据插入的过程中,尽可能地避免索引树上页分裂的问题。

回答

用自增 id 比较好,因为 UUID 是随机值,在数据插入的过程中,会导致索引树发生页分裂的问题,会影响性能。而且 UUID 是字符串类型,长度比较长,占用内存比较大,而页的大小是固定的,这样会导致索引树的高度越高,查询的时候发生的磁盘 I/O 次数也越多,性能也就更低。

但是自增 id 在分库分表环境下就不适用了,因为没办法保证全局唯一,这时候就需要考虑用雪花算法来作为主键了。

5. 普通索引和唯一索引有什么区别?哪个更新性能更好?
#

分析

哪个更新性能更好,要从 InnoDB 引擎的 change buffer 角度去分析。

回答

普通索引列的值是可以重复的,而唯一索引列的值必须唯一,当我们对唯一索引插入了一条重复的值,会因为唯一性约束而报错。

我认为普通索引的更新性能会更好,因为普通索引在更新的时候,如果更新的数据页不在内存的话,可以直接把更新操作缓存在 change buffer 中,更新操作就结束了。但是唯一索引因为需要有唯一性约束,如果更新的数据页不在内存的话,需要从磁盘读取对应的数据页到内存,判断是否有冲突,这里会涉及磁盘随机 I/O 的访问。

普通索引因为能使用 change buffer 特性,所以普通索引的更新相比于唯一索引,减少了随机磁盘访问,所以更新性能更好。

6. 主键怎么设置?追问:假如你不设置会怎么样?
#

分析

可以在创建表时,将某一列定义为主键(PRIMARY KEY)。例如:

CREATE TABLE table_name (
  id INT PRIMARY KEY,
  column1 datatype,
  column2 datatype,
  ...
);

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

  • 如果有主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐藏自增 id 列作为聚簇索引的索引键。

回答

在创建表的时候,对 id 列设置为 PRIMARY KEY,那么 id 列就是主键索引了。

追问回答

如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键,如果这个条件也没有达成的话,InnoDB 将自动生成一个隐藏 row_id 列作为聚簇索引的索引键。

7. 介绍一下什么是外键约束?
#

分析

先来看什么是外键。

假设我们有 2 个表,分别是表 A 和表 B,它们通过一个公共字段 id 发生关联关系,我们把这个关联关系叫做 R。如果 id 在表 A 中是主键,那么表 A 就是这个关系 R 中的主表。相应的,表 B 就是这个关系中的从表,表 B 中的 id,就是表 B 用来引用表 A 中数据的,叫外键。所以,外键就是「从表」中用来引用「主表」中数据的那个公共字段。

alt text

如图所示,在关联关系 R 中,公共字段(字段 A)是表 A 的主键,所以表 A 是主表,表 B 是从表。表 B 中的公共字段(字段 A)是外键。

什么是外键约束?

在 MySQL 中,外键是通过外键约束来定义的。外键约束就是约束的一种,它必须在从表中定义,包括指明哪个是外键字段,以及外键字段所引用的主表中的主键字段是什么。MySQL 系统会根据外键约束的定义,监控对主表中数据的删除操作。如果发现要删除的主表记录,正在被从表中某条记录的外键字段所引用,MySQL 就会提示错误,从而确保了关联数据不会缺失,保证了 2 个表中数据的一致性。

回答

外键就是「从表」中用来引用「主表」中数据的那个公共字段,外键约束确保了数据的引用完整性,也就是「从表」中的外键必须存在于「主表」的主键中。如果发现要删除的主表记录,正在被「从表」中某条记录的外键字段所引用,MySQL 就会提示错误,从而保证了 2 个表中数据的一致性。

8. 外键有什么优劣势?
#

分析

MySQL 外键最大的作用就是有助于维护数据的一致性和完整性。

  • 一致性:如果一个订单表引用了一个客户表的外键,外键可以确保订单的客户 ID 存在于客户表中,从而保持数据的一致性。
  • 完整性:外键可以防止在引用表中删除正在被其他表引用的记录,从而维护数据的完整性。

但是,其实在很多大型互联网公司中,很少用外键的,甚至阿里巴巴 Java 开发手册中明确规定了:「不要使用外键约束,如果数据存在外键关系,请在程序层面实现」。

alt text

使用外键会带来这些问题:

  • 性能问题:定义外键之后,数据库的每次操作都需要去检查外键约束,检查关联表是否已经存在数据。硬性保持数据一致性。这些操作会占用数据库的计算资源,如果一条记录中存在多个外键,这样的 buff 还将会被叠加(性能损耗加成)。对于插入来说,数据库需要执行数据一致性检查以确保引用的数据在表中的存在,会影响插入速度;对于更新来说,级联更新是强阻塞,存在数据库更新风暴(Database Update Storm)的风险。所谓 Database Update Storm,指的是在高并发环境下,多个客户端同时对数据库进行大量的更新操作,存在锁竞争问题甚至死锁,从而导致数据库性能急剧下降或完全崩溃。
  • 锁竞争问题:在使用外键的情况下,每次修改数据都需要去检查外键关联表里的数据,这需要额外获取读锁,如果是高并发的情况下,更容易造成死锁。
  • 无法适用分库分表场景:在大型项目中,当数据量特别大的时候,一般会采取分库分表来存储数据,但在不同的库中使用相同的外键来维护数据一致性和完整性是非常难的操作,外键难以跨越不同数据库来建立关系。所以在分布式、高并发集群的项目数据库中一般看不到外键的存在。

回答

外键能够保证数据的一致性和完整性,通过设置外键,数据库就会判断数据的完整性,不需要在应用代码里实现。

有了外键之后,每次增删改都需要额外检查外键约束,会占用数据库的计算资源,影响增删改的性能,而且还需要额外获取锁,在高并发场景下很容易发生死锁的问题。另外,外键也不适合分库分表的场景,外键难以跨越不同数据库来建立关系。

因此,基于性能开销、锁竞争、分库分表的考虑,一般项目中,很少用外键约束,都是在应用层面完成检查数据一致性的逻辑,就像大厂会使用 RC(读已提交隔离级别)来替代 RR(可重复读隔离级别)一样,会尽可能降低锁的发生,一方面提升性能,一方面降低死锁概率。

9. 为什么要建索引?
#

分析

考察索引的优点。建索引的三个优点:

  • 索引大大减少了 MySQL 需要扫描的数据量;
  • 索引可以帮助 MySQL 避免外部排序和使用临时表;
  • 索引可以将随机 I/O 变为顺序 I/O。

回答

如果没有建立索引,我们查询数据的话,搜索时间复杂度是 \(O(n)\),这样的查询效率还是比较低的,为了提高查询效率,我们可以建立索引。

建立了索引后数据都会按照顺序存储,这时候我们可以利用类似二分查找的方式快速查找数据,B+ 树索引是多叉树,搜索时间复杂度是 \(O(\log_d N)\),这样就提高了查询速度。除此之外,还可以避免外部排序和使用临时表等问题,以及将随机 I/O 变为顺序 I/O。

10. 我们一般选择什么样的字段来建立索引?
#

分析

考察索引的使用场景。

适用索引的场景:

  • 字段有唯一性限制的,比如商品编码;
  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引;
  • 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在 B+Tree 中的记录都是排序好的。

不适合索引的场景:

  • WHERE 条件、GROUP BYORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。
  • 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女。如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值在表中出现的比例很高的时候,它一般会忽略索引,进行全表扫描。
  • 经常更新的字段不用创建索引。比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改时,由于要维护 B+Tree 的有序性,就需要频繁地重建索引,这个过程会影响数据库性能。

回答

可以对频繁用于 WHERE 查询条件的字段建立索引,这样能够提高整张表的查询速度,如果查询条件不是一个字段,可以考虑建立联合索引。还有对于经常用于排序、分组的字段建立索引,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在 B+ 树中的数据都是排序好的。

不过,对于一些区分度不高的字段,比如性别字段,只有男女,不建议建立索引。如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据,在这些情况下,MySQL 的优化器发现某个值在表中出现的比例很高的时候,它一般会忽略索引,进行全表扫描,这时候建立的索引就没有起到作用,反而还占用了存储空间。

11. 索引越多越好吗?
#

分析

考察索引的缺点。索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:

  • 空间代价:需要占用物理空间,数量越大,占用空间越大;
  • 时间代价:会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。

创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大。

回答

不是的,索引虽然能提高查询效率,但是多建立一个索引,就意味着新生成一个 B+ 树索引,是需要占用存储空间的,特别是在表数据量非常大的时候,索引占用的空间越大。

还有,索引越多数据库的写入性能会下降,因为每次对表进行增删改操作的时候,都需要去维护各个 B+ 树索引的有序性。

12. 什么时候不用索引更好?
#

分析

考察对索引缺点的认识。

回答

建立了索引,虽然能提升查询效率,但是它带来了两个代价,第一个是空间代价,因为需要多构建一棵 B+ 树,会占用磁盘空间。第二个是更新时间代价,每次增删改索引,都需要动态维护 B+ 树,以满足 B+ 树的有序性。

所以,我认识到如果一张表经常被增删改的话,也就是写多读少的场景下,不建立索引会更好,因为这时候维护索引的开销可能会超过索引带来的性能提升。

还有一点,如果表中某个列的值高度重复,那么建了索引也没有用,优化器会选择全表扫描,这样建立的索引会占用存储空间,也会影响增删改的效率,选择不用索引会更好。

13. 字段为什么要定义为 NOT NULL?
#

分析

来自《高性能 MySQL》中这样一段话:

尽量避免 NULL

很多表都包含可为 NULL(空值)的列,即使应用程序并不需要保存 NULL 也是如此,这是因为可为 NULL 是列的默认属性。通常情况下最好指定列为 NOT NULL,除非真的需要存储 NULL 值。

如果查询中包含可为 NULL 的列,对 MySQL 来说更难优化,因为可为 NULL 的列使得索引、索引统计和值比较都更复杂。可为 NULL 的列会使用更多的存储空间,在 MySQL 里也需要特殊处理。当可为 NULL 的列被索引时,每个索引记录需要一个额外的字节,在 MyISAM 里甚至还可能导致固定大小的索引变成可变大小的索引。

通常把可为 NULL 的列改为 NOT NULL 带来的性能提升比较小,所以调优时没有必要首先在现有 schema 中查找并修改掉这种情况,除非确定这会导致问题。但是,如果计划在列上建索引,就应该尽量避免设计成可为 NULL 的列。

当然也有例外,例如值得一提的是,InnoDB 使用单独的位(bit)存储 NULL 值,所以对于稀疏数据有很好的空间效率。但这一点不适用于 MyISAM。

回答

  1. 如果查询中包含可为 NULL 的列,对 MySQL 的优化器来说更难优化,因为可为 NULL 的列使得索引、索引统计和值比较都更复杂。
  2. 如果某列存在 NULL 的情况,可能导致 count() 等函数执行不准确,因为 count 不会统计值为 NULL 的列。
  3. NULL 值是一个没意义的值,但是它会占用物理空间,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表。

14. 索引怎么优化?
#

分析

几种常见优化索引的方法:

  • 覆盖索引优化:假设我们只需要查询商品名称和价格这两个数据,这时候我们可以对这两个字段建立联合索引,即「商品名称、价格」作为一个联合索引,针对 SELECT product_id, product_name, price FROM table WHERE product_name = 'iphone'; 的语句,这时候就利用覆盖索引优化了。因为索引中已经包含这两个字段数据了,所以查询将不会再次检索主键索引,从而避免回表,减少了大量的 I/O 操作。
  • 主键索引最好是自增的:如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
  • 防止索引失效:当我们使用左或者左右模糊匹配的时候,也就是 LIKE %xx 或者 LIKE %xx% 这两种方式都会造成索引失效;当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引匹配,否则就会导致索引失效;在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效;索引列的区分度太小也可能导致索引失效。区分度指的是索引列中不同值的数量占总记录数的比例,当比例过低时,数据库可能会认为全表扫描比走索引更高效,从而放弃使用索引。
  • 前缀索引优化:使用前缀索引可以减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

回答

我用过这几种优化的方式:

  • 对于只需要查询几个字段数据的 SQL 来说,我们可以对这些字段建立联合索引,这样查询方式就变成了覆盖索引,避免了回表,减少了大量的 I/O 操作。
  • 我们的主键索引最好是递增的值,因为索引是按顺序存储数据的,如果主键的值是随机的值,可能会引发页分裂的现象,页分裂会导致大量的内存碎片,这样索引结构不紧凑了,就会影响查询效率。
  • 我们要避免写出发生索引失效的 SQL 语句,比如不要对索引进行计算、函数、类型转换操作,联合索引要能正确使用需要遵循最左匹配原则等等。
  • 对于一些大字符串的索引,我们可以考虑用前缀索引,只对索引列的前缀部分建立索引,节省索引的存储空间,提高查询性能。

15. 建立了索引,查询的时候一定会用到索引吗?
#

分析

两个方向回答:

  • 索引失效的场景:
    • 当对索引列使用函数时,数据库无法直接使用索引进行查找,因为函数改变了索引列的原始值,使得索引失效。比如 YEAR(user_age) 对索引列 user_age 运用了函数,数据库无法直接依据索引定位到满足条件的记录,只能进行全表扫描。
-- 假设 user_age 列有索引
SELECT * FROM users WHERE YEAR(user_age) = 1990;
  • 如果索引列参与了计算,数据库同样无法使用索引进行查找。比如 price + 10 让索引列 price 参与了计算,数据库不能直接通过索引来确定符合条件的记录,只能全表扫描。
-- 假设 price 列有索引
SELECT * FROM products WHERE price + 10 < 100;
  • 在使用 LIKE 进行模糊查询时,若通配符 % 位于开头,数据库无法使用索引进行查找。比如 LIKE '%test' 以通配符 % 开头,数据库无法通过索引快速定位到满足条件的记录,只能全表扫描。不过,如果通配符不在开头,如 LIKE 'test%',数据库仍可使用索引。
-- 假设 username 列有索引
SELECT * FROM users WHERE username LIKE '%test';
  • 当查询条件中的数据类型与索引列的数据类型不一致时,数据库可能无法使用索引。要知道 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。那么这个字符串转为数字的过程,实际上背后会执行 CAST 函数。如果字符串对象如果是索引列的话,那么就会对索引进行隐式转换,这时候 CAST 函数就会对索引列,那么相当于索引用了函数,于是就会导致索引失效。
-- 假设 name 列是字符串类型且有索引
SELECT * FROM users WHERE name = 123;

-- 背后的效果执行效率是:
SELECT * FROM t_user WHERE CAST(name AS signed int) = 123;
  • 如果使用 OR 连接多个条件,且其中部分条件未使用索引,数据库可能不会使用索引。比如下面的例子,由于 user_name 列没有索引,数据库可能不会使用 user_id 列的索引,而是进行全表扫描。
-- 假设 user_id 列有索引,user_name 列无索引
SELECT * FROM users WHERE user_id = 1 OR user_name = 'test';
  • 优化器是基于成本考虑,即使查询条件使用了索引,如果走索引的查询成本太高,也不会选择走索引。

回答

不是的。

我了解到即使查询使用到了索引,也是可能不走索引的,比如:

  • 当我们查询语句对索引字段进行左模糊匹配、表达式计算、函数、隐式类型转换操作,这时候查询语句就无法走索引了,查询方式就变成了全表扫描的方式。还有我们使用联合索引进行查询的时候,如果没有遵循最左匹配原则,也是会发生索引失效的。
  • 优化器是基于成本考虑来选择查询的方式,在使用二级索引进行查询的时候,优化器会计算回表的成本和全表扫描的成本,如果回表的代价太高,优化器会选择不走索引,而是走全表扫描。

16. 如果我定义了一个 varchar 类型的日期字段,并且有一个数据是 20230922,如果这个日期字段上有索引,那如果我查询的 where 条件是 where time=20230922 不加单引号,还会命中索引吗?为什么?
#

分析

不会命中索引。

要明白这个原因,首先我们要知道 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。那么这个字符串转为数字的过程,实际上背后会执行 CAST 函数。

-- 查询语句
SELECT * FROM t_user WHERE time = 20230922;

-- 背后的效果执行效果是:
SELECT * FROM t_user WHERE CAST(time AS signed int) = 20230922;

而题目中的字符串对象是 time,也就是索引字段,这时候 CAST 函数就会作用到 time 索引字段,相当于对索引字段进行了函数计算,因此就会发生索引失效。

而如果反过来,id 是索引且是整型类型,那么下面这条语句就不会发生索引失效,因为字符串对象是 "1",是在它身上发生函数计算,id 并不会发生函数计算,所以没有问题。

-- 查询
SELECT * FROM t_user WHERE id = "1";

-- 等价于
SELECT * FROM t_user WHERE id = CAST("1" AS signed int);

回答

不会命中索引。

因为 MySQL 在遇到字符串和数字比较的时候,会发生隐式类型转换,会将字符串的对象转为数字,这个转换的过程实际上会涉及到函数。你说的这个查询,日期字段是字符串,那么发生隐式类型转换的时候,就会作用在日期这个索引字段上,对索引进行函数计算的话,是会发生索引失效的。

17. MySQL 最新版本解决了索引失效的哪些情况了吗?
#

分析

MySQL 8.0 新特性:函数索引和索引跳跃扫描机制。

  • 函数索引

MySQL 8.0 索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

举个例子,我通过下面这条语句,对 length(name) 的计算结果建立一个名为 idx_name_length 的索引。

ALTER TABLE t_user ADD KEY idx_name_length ((length(name)));

然后我再用下面这条查询语句,这时候就会走索引了。

EXPLAIN SELECT * FROM t_user WHERE length(name) = 6;

alt text

  • 索引跳跃扫描机制

MySQL 8.0 新增索引跳跃扫描机制,支持不符合联合索引最左前缀原则条件下的 SQL,依然能够使用联合索引,减少不必要的扫描。

alt text

但是索引跳跃的使用也是有前提条件的,要满足下面这些条件才能用得到:

  1. 查询只能涉及一张表,多表关联无法使用该特性。
  2. 查询 SQL 不能使用 GROUP BY 或者 DISTINCT 子句。
  3. 查询字段必须是索引中的字段,所以只要涉及回表的查询,就无法用到索引跳跃。
  4. 组合索引形式:([A_1, ..., A_k], B_1, ..., B_m, C[, D_1, ..., D_n])AD 可以为空,但是 BC 不能为空。只要查询的时候中间的索引字段 B 没用到,就会用不了索引跳跃的优化。

前面2个条件好理解,后面2个条件,我举一些例子来跟大家说明,假设test表有 (a, b, c) 联合索引。

  • SELECT a, c, b FROM test WHERE a = 100 AND c = 100; 没用到跳跃索引的优化,因为没满足条件 4。

    alt text

  • SELECT a, c, b FROM test WHERE c = 100; 没用到跳跃索引的优化,因为没满足条件 4。

    alt text

  • SELECT a, c, b FROM test WHERE b = 100;SELECT a, c, b FROM test WHERE b = 1 AND c = 100; 都用了跳跃索引优化,因为满足了所有条件。

    alt text
    alt text

  • SELECT * FROM test WHERE b = 1 AND c = 100; 没用到跳跃索引的优化,因为没满足条件 3(查询字段必须是索引中的字段)。

    alt text

回答

我了解到 MySQL 8.0 可以给字段增加函数索引,这个新特性可以解决对索引使用函数的时候,索引失效的问题。

还有一个新特性是索引跳跃式扫描,5.7 版本之前,使用联合索引的时候,如果不满足最左匹配原则,就会发生索引失效,而 8.0 出了索引跳跃式扫描特性之后,即使没有遵循最左匹配原则,部分场景下,依然可以使用联合索引。

18. 什么是最左匹配原则?
#

分析

要知道联合索引的结构,才能理解最左匹配原则。

如果创建了一个 (a, b, c) 联合索引,联合索引的索引顺序是先按 a 排序,在 a 相同的情况下再按 b 排序,在 b 相同的情况下再按 c 排序。

因此,使用联合索引时,存在最左匹配原则。

例如,如果有一个联合索引 (a, b, c),当查询条件为 WHERE a = 1 AND b = 2 时,MySQL 可以使用这个索引进行查询,因为查询条件匹配了索引的最左边的两个列。但是如果查询条件为 WHERE b = 2 AND c = 3,则 MySQL 无法使用这个索引进行查询,因为查询条件不匹配索引的最左边的列。

回答

假设有一个 (a, b, c) 联合索引,它的存储顺序是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。由于这个特性,在使用联合索引时,存在最左匹配原则,具体的规则:

  • MySQL 会从联合索引从最左边的索引列开始匹配查询条件,然后依次从左到右的顺序匹配,如果查询条件没有使用到某个列,那么该列右边的所有列都无法使用走索引。
  • 当查询条件中使用了某个列,但是该列的值包含范围查询,范围查询的字段可以用到联合索引,但是在范围查询字段后面的字段无法用到联合索引。

所以,我们在使用联合索引的时候,要遵守最左匹配原则,否则可能会出现部分索引字段走不了索引。

19. 建立联合索引有什么需要注意的?
#

分析

建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。

区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:

alt text

比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。

因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。

回答

最好把区分度比较大的字段放在联合索引最左侧,有助于提高索引的过滤效果,比如 UUID 这类字段就比较适合排在联合索引列的靠前的位置。

如果区分度很低的字段放在了联合索引最左侧,有可能会导致查询优化器会选择全表扫描,而不走索引了。

20. 了解索引下推吗?什么情况下会下推到引擎去处理?
#

分析

索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。

举一个具体的例子,方便大家理解。这里一张用户表对 agereward 字段建立了联合索引 (age, reward)

alt text

现在有下面这条查询语句:

SELECT * FROM t_user WHERE age > 20 AND reward = 100000;

联合索引当遇到范围查询时就会停止匹配,也就是 age 字段能用到联合索引,但是 reward 字段则无法利用到索引。

那么,不使用索引下推(MySQL 5.6 之前的版本)时,执行器与存储引擎的执行流程是这样的:

  • Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
  • 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给 Server 层;
  • Server 层在判断该记录的 reward 是否等于 100000,如果成立则将其发送给客户端;否则跳过该记录;
  • 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引上定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层;
  • 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,没有索引下推的时候,每查询到一条二级索引记录,都要进行回表操作,然后将记录返回给 Server,接着 Server 再判断该记录的 reward 是否等于 100000

而使用索引下推后,判断记录的 reward 是否等于 100000 的工作交给了存储引擎层,过程如下:

  • Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
  • 存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引中包含的列(reward 列)的条件(reward 是否等于 100000)是否成立。如果条件不成立,则直接跳过该二级索引;如果成立,则执行回表操作,将完成记录返回给 Server 层。
  • Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
  • 如此往复,直到存储引擎把表中的所有记录读完。

可以看到,使用了索引下推后,虽然 reward 列无法使用到联合索引,但是因为它包含在联合索引 (age, reward) 里,所以直接在存储引擎过滤出满足 reward = 100000 的记录后,才去执行回表操作获取整行记录。相比于没有使用索引下推,节省了很多回表操作。

当执行计划里的 Extra 部分显示了 Using index condition,说明使用了索引下推。

alt text

回答

索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。

举个例子,联合索引 (a, b, c),查询条件为 a = ? AND c = ? 的时候,由于联合索引的最左匹配原则,c 是无法走索引的。在没有索引下推机制之前,查询语句走二级索引的时候,需要回表读取 c 的值,然后在 Server 层进行过滤。有了索引下推机制后,即使 c 无法走索引,但是由于 c 在二级索引里,那么将过滤 c 的工作从 Server 层下推到存储引擎层,这样直接在二级索引里过滤满足 c 条件的记录,减少了回表的次数。

21. 联合索引 (a, b, c),下面的查询语句会不会走索引?如果走具体是哪些字段能走?
#

  1. SELECT * FROM T WHERE a = 1 AND b = 2 AND c = 3;
  2. SELECT * FROM T WHERE a = 1 AND b > 2 AND c = 3;
  3. SELECT * FROM T WHERE c = 1 AND a = 2 AND b = 3;
  4. SELECT * FROM T WHERE a = 2 AND c = 3;
  5. SELECT * FROM T WHERE b = 2 AND c = 3;
  6. SELECT (a, b) FROM T WHERE a = 1 AND b > 2;

分析

大厂面试的时候,喜欢出这种题目,列几条 SQL 语句让你肉眼判断走不走索引,其实也是在考察你对最左匹配原则的理解。

回答

  1. 遵循最左匹配原则,所以 abc 三个字段都可以走索引,查询方式是在联合索引找到主键值后,会回主键索引找完整的数据行。
  2. 根据最左匹配原则,范围查询后面的字段无法使用索引,所以 ab 可以走索引,c 无法走索引,不过 c 可以进行索引下推。
  3. abc 都能走索引,因为 WHERE 查询条件字段的顺序并不会影响,MySQL 优化器会帮我们调整字段的查询顺序,所以也是符合最左匹配原则的。
  4. a 能走索引,根据最左匹配原则,c 无法走索引,但是 c 可以被索引下推。
  5. 根据最左匹配原则,bc 都无法走索引。
  6. ab 都能走索引,查询方式是覆盖查询,不需要回表。

22. WHERE a > 1 AND b = 2 AND c < 3 怎么建立索引?
#

分析

这题属于上一题的反向思维,根据查询条件,创建联合索引,来提高这条语句的查询效率,所以就要创建一个能让更多字段能走索引的联合索引。

假设:

  • 创建 (abc)(acb)(ab)(ac) 联合索引,只有 a 能索引。
  • 创建 (cab)(cba)(ca)(cb) 联合索引,只有 c 能索引。
  • 创建 (ba) 联合索引,ba 都能走索引。
  • 创建 (bc) 联合索引,bc 都能走索引。
  • 创建 (bac) 联合索引,ba 都能走索引,但比 (ba) 联合索引多了一个好处,c 字段能索引下推,会减少回表的次数。
  • 创建 (bca) 联合索引,bc 都能走索引,但比 (bc) 联合索引多了一个好处,a 字段能索引下推,会减少回表的次数。

回答

我会创建 (bac) 联合索引或者 (bca) 联合索引,因为这两种联合索引都可以有 2 个字段走索引。比如:

  • 创建 (bac) 联合索引,ba 都能走索引,c 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数;
  • 创建 (bca) 联合索引,bc 都能走索引,a 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数。

23. WHERE a = ? AND b = ? ORDER BY c 怎么建立索引?
#

分析

这里有 ORDER BY 排序,我们尽量要用索引来避免额外排序的操作,可以考虑建立 (a, b, c) 联合索引,因为 c 有序的前提是建立在 a = ? AND b = ? 的场景下,刚好符合这个查询条件,这样 c 就不需要额外排序了,天然利用了索引的有序性。

回答

可以建立 (a, b, c) 联合索引,这样 c 排序的时候,就能利用索引的有序性,避免 using filesort 了。

24. WHERE a > 100 AND b = 100 AND c = 123 ORDER BY d 怎么建立联合索引?
#

分析

如果是 bcad 联合索引的话,虽然 bca 能走索引,但是排序 d 无法利用索引,会发生 file sort(因为 a > 100 范围查询后获得的记录,d 并不一定是有序的了,所以需要额外排序,d 有序的前提是 a 相等的情况下)。

alt text

如果 bcda 联合索引,d 不仅能利用索引有序性,避免 file sorta 虽然走不了索引,但是可以索引下推,所以建立 (bcda) 联合索引会比较好。

alt text

回答

我觉得建立 bcda 顺序的联合索引比较好,这时候 bc 字段都能走索引,而且 d 能利用索引有序性,避免 filesort。最后的 a 字段虽然无法走索引,但是可以利用索引下推,减少回表的次数。

25. SELECT b FROM table WHERE a = 10 AND c > 20 怎么创建索引?
#

分析

优先考虑能让 WHERE 查询中的字段能走索引,所以可以考虑 (a, c) 联合索引,这样查询的时候,ac 都能走联合索引,然后再考虑 SELECT 的列是否能索引覆盖。很明显这个查询场景,只需要查询 b 列,那么我们可以考虑创建 (a, c, b) 联合索引,这时候查询的时候,ac 既能走索引,也能索引覆盖,避免了回表。

回答

可以考虑创建 (a, c, b) 顺序的联合索引,这时候查询的时候,ac 既能都走索引,也能利用索引覆盖的特性,避免了回表。

26. SELECT id, name FROM XX WHERE age > 10 AND name LIKE 'xx%',有联合索引 (name, age),说一下查询过程
#

分析

有三点需要说出来:

  • 能不能走索引?哪些字段能走索引?能走索引,name 能走索引,age 不能走索引。
  • 哪个字段能索引下推?age 字段能索引下推。
  • 查询需不需要回表?不需要回表,索引覆盖查询。

回答

联合索引的顺序是先 name,再 age,结构上是先根据 name 排序,name 相等的情况下再根据 age 排序。所以优化器需要先匹配 namename 这时候是右模糊查询,并不会发生索引失效,所以这条 SQL 是能走联合索引的。具体的话,只有 name 能走索引,这是因为由于 name 右模糊查询后,age 字段的值并不是有序的,因此 age 无法走索引,但是 age 可以进行索引下推。

最后查询的字段是 idname,这两个字段都能在联合索引上查找到,所以不需要回表,是索引覆盖查询。

27. WHERE id NOT IN (?, ?, ?) 会走索引吗?
#

分析

IN 能不能走索引,关键是看查询成本,没有绝对说 IN 会发生索引失效,也没有绝对说 IN 一定能走索引。

回答

要看查询成本,如果走某个索引花费的随机 I/O 比从聚簇索引顺序查(顺序 I/O)的成本都还要高,那还不如直接去全表扫描。

举例:num 字段(非唯一二级索引)只包含 3 个值,1233 只有几行,而 12 各有 100w 行。如果查询条件是 NOT IN (1, 2) 会走索引,如果查询条件是 NOT IN (3) 不会走索引。

28. 如果查询条件中包含索引列和非索引列,MySQL 的具体查询流程是什么样的?
#

分析

假设 a 是索引列,d 是非索引列,SELECT a FROM test WHERE a = ? AND d = ? 查询过程先按索引去查,然后回表再过滤非索引列,会涉及回表的过程。

alt text
alt text

从执行计划也可以看到,Extra 没有显示 Using index,代表没有覆盖索引,然后走了二级索引,所以查询过程有发生回表。

回答

查询过程先按索引去二级索引 B+ 树查,然后拿到主键 id,回表到主键索引 B+ 树再过滤非索引列。查询过程会查 2 个 B+ 树,涉及回表的过程。

MySQL面试题库 - 这篇文章属于一个选集。
§ 4: 本文