MySQL面试题总结

  1. 1. 基础
    1. 1.1 数据库的三范式是什么?
    2. 1.2 MySQL支持哪些存储引擎?
    3. 1.3 超键、候选键、主键、外键分别是什么?
    4. 1.4 SQL约束有哪几种?
    5. 1.5 MySQL中的varchar和char有什么区别?
    6. 1.6 MySQL中in和exists区别?
    7. 1.7 drop、delete与truncate的区别?
    8. 1.8 什么是存储过程?有哪些优缺点?
    9. 1.9 MySQL执行查询的过程?
  2. 2. 事务
    1. 2.1 什么是数据库事务?
    2. 2.2 事务具有的四个特征?
    3. 2.3 MySQL的四种隔离级别?
    4. 2.4 什么是脏读、不可重复读与幻读?
    5. 2.5 事务的实现原理?
    6. 2.6 介绍一下MySQL事务日志?
    7. 2.7 什么是MySQL的binlog?
    8. 2.8 在事务中可以混合使用存储引擎吗?
    9. 2.9 什么是MVCC?
    10. 2.10 MVCC的实现
  3. 3. 锁
    1. 3.1 为什么要加锁?
    2. 3.2 按照锁的粒度分数据库锁有哪些?
    3. 3.3 从锁的类别上分MySQL都有哪些锁呢?
    4. 3.4 数据库的乐观锁和悲观锁是什么?怎么实现的?
    5. 3.5 InnoDB引擎的行锁是怎么实现的?
    6. 3.6 什么是死锁?怎么解决?
    7. 3.7 隔离级别与锁的关系?
    8. 3.8 优化锁方面的意见?
  4. 4. 索引
    1. 4.1 索引是什么?
    2. 4.2 索引有哪些优缺点?
    3. 4.3 MySQL有哪几种索引类型?
    4. 4.4 说一说索引的底层实现?
    5. 4.5 为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?
    6. 4.6 讲一讲聚簇索引与非聚簇索引?
    7. 4.7 非聚簇索引一定会回表查询吗?
    8. 4.8 联合索引是什么?为什么需要注意联合索引中的顺序?
    9. 4.9 MySQL的最左前缀原则是什么?
    10. 4.10 前缀索引是什么?
    11. 4.11 如何创建索引?
    12. 4.12 创建索引时需要注意什么?

MySQL 常见面试题总结,文章将不断更新。

1. 基础

1.1 数据库的三范式是什么?

  • 第一范式(1NF):强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
  • 第二范式(2NF):要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
  • 第三范式(3NF):任何非主属性不依赖于其它非主属性。

1.2 MySQL支持哪些存储引擎?

MySQL 支持多种存储引擎,比如 InnoDB、MyISAM、Memory、Archive 等等。在大多数的情况下,直接选择使用 InnoDB 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。

MyISAM 和 InnoDB 的区别有哪些:

  • InnoDB 支持事务,MyISAM 不支持。
  • InnoDB 支持外键,MyISAM 不支持。
  • InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。
  • InnoDB 不支持全文索引,MyISAM 支持全文索引,查询效率上 MyISAM 更高。
  • InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。
  • MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

1.3 超键、候选键、主键、外键分别是什么?

  • 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键
  • 候选键:是最小超键,即没有冗余元素的超键。
  • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(NULL)。
  • 外键:在一个表中存在的另一个表的主键称此表的外键。

1.4 SQL约束有哪几种?

  • NOT NULL:用于控制字段的内容一定不能为空(NULL)。
  • UNIQUE:控制字段内容不能重复,一个表允许有多个 UNIQUE 约束。
  • PRIMARY KEY:也是用于控制字段内容不能重复,但它在一个表只允许出现一个。
  • FOREIGN KEY:用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
  • CHECK:用于控制字段的值范围。

1.5 MySQL中的varchar和char有什么区别?

char 是一个定长字段,假如申请了 char(10) 的空间,那么无论实际存储多少内容,该字段都占用10个字符;而 varchar变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度 + 1,最后一个字符存储使用了多长的空间。

在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar,例如存储用户 MD5 加密后的密码,则可以使用 char

1.6 MySQL中in和exists区别?

MySQL 中的 in 语句是把外表和内表作 Hash 连接,而 exists 语句是对外表作 Loop 循环,每次 Loop 循环再对内表进行查询。一直大家都认为 existsin 语句的效率要高,这种说法其实是不准确的。这个是要区分环境的:

  • 如果查询的两个表大小相当,那么用 inexists 差别不大。
  • 如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in
  • not innot exists:如果查询语句使用了 not in,那么内外表都进行全表扫描,没有用到索引;而 not extsts 的子查询依然能用到表上的索引。所以无论哪个表大,用 not exists 都比 not in 要快。

1.7 drop、delete与truncate的区别?

三者都表示删除,但是三者有一些差别:

delete truncate drop
类型 属于 DML 属于 DDL 属于 DDL
回滚 可回滚 不可回滚 不可回滚
删除内容 表结构还在,删除表的全部或者一部分数据 表结构还在,删除表中的所有数据 从数据库中删除表,所有的数据行、索引和权限也会被删除
删除速度 删除速度慢,需要逐行删除 删除速度快 删除速度最快

1.8 什么是存储过程?有哪些优缺点?

存储过程是一些预编译的 SQL 语句。

  1. 更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用它就行了。
  2. 存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T-SQL 语句,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全。

但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好。

1.9 MySQL执行查询的过程?

  1. 客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配。
  2. 查缓存(当判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中)。
  3. 语法分析(SQL 语法是否写错了):如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。
  4. 优化:是否使用索引,生成执行计划。
  5. 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。

更新语句执行会复杂一点,需要检查表是否有排它锁,写 binlog、刷盘、是否执行 commit

2. 事务

2.1 什么是数据库事务?

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。

假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

2.2 事务具有的四个特征?

事务就是一组原子性的操作,这些操作要么全部发生,要么全部不发生。事务把数据库从一种一致性状态转换成另一种一致性状态。

  • 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。
  • 一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
  • 隔离性(Isolation):一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持续性(Durability):也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

2.3 MySQL的四种隔离级别?

  • Read Uncommitted(读取未提交内容)
    在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
  • Read Committed(读取提交内容)
    这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交的事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。
  • Repeatable Read(可重读)
    这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。
  • Serializable(可串行化)
    通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
隔离级别 脏读 不可重复读 幻影读
Read Uncommitted
Read Committed
Repeatable Read
Serializable

MySQL 默认采用的是 REPEATABLE-READ 隔离级别,Oracle 默认采用的是 READ-COMMITTED 隔离级别。

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是 MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容),但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。

InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE(可串行化)隔离级别。

2.4 什么是脏读、不可重复读与幻读?

  • 脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据。
  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。
  • 幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

总结:不可重复读侧重于修改,幻读侧重于新增或删除(多了或少了行),脏读是一个事务回滚影响另外一个事务。

2.5 事务的实现原理?

事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。

每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。

每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。

2.6 介绍一下MySQL事务日志?

InnoDB 事务日志包括 redo log 和 undo log。

undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。

事务日志的目的:实例或者介质失败,事务日志文件就能派上用场。

  • redu log
    redo log 不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo log 中。具体的落盘策略可以进行配置。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。redo log 是为了实现事务的持久性而出现的产物。
  • undo log
    undo log 用来回滚行记录到某个版本。事务未提交之前,undo log 保存了未提交之前的版本数据,undo log 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。

2.7 什么是MySQL的binlog?

MySQL 的 binlog 是记录所有数据库表结构变更(例如 CREATEALTER TABLE)以及表数据修改(例如 INSERTUPDATEDELETE)的二进制日志。binlog 不会记录 SELECTSHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。

MySQL binlog 以事件形式记录,还包含语句执行所消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。

binlog 有三种格式,各有优缺点:

  • statement:基于 SQL 语句的模式,某些语句和函数如 UUIDLOAD DATA INFILE 等在复制过程中可能导致数据不一致甚至出错。
  • row:基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。
  • mixed:混合模式,根据语句来选用是 statement 还是 row 模式。

2.8 在事务中可以混合使用存储引擎吗?

尽量不要在同一个事务中使用多种存储引擎,MySQL 服务器层不管理事务,事务是由下层的存储引擎实现的。

如果在事务中混合使用了事务型和非事务型的表(例如 InnoDB 和 MyISAM 表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。

2.9 什么是MVCC?

MVCC,即多版木并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

2.10 MVCC的实现

对于 InnoDB,聚簇索引记录中包含3个隐藏的列:

  • ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
  • 事务 ID:记录最后一次修改该记录的事务 ID。
  • 回滚指针:指向这条记录的上一个版本。

我们举个例子,假如现在有两个事务:

  • 事务1:insert into t1(a, b) values (1, 1);
  • 事务2:update t1 set b = 666 where a = 1;

如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为1,b 字段为1,ROW ID 也为1,事务 ID 假设为1,回滚指针假设为 null。当执行 update t1 set b = 666 where a = 1 时,大致步骤如下:

  • 数据库会先对满足 a = 1 的行加排他锁;
  • 然后将原记录复制到 undo 表空间中;
  • 修改 b 字段的值为666,修改事务 ID 为2;
  • 并通过隐藏的回滚指针指向 undo log 中的历史记录;
  • 事务提交,释放前面对满足 a = 1 的行所加的排他锁。

因此可以总结出 MVCC 实现的原理大致是:

InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。

MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的隔离性。

3. 锁

3.1 为什么要加锁?

当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能读取和存储不正确的数据,破坏数据库的一致性。因此需要加锁使得在多用户环境下保证数据库的完整性和一致性。

3.2 按照锁的粒度分数据库锁有哪些?

在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(InnoDB 引擎)、表级锁(MyISAM 引擎)和页级锁(BDB引擎)。

  • 行级锁
    • 行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。
    • 开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁
    • 表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
    • 开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 页级锁
    • 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁之间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。所以取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。
    • 开销和加锁时间界于表锁和行锁之间,会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般。

MyISAM 和 InnoDB 存储引擎使用的锁:

  • MyISAM 采用表级锁(table-level locking)。
  • InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

3.3 从锁的类别上分MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

  • 共享锁:又叫做读锁,当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
  • 排他锁:又叫做写锁,当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,它和其它的排他锁,共享锁都相斥。

用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。

锁的粒度取决于具体的存储引擎,InnoDB 实现了行级锁,页级锁,表级锁。他们的加锁开销从大到小,并发能力也是从大到小。

3.4 数据库的乐观锁和悲观锁是什么?怎么实现的?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定。实现方式:乐观锁一般会使用版本号机制或 CAS 算法实现。

两种锁的使用场景:

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

3.5 InnoDB引擎的行锁是怎么实现的?

InnoDB 是基于索引来完成行锁的。

例如:select * from tab_with_index where id = 1 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么 InnoDB 将完成表锁,并发将无从谈起。

3.6 什么是死锁?怎么解决?

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。常见的解决死锁的方法有:

  • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。

如果业务处理不好可以用分布式事务锁或者使用乐观锁。

3.7 隔离级别与锁的关系?

  • 在 Read Uncommitted 级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突。
  • 在 Read Committed 级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁。
  • 在 Repeatable Read 级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
  • SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

3.8 优化锁方面的意见?

  • 使用较低的隔离级别。
  • 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突。
  • 选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。例如,修改数据的话最好申请排他锁,而不是先申请共享锁,修改时再申请排他锁,这样会导致死锁。
  • 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能固定顺序地获取表中的行,这样将大大减少死锁的机会。
  • 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响。
  • 不要申请超过实际需要的锁级别。
  • 数据查询的时候不是必要,不要使用加锁。MySQL 的 MVCC 可以实现事务中的查询不用加锁,优化事务性能:MVCC 只在 Read Committed(读提交)和 Repeatable Read(可重复读)两种隔离级别。
  • 对于特定的事务,可以使用表锁来提高处理速度或者减少死锁的可能。

4. 索引

4.1 索引是什么?

索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用 B 树及其变种 B+ 树。更通俗地说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。而且索引是一个文件,它是要占据物理空间的。

MySQL 索引的建立对于 MySQL 的高效运行是很重要的,索引可以大大提高 MySQL 的检索速度。比如我们在查字典的时候,前面都有检索的拼音和偏旁、笔画等,然后找到对应字典页码,打开字典的页数就可以知道我们要搜索的某一个 key 的全部值的信息了。

4.2 索引有哪些优缺点?

索引的优点:

  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

索引的缺点:

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增删改的执行效率。
  • 空间方面:索引需要占用物理空间。

4.3 MySQL有哪几种索引类型?

  • 从存储结构上来划分:BTree 索引(B-Tree 或 B+Tree 索引)、Hash 索引、full-index 全文索引、R-Tree 索引。这里所描述的是索引存储时保存的形式。
  • 从应用层次来分:普通索引、唯一索引、复合索引。
    • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。
    • 唯一索引:索引列的值必须唯一,但允许有空值。
    • 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
    • 聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB 的聚簇索引其实就是在同一个结构中保存了 B-Tree 索引(技术上来说是 B+Tree)和数据行。
    • 非聚簇索引:不是聚簇索引,就是非聚簇索引。
  • 根据表中数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。

4.4 说一说索引的底层实现?

  • Hash 索引:基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且 Hash 索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。
  • B-Tree 索引(MySQL 使用 B+Tree):B-Tree 能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。
  • B+Tree 索引:B-Tree 的改进版本,同时也是数据库索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比 B-Tree 来说,进行范围查找时只需要查找两个节点,进行遍历即可。而 B-Tree 需要获取所有节点,相比之下 B+Tree 效率更高。
    B+Tree 性质:
    • n 棵子树的节点包含 n 个关键字,不用来保存数据而是保存数据的索引。
    • 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身根据关键字的大小自小而大顺序链接。
    • 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
    • B+ 树中,数据对象的插入和删除仅在叶节点上进行。
    • B+ 树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

4.5 为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?

  • B-tree:从两个方面来回答:
    • B+ 树的磁盘读写代价更低:B+ 树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对 B 树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对 IO 读写次数就降低了。
    • 由于 B+ 树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是 B 树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以 B+ 树更加适合区间查询的情况,所以通常 B+ 树用于数据库索引。
  • Hash:
    • 虽然可以快速定位,但是没有顺序,IO 复杂度高;
    • 基于 Hash 表实现,只有 Memory 存储引擎显式支持哈希索引;
    • 适合等值查询,如 =in()<=>,不支持范围查询;
    • 因为不是按照索引值顺序存储的,就不能像 B+Tree 索引一样利用索引完成排序;
    • Hash 索引在查询等值时非常快;
    • 因为 Hash 索引始终索引所有列的全部内容,所以不支持部分索引列的匹配查找;
    • 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。
  • 二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且 IO 代价高。
  • 红黑树:树的高度随着数据量增加而增加,IO 代价高。

4.6 讲一讲聚簇索引与非聚簇索引?

在 InnoDB 里,索引 B+Tree 的叶子节点存储了整行数据为主键索引,也被称之为聚簇索引,即将数据存储与索引放到了一块,找到索引也就找到了数据。

而索引 B+Tree 的叶子节点存储了主键的值为非主键索引,也被称之为非聚簇索引、二级索引。

聚簇索引与非聚簇索引的区别:

  • 非聚簇索引与聚簇索引的区别在于非聚簇索引的叶子节点不存储表中的数据,而是存储该列对应的主键(行号)。
  • 对于 InnoDB 来说,想要查找数据我们还需要根据主键再去聚簇索引中进行查找,这个再根据聚簇索引查找数据的过程,我们称为回表。第一次索引一般是顺序 IO,回表的操作属于随机 IO。需要回表的次数越多,即随机 IO 次数越多,我们就越倾向于使用全表扫描。
  • 通常情况下,主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可。

注意:MyISAM 无论主键索引还是二级索引都是非聚簇索引,而 InnoDB 的主键索引是聚簇索引,二级索引是非聚簇索引。我们自己建立的索引基本都是非聚簇索引。

4.7 非聚簇索引一定会回表查询吗?

不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为“覆盖索引”。举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行 select score from stuaent where score > 90 的查询时,在索引的叶子节点上,已经包含了 score 信息,不会再次进行回表查询。

4.8 联合索引是什么?为什么需要注意联合索引中的顺序?

MySQL 可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。具体原因为:

  • MySQL 使用索引时需要索引有序,假设现在建立了 name, age, school 的联合索引,那么索引的排序为:先按照 name 排序,如果 name 相同,则按照 age 排序,如果 age 的值也相等,则按照 school 进行排序。
  • 当进行查询时,此时索引仅仅按照 name 严格有序,因此必须首先使用 name 字段进行等值查询,之后对于匹配到的列而言,其按照 age 字段严格有序,此时可以使用 age 字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

4.9 MySQL的最左前缀原则是什么?

最左前缀原则就是最左优先,在创建多列索引时,要根据业务需求,where 子句中使用最频繁的一列放在最左边。MySQL 会一直向右匹配直到遇到范围查询><betweenlike)就停止匹配,比如:对于 a = 1 and b = 2 and c > 3 and d = 4,如果建立 (a, b, c) 顺序的索引,d 是用不到索引的,如果建立 (a, b, d, c) 的索引则都可以用到,a, b, d 的顺序可以任意调整。=in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立 (a, b, c) 索引可以任意顺序,MySQL 的查询优化器会帮你优化成索引可以识别的形式。

4.10 前缀索引是什么?

因为可能我们索引的字段非常长,这既占内存空间,也不利于维护。所以我们就想,如果只把很长字段的前面的公共部分作为一个索引,就会产生超级加倍的效果。但是,我们需要注意,order by 不支持前缀索引。

创建前缀索引的流程如下:

  • 先计算完整列的选择性:select count(distinct col_1)/count(1) from table_1
  • 再计算不同前缀长度的选择性:select count(distinct left(col_1, 4))/count(1) from table_1
  • 找到最优长度之后,创建前缀索引:create index idx_front on table_1 (col_1(4))

4.11 如何创建索引?

创建索引有以下三种方式:

(1)在执行 CREATE TABLE 时创建索引:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE table_name (
id INT auto_increment PRIMARY KEY,
first_name VARCHAR (16),
last_name VARCHAR (16),
id_card VARCHAR (18),
information text,
KEY name (first_name, last_name),
FULLTEXT KEY (information) , -- 在information字段上建立全文索引
UNIQUE KEY (id_card) -- 在id_card字段上建立唯一索引
);

(2)使用 ALTER TABLE 命令添加索引:

1
ALTER TABLE table_name ADD INDEX index_name (column_list);

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

其中 table_name 是要增加索引的表名,column_list 指出对哪些列进行索引,如果索引多列则各列之间用逗号分隔。

索引名 index_name 可自己命名,缺省时,MySQL 将根据第一个索引列赋一个名称。另外,ALTER TABLE 允许在单个语句中更改多个表,因此可以同时创建多个索引。

(3)使用 CREATE INDEX 命令创建索引:

1
CREATE INDEX index_name ON table_name (column_list);

4.12 创建索引时需要注意什么?

  • 非空字段:应该指定列为 NOT NULL,除非你想存储 NULL。在 MySQL 中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0或者一个特殊的值或者一个空串代替空值;
  • 取值离散(变量各个取值之间的差异程度)大的字段的列放到联合索引的前面,可以通过 count() 函数查看字段的差异值,返回值越大说明字段的唯一值越多,字段的离散程度高;
  • 索引字段越小越好:数据库的数据存储以页为单位,一页存储的数据越多则一次 I/O 操作获取的数据越多,效率越高。