Mysql面试题库
Mysql面试题库
1. count主键和count非主键结果会不同吗?
分析
count()函数是返回表中某个列的非NULL值的数量。
由于主键的列不能存NULL值,所以 count(主键) 返回的结果,可以表示数据库表中所有行数据的数量。
由于非主键的列可以存NULL值,那么count(非主键)返回表中非主键列的非NULL值的数量。
回答
主键是不能存NULL值的,所以 count 主键代表统计表中所有行数据的数量。
而非主键是可以存NULL值的,所以 count 非主键统计的是表中这个列的非NULL值的数量。
推荐学习
count(*) 和 count(1) 有什么区别?哪个性能最好?
2. MySQL内连接、外连接有什么区别?
分析
内连接(INNER JOIN):内连接返回两个表中匹配的行,即只返回两个表中共有的数据。

外连接(OUTER JOIN):外连接则返回两个表中匹配和不匹配的行。MySQL 外连接主要有左外连接(LEFT JOIN)、右外连接(RIGHT JOIN)两种。
- 左连接(LEFT JOIN):
SELECT * FROM A LEFT JOIN B ON A.A_id = B.B_id,将返回左表 A 中的所有行和右表 B 中与之匹配的行。如果 B 表中没有匹配的行,则 B 表相关的列使用 NULL 值填充。

- 右连接(RIGHT JOIN):
SELECT * FROM A RIGHT JOIN B ON A.A_id = B.B_id,将返回右表 B 中的所有行和左表 A 中与之匹配的行。如果 A 表中没有匹配的行,则 A 表相关的列使用 NULL 值填充。

回答
内连接和外连接都是用于连表查询。
内连接是只返回两个表匹配的数据行,外连接可以返回两个表匹配和不匹配的数据行,外连接主要分为左连接和右连接。
左连接返回左表中的所有行和右表中匹配的行,如果右表中没有匹配的行,则用 NULL 值填充。
右连接返回右表中的所有行和左表中匹配的行,如果左表中没有匹配的行,则用 NULL 值填充
推荐学习
3. 外连接时 on 和 where 过滤条件区别?
分析

对于内连接(inner join)查询,
WHERE和ON中的过滤条件等效;对于外连接(outer join)查询,
ON中的过滤条件在连接时进行,WHERE中的过滤条件在连接操作之后执行。
回答
在外连接中,使用 on 和 where 过滤条件的区别在于:
on 用于指定连接两个表的条件,通常用于指定两个表之间的关联条件,即连接条件,在连接时进行过滤。
where 用于指定过滤条件,对连接后的结果集进行进一步筛选。
推荐学习
SQL 面试题:WHERE 和 HAVING、ON 有什么区别?
4. having与where的区别?
分析
WHERE与HAVING的根本区别在于:
WHERE子句在GROUP BY分组和聚合函数之前对数据行进行过滤,where 子句无法使用聚合函数。HAVING子句对GROUP BY分组和聚合函数之后的数据行进行过滤,having 子句可以使用聚合函数。
回答
在 GROUP BY 分组查询过程中,Where 是工作在GROUP BY之前,Where 是对分组之前的数据进行筛选,无法使用聚合函数,Having 是工作在GROUP BY之后,Having 主要对分组之后的数据进行筛选,可以使用聚合函数。
推荐学习
5. EXISTS 和 IN的区别是什么?
分析
in 和 exists 一般用于子查询,语法如下:
select * from A where id in (select id from B);
select * from A where exists (select 1 from B where A.id=B.id);性能区别:
当A表(外表)数据与B表(内表)数据一样大时,in与exists效率差不多,可任选一个使用。
IN适合于外表大而内表小的情况,EXISTS适合于外表小而内表大的情况。
in 的工作原理:
- IN()里的查询只会执行一次,它查出B表中的所有id字段并缓存起来。之后,检查A表的id是否与B表中的id相等(在内存判断),如果相等则将A表的记录加入结果集中,直到遍历完A表的所有记录。它的查询过程类似于以下过程:
List resultSet={};
Array A=(select * from A);
Array B=(select id from B);
for(int i=0;i<A.length;i++) {
for(int j=0;j<B.length;j++) {
if(A[i].id==B[j].id) {
resultSet.add(A[i]);
break;
}
}
}
return resultSet;可以看出,当B表数据较大时不适合使用in(),因为它会把B表数据全部遍历一次。
**例1:**A表有10000条记录(小表),B表有1000000条记录(大表),那么最多有可能遍历10000*1000000次,效率很差。
**例2:**A表有10000条记录(大表),B表有100条记录(小表),那么最多有可能遍历10000*100次,遍历次数大大减少,效率大大提升。
exist 工作原理:
exists()会执行A.length次,它并不缓存exists()结果集,因为exists()结果集的内容并不重要,重要的是其内查询语句(执行内查询 sql,会查数据库)的结果集空或者非空,空则返回false,非空则返回true。它的查询过程类似于以下过程:
List resultSet={};
Array A=(select * from A);
for(int i=0;i<A.length;i++) {
if(exists(A[i].id) { //执行select 1 from B where B.id=A.id是否有记录返回
resultSet.add(A[i]);
}
}
return resultSet;当B表(大表)比A表(小表)数据大时适合使用exists(),因为它没有那么多遍历操作,只需要再执行一次查询就行。
**例1:**A表有10000条记录,B表有1000000条记录,那么exists()会执行10000次去判断A表中的id是否与B表中的id相等。
**例2:**A表有10000条记录,B表有100000000条记录,那么exists()还是执行10000次,因为它只执行A.length次,可见B表数据越多,越适合exists()发挥效果。
**例3:**A表有10000条记录,B表有100条记录,那么exists()还是执行10000次,还不如使用in()遍历10000*100次,因为in()是在内存里遍历比较,而exists()需要查询数据库,我们都知道查询数据库所消耗的性能更高,而内存比较很快。
回答
内部工作原理区别:
in 是先执行内表,会把查询到内表数据缓存起来,然后会进行双重for 循环,外层的 for 循环是遍历外表记录,内层的 for 循环是遍历内表记录,最后每一次 for 循环时在内存判断内表的记录与外表记录是否一致。
exist 会 for 循环遍历外表的记录,每一次 for 循环都会进行一次内查询来判断数据是否匹配。
性能区别:
如果查询的两个表大小相当,那么用 in 和 exists 性能差别不大。
如果查询的两个表中一个是小表,一个是大表,IN适合于外表大而内表小的情况,EXISTS适合于外表小而内表大的情况。
推荐学习
6. mysql的约束有哪些?
分析
当我们创建数据表的时候,还会对字段进行约束,约束的目的在于保证数据库里面数据的准确性和一致性。
主要有六大约束:
主键约束(PRIMARY KEY):主键起的作用是唯一标识一条记录,不能重复,不能为空,即 UNIQUE+NOT NULL。一个数据表的主键只能有一个。主键可以是一个字段,也可以由多个字段复合组成,一般我们会把数据库表中ID字段设置为主键,每个表中只能有一个PRIMARY KEY约束,但是可以有多个UNIQUE约束。
外键约束(FOREIGN KEY ):外键确保了表与表之间引用的完整性。一个表中的外键对应另一张表的主键。外键可以是重复的,也可以为空。
唯一性约束(UNIQUE):唯一性约束表明了字段在表中的数值是唯一的,即使我们已经有了主键,还可以对其他字段进行唯一性约束。比如我们在 player 表中给 player_name 设置唯一性约束,就表明任何两个球员的姓名不能相同。需要注意的是,唯一性约束和普通索引(NORMAL INDEX)之间是有区别的。唯一性约束相当于创建了一个约束和普通索引,目的是保证字段的正确性,而普通索引只是提升数据检索的速度,并不对字段的唯一性进行约束。
非空约束(NOT NULL ):对字段定义了 NOT NULL,即表明该字段不应为空,必须有取值。
默认约束(DEFAULT):表明了字段的默认值。如果在插入数据的时候,这个字段没有取值,就设置为默认值。比如我们将身高 height 字段的取值默认设置为 0.00,即DEFAULT 0.00。
检查约束(CHECK):用来检查特定字段取值范围的有效性,CHECK 约束的结果不能为 FALSE,比如我们可以对身高 height 的数值进行 CHECK 约束,必须≥0,且<3,即CHECK(height>=0 AND height<3)。
注意:MySQL只支持前 5 钟约束,不支持检查约束,MySQL默默地忽略CHECK约束并且不执行数据验证, 要CHECK在MySQL中实现约束,可以使用触发器或视图。
回答
数据库的约束主要有 6 大约束,分别是主键约束、外键约束、唯一性约束、非空约束、默认约束、检查约束,MySQL只支持前 5 钟约束,不支持检查约束。
MySQL 支持的这些约束的作用如下:
主键约束的作用唯一标识一条记录,不能重复也不能为空,一张表数据库表只能有一个主键,一般我们会针对 id 字段设置为主键
外键约束的作用是确保表与表之间引用的完整性
唯一性约束的作用是保证字段在表中的数值是唯一的,如果插入相同字段值的记录,就会报唯一性约束的错误。
非空约束的作用是保证字段不能为 NULL
默认约束的作用是给字段设置默认值,如果插入数据的时候,这个字段没有取值的话,就会用默认值
推荐学习
MySQL 学习指引(SQL学习指引-六大约束)
7. delete、drop、truncate有什么区别?
分析

delete 用来删除记录。该命令其实只是把「记录的位置」或者「数据页」标记为了“可复用”,但磁盘文件的大小是不会变的。也就是说,通过 delete 命令是不能回收表空间的。但是,delete 全表是很慢的,需要生成回滚日志、写 redo、写 binlog。所以,从性能角度考虑,你应该优先考虑使用 truncate table 或者 drop table 命令。
drop 可以用来删除表和表数据。每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中,那么使用drop table 命令,系统就会直接删除这个文件,从而回收表空间大小。如果表的数据放在系统共享表空间,即使drop table 命令,空间也是不会回收的。
truncate 可以用来删除表中的所有记录来实现数据清空,与
DELETE命令不同,它不逐行删除数据。但表结构及其列、约束、索引等保持不变,并且重置 id 从 1开始,在InnoDB存储引擎中,TRUNCATE命令会释放表的表空间,但表文件(如.ibd文件)不会被删除。表文件会被重用,而不是删除和重新创建。
回答
delete 是删除表中的数据,我们可以选择删除部分数据或者全部数据,delete 删除的数据是可以回滚的,delete 操作并不是真的把数据删除掉了,而是给数据打上删除标记,目的是为了空间复用,所以 delete 删除表数据,磁盘文件的大小是不会缩减的。
drop 是删除表结构和表中所有的数据,truncate 是只删除表中所有的记录,表结构并不会被删除,drop 和 truncate 删除的数据都是不可以回滚的,并且删除表会立刻释放磁盘空间 。
从删除表的性能来看,drop>truncate>delete。
推荐学习
Delete、Drop、Truncate有什么区别?你知道吗?
8. 联合查询中 union 和 union all的区别是什么?
分析
主要区别在于处理重复值的方式:
UNION:UNION操作符用于合并多个查询结果,并去除重复的行。当使用UNION时,查询结果中的重复行只会被包含一次。这意味着如果两个查询结果中有相同的行,则只会在最终的结果中包含一次。
UNION ALL:UNION ALL操作符也用于合并多个查询结果,但不会去除重复的行。当使用UNION ALL时,查询结果中的重复行会被全部包含。这意味着如果两个查询结果中有相同的行,则在最终的结果中会包含所有的重复行。
回答
UNION:在合并结果集后会自动剔除重复的行。UNION ALL:则会保留所有的重复行,不会进行去重操作。
推荐学习
9. 数据库三大范式是什么?追问1:范式设计是为了解决什么问题? 追问 2:范式设计有什么缺点?
分析
数据库三大范式是数据库设计中的规范化原则,包括:
第一范式(1NF):要求关系模式中的每个属性都是原子的,即属性不可再分。确保每个属性都是不可再分的基本数据项。比如学生(学号,姓名,性别,出生年月日),如果认为最后一列还可以再分成(出生年,出生月,出生日),它就不是一范式了,否则就是。
第二范式(2NF):要求关系模式必须符合第一范式,并且非主属性必须完全依赖于候选关键字,而不是部分依赖。确保每个非主属性都完全依赖于候选关键字。比如「表:学号、课程号、姓名、学分」,这个表包含了两种信息:学生信息,课程信息。由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖与学号,所以不符合二范式。符合第二范式的做法是:「学生:
Student(学号, 姓名)」;「课程:Course(课程号, 学分)」;「选课关系:StudentCourse(学号, 课程号, 成绩)」。第三范式(3NF):要求关系模式必须符合第二范式,并且不存在传递依赖,即所有非主属性之间不能存在依赖关系。确保不存在非主属性对其他非主属性的传递依赖。举例来说,如果有一个关系模式 R(A, B, C),其中 A → B(A 决定 B)、B → C(B 决定 C),那么存在传递依赖,因为A 决定了 B,B 又决定了 C,从而导致 A → C 的传递依赖。比如,「表: 学号, 姓名, 年龄, 学院名称, 学院电话」,上表属于第二范式,因为主键由单个属性组成(学号)。但是存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) 。符合第二范式的做法是:「学生:(学号, 姓名, 年龄, 所在学院)」;「学院:(学院,学院名称, 电话)」
回答
一范式要求所有属性都是不可分的基本数据项;
二范式目的是解决部分依赖;
三范式目的是解决传递依赖。
在实际的工程实践上没有必要严格遵循三范式要求,比如说可以通过字段冗余的设计,避免联表查询。
追问1回答
数据库的范式是为了解决数据冗余、数据不一致性、数据更新异常和插入异常等问题。
数据冗余:数据冗余是指数据库中存储了大量重复的数据,这不仅浪费了存储空间,而且可能导致数据不一致的问题。范式通过规定数据的结构和组织方式,使得每一份数据只需要被存储一次,从而避免了数据冗余。
数据更新异常:更新异常是指当我们尝试更新一份数据时,可能需要在多个地方进行修改,而如果某一处修改被遗漏,就会导致数据不一致的问题。范式通过规定数据的组织和关联方式,使得每一份数据只需要被修改一次,从而避免了更新异常。
插入异常:插入异常是指当我们尝试插入一份新的数据时,可能因为数据的组织和关联方式的问题,而无法进行插入。范式通过规定数据的组织和关联方式,使得任何合法的新数据都可以被顺利插入,从而避免了插入异常。
**保证数据的一致性和完整性:**范式化的设计通过消除数据冗余和依赖关系,使得数据的一致性和完整性得到保证。当数据只存在于一个位置时,更新和修改数据更加简单和可靠。通过实施数据库范式,开发人员能够确保数据库架构更具可维护性和可扩展性,尤其在处理大型数据集时,范式化能够确保数据的一致性,并使数据库操作更高效。
追问 2 回答
范式化将数据分解为多个表,那么查询数据的时候,就需要进行更多的表连接操作,在应用中,进行表关联的成本是很高,也不适合分库分表的场景,所以有时候实际应用,设计表的时候会反范式的,比如说可以通过字段冗余的设计,避免联表查询。
推荐学习
10. count(*)性能比count(1)好吗?
分析

性能对比:count(*)=count(1)>count(主键)>count(字段)
count(*) 其实等于 count(0),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。

所以,count(*) 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。
回答
不是的。
MySQL 会将星号参数转化为参数 0 来处理,所以count(*) 和count(1) 性能是一样的。
推荐学习
count(*) 和 count(1) 有什么区别?哪个性能最好?
存储引擎
11. 说一说执行一条查询 SQL 语句的全过程
分析
考察你对 MySQL 整体架构的理解和认识,大体的流程如下图:

回答
MySQL 执行一条查询 SQL 语句的时候,会经过连接器、查询缓存、解析器、优化器、执行器、存储引擎这些模块。
首先 MySQL 的连接器会负责建立连接、校验用户身份、接收客户端的 SQL 语句;
第二步 MySQL 会在查询缓存中查找数据,如果命中直接返回数据给客户端,否则就需要继续往下查询,不过查询缓存功能在MySQL 8.0 版本被删除了,原因是只要对这张表进行了写操作,这张表的查询缓存就会失效,所以在实际场景中,查询缓存的命中率其实不高;
第三步 MySQL 的解析器会对 SQL 语句进行词法分析和语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
第四步 MySQL 的优化器会基于查询成本的考虑,会判断每个索引的执行成本,从中选择查询成本最小的执行计划;
第五步 MySQL 的执行器会根据执行计划来执行查询语句,从存储引擎读取记录,返回给客户端;
推荐学习
插入一条数据的流程
这个流程在底层不仅是执行器调用一下存储引擎那么干瘪,它是大名鼎鼎的 WAL(Write-Ahead Logging)和 两阶段提交(2PC) 的完美结晶,主要防的就是断电: 我一般把它在脑海里分成几个核心动作:
调页入存: 执行器把数据交接给 InnoDB。引擎先去看看放这条数据的那个“数据页”在不在内存的 Buffer Pool 里。不在的话从磁盘捞上来(这个时候还只是块干净的内存)。
写 Undo 撤销日志: 这辈子做坏事要留退路,InnoDB 动手前会把要改的操作逻辑(或者是那空槽位的状态)写入 Undo Log。这是为了防备后续操作报错要执行 Rollback,或者是支撑其它长事务的 MVCC 快照读。
内存涂改与 Redo 记账: 在内存中把那条数据正式插入修改。此刻这个内存页被弄脏了(脏页)。既然没落地磁盘,停电不全没了吗?所以它立马把“我们在内存 x 页执行了 y 增加动作”这段极简纯物理逻辑,飞速写进磁盘中的 Redo Log,这个状态叫
prepare(准备期)。Server 层的 Binlog: 既然引擎账本记完了,Server 层也会把生成的逻辑 SQL 流水化记录刷进磁盘中的 Binlog。
最后的 Commit: 二者核对完毕后,引擎层将刚刚 Redo Log 的状态最终改为
commit。 至此,虽然真正的数据还在内存也就是脏页里闲逛(后续会靠后台的 Page Cleaner 线程慢条斯理地真正刷入大磁盘),但这笔 Insert 交易已经算是向客户端保证“打死我都不会丢”了。
12. MySQL 存储引擎有哪些?
分析
MySQL 整体上分为 Server 层和存储引擎层,Server 层负责的部分是连接器、查询缓存、解析器、优化器、执行器,存储引擎层数据的读取和存储。存储引擎就像是一个插件,MySQL 可以根据业务场景使用不同的存储引擎,目前 MySQL 支持 InnoDB、MyISAM、Memory、Archive、CSV、NDB Cluster 等多个存储引擎。
面试时只需要说出 InnoDB、MyISAM、Memory 这三种就可以。
回答
MySQL 常见的存储引擎有 InnoDB、MyISAM、Memory。
我比较熟悉的是 InnoDB 引擎,它是 MySQL 默认的存储引擎,支持事务和行级锁,具有事务提交、回滚和崩溃恢复功能。
MyISAM 引擎我没有用过,但是我在学习的时候有了解过,它是不支持事务和行级锁的,而且由于只支持表锁,锁的粒度比较大,更新性能比较差,我认为它比较适合读多写少的场景。
Memory 引擎我了解不多,大概知道它是将数据存储在内存中,所以数据的读写还是比较快的,但是数据不具备持久性,我觉得适用于临时存储数据的场景。
推荐学习
引擎分类(官方文档)
《高性能mysql第三版》(1.5 MySQL 存储引擎)
13. MyISAM 和 InnoDB 存储引擎有什么区别?
分析

从数据存储、B+树结构、锁粒度、事务这四个角度来分析。
数据存储:InnoDB 引擎数据存储的方式采用的是**索引组织表,在索引组织表中,数据即索引,索引即数据,因此表数据和索引数据都存储在同一个文件中。MyISAM 引擎数据存储的方式采用的是堆表,在堆表的组织结构中,数据和索引分开存储,**因此表数据和索引数据会分别放在两个不同的文件中存储。
索引组织表优点:
在索引组织表将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快,查询数据会更快
在索引组织表中,二级索引设计有一个非常大的好处,若记录发生了修改,则其他索引无须进行维护,除非记录的主键发生了修改,与堆表的索引实现对比着看,你会发现索引组织表在存在大量变更的场景下,性能优势会非常明显,因为大部分情况下都不需要维护其他二级索引。
堆表缺点:
堆表中的索引都是二级索引,哪怕是主键索引也是二级索引,也就是说它没有聚簇索引,每次索引查询都要回表。
由于索引的叶子节点存放了数据在堆表中的地址,当堆表的数据发生改变且位置发生了变更,那么所有索引中的地址都要更新,这非常影响性能。
B+树结构:InnoDB 引擎 B+ 树叶子节点存储索引+数据,MyISAM 引擎 B+ 树叶子节点存储索引+数据地址。
锁粒度:InnoDB 引擎支持行级锁,而 MyISAM 不支持行级锁,仅支持表锁。
事务:InnoDB 支持事务,而 MyISAM 不支持事务。
回答
InnoDB 引擎和 MyISAM 引擎在数据存储上有很大区别,InnoDB 引擎数据存储的方式采用的是**索引组织表,在索引组织表中,数据即索引,索引即数据,因此表数据和索引数据都存储在同一个文件中。MyISAM 引擎数据存储的方式采用的是堆表,在堆表的组织结构中,数据和索引分开存储,**因此表数据和索引数据会分别放在两个不同的文件中存储,索引组织表有两个优势:
在索引组织表将索引和数据保存在同一个B+树中,相比非聚簇索引每次查询都需要回表,因此从聚簇索引中获取数据比非聚簇索引更快,查询数据会更快
在索引组织表中,如果记录发生了修改,则其他索引无须进行维护,除非记录的主键发生了修改,而当堆表的数据发生改变且位置发生了变更,那么所有索引中的地址都要更新,这非常影响性能。
另外,InnoDB 引擎支持行级锁和事务,而 MyISAM 引擎都不支持,只支持表锁。
推荐学习
MySQL引擎篇:半道出家的InnoDB为何能替换官方的MyISAM?
14. MySQL为什么选择InnoDB作为默认引擎?
分析
最重要原因是 InnoDB 支持事务,其他存储引擎都不支持。
回答
InnoDB引擎在事务支持、并发性能、崩溃恢复等方面具有优势,因此被MySQL选择为默认的存储引擎。
事务支持:InnoDB引擎提供了对事务的支持,可以进行ACID(原子性、一致性、隔离性、持久性)属性的操作。Myisam存储引擎是不支持事务的。
并发性能:InnoDB引擎采用了行级锁定的机制,可以提供更好的并发性能,Myisam存储引擎只支持表锁,锁的粒度比较大。
崩溃恢复:InnoDB引引擎通过 redolog 日志实现了崩溃恢复,可以在数据库发生异常情况(如断电)时,通过日志文件进行恢复,保证数据的持久性和一致性。Myisam是不支持崩溃恢复的。
推荐学习
MySQL引擎篇:半道出家的InnoDB为何能替换官方的MyISAM?
15. 用 count(*) 哪个存储引擎会更快?
分析
InnoDB 引擎执行 count 函数的时候,需要通过遍历的方式来统计记录个数,而 MyISAM 引擎执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。
而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。
回答
如果查询语句没有 where 查询条件的话,用 MyISAM 引擎会比较快,因为 MyISAM 引擎的每张表会用一个变量存储表的总记录个数,执行 count 函数的时候,直接读这个变量就行了,而 InnoDB 引擎执行 count 函数的时候,需要通过遍历的方式来统计记录个数。
如果查询语句有 where 查询条件的话,MyISAM 和 InnoDB 引擎执行 count 函数的时候,性能都差不多,都需要根据查询条件一行行的进行统计。
推荐学习
count(*) 和 count(1) 有什么区别?哪个性能最好?
16. NULL 值是如何存储的?
分析
MySQL 存储一行数据的时候, 会使用下面这个格式进行存储,其中 NULL 值列表就是用来保存 NULL 值的。

如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
二进制位的值为
1时,代表该列的值为NULL。二进制位的值为
0时,代表该列的值不为NULL。
以 t_user 表的这三条记录作为例子:

我们直接看第三条记录,第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。

回答
MySQL 行格式中会用「NULL值列表」来标记值为 NULL 的列,每个列对应一个二进制位,如果列的值为 NULL,就会标记二进制位为 1,否则为 0,所以NULL 值并不会存储在行格式中的真实数据部分。
NULL 值列表最少会占用 1 字节空间,当表中所有列都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可以至少节省 1 字节的空间。
推荐学习
17. char 和 varchar 有什么区别?追问:哪个性能更好?
分析
可以从三个角度来分析:
语义上的区别;
char类型是一种固定长度的字符串类型,它在数据库中占用固定的存储空间,无论实际存储的数据长度是多少,都会占用定义时指定的固定长度。例如,如果定义一个char(10)类型的字段,那么无论实际存储的数据长度是多少,都会占用10个字节的存储空间(字符集为ASCII的情况下, 1 个字符是 1字节大小,10 个字符就是 10 字节大小)。
而varchar类型是一种可变长度的字符串类型,它在数据库中只占用实际存储数据的长度加上一定的额外存储空间。例如,如果定义一个varchar(10)类型的字段,并存储了一个长度为5的字符串,那么它只会占用5个字节的存储空间(字符集为ASCII的情况下)加上一定的额外存储空间(存储字符串长度的空间)。
存储上的区别;
varchar会占用额外的1~2字节来存储字符串长度。如果最大长度超过255,就需要2字节,否则1字节。
对于char(N)字段,如果实际存储数据小于N字节,会填充空格到N个字节。
性能上的区别;
理论上CHAR比VARCHAR更快,因为CHAR是固定长度的,而VARCHAR需要增加一个长度标识,处理时需要多一次运算。
理论上CHAR比VARCHAR快的根本原因是站在CPU的角度来说的,但性能是综合各种因素后的最终结果,当Innodb buffer pool小于表大小时,“磁盘读写"成为了性能的关键因素,而VARCHAR更短,因此性能反而比CHAR高。但是当Innodb buffer pool足够大时,CHAR 和VARCHAR性能没有太大的差别。
回答
char 是固定长度的字符串类型,它在数据库中占用固定的存储空间,无论实际存储的数据长度是多少,都会占用定义时指定的固定长度,如果实际存储的字符串长度小于定义的长度,系统会自动用空格填充。比如如果定义一个char(10)类型的字段,即使实际数据只使用 5 字节,会自动填充 5 字节的空格,使得存储空间固定占用 10 字节。
Varchar 是可变长度的字符串类型。实际存储时只占用实际字符串长度的空间,不会进行空格填充。比如如果定义一个varchar(10)类型的字段,并存储了一个长度为5的字符串,那么它只会占用5个字节的存储空间,并且还会额外用 1-2 字节存储「可变长字符串长度」的空间。
追问回答
站在CPU角度来看,理论上CHAR比VARCHAR更快,因为CHAR是固定长度的,而VARCHAR需要增加一个长度标识,处理时需要多一次运算。
但性能是综合各种因素后的最终结果,当Innodb buffer pool小于表大小时,“磁盘读写"成为了性能的关键因素,而VARCHAR更短,因此性能反而比CHAR高。但是当Innodb buffer pool足够大时,CHAR 和VARCHAR性能没有太大的差别了。
推荐学习
MySQL Innodb数据库性能实践——VARCHAR vs CHAR
18. 假如说一个字段是varchar(10),但它其实只有6个字节,那他在内存中占的存储空间是多少?在文件中占的存储空间是多少?
分析
varchar 是可变长字符串,保存到文件的时候,只会存储实际使用的字符串大小。但是内存是会按varchar最大值来固定分配大小。
下图是《高性能MySQL》第 4.1.3 小节提到的,MySQL会分配固定大小的内存块来保存字段的值。

回答
内存会占用 10 字节,文件存储空间会占用 6 字节,并且还会额外用 1-2 字节存储「可变长字符串长度」的空间。
推荐学习
MySQL中varchar(10)和varchar(100)的优缺点
19. 如果硬件内存特别大,MySQL 缓存能否替代 redis?
分析
这个是一个字节的面试题,这里的 MySQL 缓存代表用于缓存数据页的 buffer pool。所以问题的意思是,假设 buffer pool 无限大,能在内存装下所有数据,是否可以替代 redis?
问题的回答思路,要去想 Redis 缓存哪些优点是 MySQL 没有的?
回答
我觉得还是不能替代。
MySQL 所有模块,比如 buffer pool 、日志技术、事务并发模块,都是面向磁盘页而设计的,因此其首要目标不是减少内存访问的代价,而是I/O代价,所以内存访问代价却并不是最优的选择,而 Redis 是面向内存而设计的数据库。
MySQL 在内存查询一个数据页的时候,都需要先查页表,也就是需要走 b+ 树的搜索过程,时间复杂度是 O(logdN),而 Redis 提供了很多种的数据类型,比如用Hash 数据对象的时候,可以在 O(1)时间复杂度查到数据。
MySQL 在更新数据的时候,MySQL 为了保证事务的隔离性,是需要加锁的,而 Redis 更新操作都是不需要加锁的,还有MySQL为了保证事务的持久性,还需要刷盘 redolog 日志和 binlog 日志,Redis 可以选择不持久化数据。
因此,即使 buffer pool 无限大,MySQL 缓存的性能还是没有 Redis 好的。
推荐学习资料
既然有了innodb buffer pool为什么要有redis? - 知乎
索引结构(重要)
20. MySQL 有哪些索引类型?
分析
索引都是由存储引擎来实现的,不同存储引擎支持的索引类型也是不同的。大多数存储引擎都是支持 B+ 树索引,而哈希索引只有 Memory 引擎实现了。

B+ 树索引、哈希索引、全文索引的区别:
B+ 树索引:B+ 树索引是一种平衡树数据结构,它将数据按照索引键值有序地存储在树的叶子节点上,非叶子节点只存储索引键值和指向下一层节点的指针,适合于范围查找、排序查询、等值查询的情况,并且性能稳定,因为 B+ 树保存千万级别的数据,树的高度依然维持在 3~4 层左右,也就是从千万级数据查询一条数据只需要 3~4 次的磁盘 I/O 操作就能查询到目标数据。
哈希索引:哈希索引是通过哈希算法将键值(key-value)映射到哈希表中,再根据哈希表进行索引操作。哈希索引适合于等值查询,例如根据主键查询某条记录,查询时间复杂度为O(1),效率非常高,但不支持排序、范围查询及模糊查询等。
全文索引:全文索引是一种用于全文搜索的索引技术,可以对文本内容进行索引,支持关键词的模糊匹配和搜索。一般用于查找文本中的关键字,而不是直接比较是否相等,主要是用来解决 WHERE name LIKE “%aaaa%” 等针对文本的模糊查询效率低的问题。
回答
我了解到 MySQL 支持 B+ 树索引、哈希索引、全文索引这三种索引类型。 我比较常用的是 B+ 树索引,因为它是 InnodB 引擎默认使用的索引类型,支持排序、分组、范围查询、模糊查询等功能。
推荐学习
引擎分类(官方文档)
-1. B+树索引数据结构是什么?
MySQL(InnoDB)的 B+ 树索引是其高效查询和范围扫描的基石。理解它的结构,不仅能帮你优化 SQL,还能让你在设计表结构时避开深坑。
一、B+ 树的核心特征(与 B 树区别)
B+ 树是一种平衡多路搜索树,但它在两个关键点上与 B 树不同:
数据只在叶子节点:非叶子节点(内部节点)只存储索引键值和指向子节点的指针,不存储实际数据。这意味着内部节点能容纳更多的键值,使得树的高度极低(通常 2~4 层)。
叶子节点形成有序双向链表:所有叶子节点通过指针按主键顺序串联。这是 B+ 树支持范围查询(
BETWEEN、ORDER BY)和区间扫描的核心原因。
二、物理存储结构(16KB 数据页)
InnoDB 的索引以 数据页(Page) 为单位存储在磁盘上,默认大小 16KB。B+ 树的每一个节点都对应一个数据页。
一个数据页的内部布局:
页头(Page Header):存储页号、上一页/下一页指针等信息。
页目录(Page Directory):这是一个关键优化点。页内存储的“槽(Slot)”按主键顺序排列,每个槽指向页内某条记录。查找数据时,先在页目录中通过二分查找快速定位到目标槽,再在槽内遍历,极大减少了页内线性扫描的时间。
行记录(User Records):实际存储的索引条目。
三、B+ 树存储的数据类型
在 InnoDB 中,B+ 树在不同的索引类型下存储的内容不同:
聚簇索引(主键索引):叶子节点存储的是完整的行数据。这意味着主键查询只需一次 B+ 树遍历就能拿到所有字段,无需回表。
二级索引(非主键索引):叶子节点存储的是**(索引列值 + 主键值)。如果要查询索引字段以外的列,就必须拿着主键值回到聚簇索引中再查一遍(这就是之前讨论过的回表**)。
四、查找过程(以主键为例)
从根节点(常驻内存) 开始,通过二分查找定位到下一层子节点的指针。
层层下探,每层都在内存/磁盘间切换,直至到达叶子节点。
在叶子节点的数据页内,先查页目录的二分查找,再遍历记录,最终拿到完整数据行。
由于 B+ 树的高度极低,对于千万级数据,通常只需要 2~4 次磁盘 I/O 即可完成查询。
五、Go 后端开发需要警惕的“页分裂”坑
这是 B+ 树结构带来的最隐蔽的写入性能陷阱:
顺序主键(如自增 ID):数据页写满后,直接申请新的数据页追加在链表尾部。写入效率极高(顺序 I/O)。
无序主键(如 UUID、业务字符串):新插入的主键值随机落在现有数据页的中间位置。当数据页满了,MySQL 必须将当前页拆成两个页(页分裂),大量移动数据,消耗极高的 CPU 和磁盘 I/O,甚至导致索引碎片化。
结论:在 Go 的订单表、日志表等高并发写入场景,强烈建议使用自增数字 ID 或雪花 ID(趋势递增)作为主键,尽量避免随机字符串做主键。
🎤 面试精简回答(260字)
InnoDB 的 B+ 树索引是一种层次极浅的平衡多叉树。它的核心设计是所有数据只存放在叶子节点,并通过双向有序链表串联,这让它在千万级数据下仍能保持 2~4 层的树高,保证了查询效率。非叶子节点只存键值和指针,不存数据,因而能容纳更多索引条目,进一步降低了磁盘 I/O。
在物理存储上,16KB 的数据页内部引入了页目录(槽),使得页内检索通过二分查找快速定位,兼顾了范围扫描和点查的高效性。
在实际 Go 项目中,我对 B+ 树的关注不在查询,而在写入。我最警惕的是 “页分裂” ——如果主键使用 UUID 等随机值,数据插入会落在数据页中间,导致频繁分裂和碎片化。因此在设计高吞吐的订单或日志表时,我会强制使用自增 ID 或雪花算法生成趋势递增的主键,以此来保证插入的追加性,充分利用 B+ 树的顺序写入优势,从底层避免性能抖动。
1.event_id 缺失 - PolicyNotification 恢复 EventID 字段
2.pull_url 相对路径 - 自动拼接 /api-biz 前缀
3.openn : no such file - ctl.NewPaths() 正确初始化路径
4.Kafka DNS 解析失败 - 自定义 Resolver 强制域名解析为 IP
5. syslog协议大小写不匹配 - 改用大小写不敏感比较
6.修复证书下载url与文档对齐
7.支持重复下发相同 encryption 策略
22. B+树的特性是什么?
分析
至少回答出 B+树这 2 个特点:
叶子节点会存储索引+数据,中间节点不会存储数据
叶子节点之间用双向链表组织
回答
B+树是一个多叉树,一个父节点,可以有多个子节点,主要的特性有三个:
B+树的中间节点不会存储数据,而只有叶子节点才会存储,中间节点只用于存储到叶子节点的路由信息(即索引),而且每个节点里的数据都是根据索引的值来顺序存放的
B+树的所有的叶子节点之间会通过双向指针串联在一起,构成一个双向链表,可以方便扫表和范围查询
B+查询性能稳定,因为所有叶子节点都在同一层**,**确保了所有数据项的检索都具有相同的I/O延迟,而且 B+ 树保存千万级别的数据,树的高度依然维持在 3~4 层左右,也就是从千万级数据查询一条数据只需要 3~4 次的磁盘 I/O 操作就能查询到目标数据。
推荐学习
数据结构与算法学习指引(B+树)
23. B+ 和 B 树有什么区别?
分析
从三个角度来说明区别:
数据存储的区别
范围查询的区别
查询效率的区别
回答
B+ 和 B 树都是多叉平衡树,每个节点包含多个键和多条链,主要的区别有这些:
B树所有节点都会存储索引+数据,而 B+ 树只有叶子节点才会存储数据,中间节点则只有索引,因此存储相同数据量的情况下, B+ 树可以比 B 树更矮胖,查询叶子节点的磁盘 I/O次数会更少;
B+树叶子节点之间会通过双向指针串联在一起,构成一个双向链表,这种设计对范围查找非常有帮助,而 B 树没有将所有叶子节点用链表串联起来的结构,只能通过中序遍历来完成范围查询,这会比B+树范围查询涉及更多个节点的磁盘 I/O 操作,因此范围查询效率不如 B+ 树;
B树的优势是当你要查找的值恰好处在一个非叶子节点时,由于该节点也包含数据,查找到该节点就会成功并结束查询,最快可以在 O(1) 的时间代价内就查到,而B+树由于数据只在叶子节点,所以每次查询都需要从根节点搜索到叶子节点,从平均时间代价来看,会比 B+ 树稍快一些,但是B+树的查询会更稳定,因为每次查询都是相同的I/O延迟
推荐学习
24. MySQL 为什么使用 B+ 树?
分析
这种面试官没有问 B+树和其他数据结构区别的问题,就需要自己主动去对比,比如平衡树、红黑树、跳表、B树。
回答
B+树是多叉树,而平衡二叉树、红黑树是二叉树,在同等数据量下,平衡二叉树、红黑树高度更高,磁盘IO次数更多,性能更差,而且它们会频繁执行再平衡过程,来保证树形结构平衡。
跳表和B+树相比,跳表在极端情况下会退化为链表,平衡性差,而数据库查询需要一个可预期的查询时间,并且跳表需要更多的内存。
B 树和B+树相比,B 树的数据存储在全部节点中,对范围查询不友好。非叶子节点存储了数据,导致内存中难以放下全部非叶子节点。如果内存放不下非叶子节点,那么就意味着查询非叶子节点的时候都需要磁盘 IO。
推荐学习
25. 为什么索引用 B+ 树?而不用红黑树?
分析
InnodB 引擎的数据都是存储在磁盘上的,所以选择数据结构的第一优先级是考虑从磁盘查询数据的成本,如果树的高度越高,意味着磁盘I/O就越多,这样就会影响查询性能。
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。
在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。
而红黑树本质上是二叉树,二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
回答
我觉得主要原因是随着数据量的增多,红黑树的树高会比 B+ 树的树高,这样查询数据的时候会面临更多的磁盘 I/O,查询性能没那么好。因为红黑树本质上是二叉树,而 B+ 树是多叉树,存储相同数量量的情况下,红黑树的树高会比 B+ 树的树高,由于 InnodB 引擎的数据都是存储在磁盘上的,如果树的高度越高,意味着磁盘 I/O 就越多,这样就会影响查询性能。
另外,B+树叶子节点是通过双向链表组织的,可以很好的实现范围查询,而红黑树要实现范围查询需要通过中序遍历,这会比B+树范围查询涉及更多个节点的磁盘 I/O 操作,因此范围查询效率不如 B+ 树。
所以, B+ 树相比红黑树有两个优势,第一个优势是B+ 树随着数据的增多的时候树的高度会比红黑树低,第二优势是B+树范围查询很方便,直接通过叶子节点的链表就能完成了,因此 InnodB 存储引擎选择了 B+ 树作为索引。
推荐学习
-1. 为什么索引用 B+ 树?而不用 B 树?
我觉得主要有三个原因:
B+树的磁盘读写代价更低:B+ 树只有叶子节点才会存放索引和数据,非节点只存放索引,而 B 树所有节点都会存放索引和数据,因此存储相同数据量的情况下, B+ 树可以比 B 树更矮胖,查询叶子节点的磁盘 I/O次数会更少;
B+树便于范围查询:MySQL 是需要经常使用范围查询的,B+ 树所有叶子节点间会用链表进行连接,这种设计对范围查找非常有帮助,而 B 树没有将所有叶子节点用链表串联起来的结构,只能通过中序遍历来完成范围查询,这会比B+树范围查询涉及更多个节点的磁盘 I/O 操作,因此范围查询效率不如 B+ 树;
B+树增删查改效率更加稳定:B+ 树有大量的冗余节点,这些冗余数据可以让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化。另外,B+树把所有的用户记录都放到了叶子节点这一层,因此查询、插入、删除数据都需要走到最后一层,这不同于 B 树可能在任意一层找到数据,所以B+树更为稳定。
所以,InnodB 引擎的索引选择了 B+ 树。
推荐学习
27. 为什么索引用 B+ 树?而不用哈希表?
分析
哈希表的数据是散列分布的,不具备有序性,无法进行范围查和排序
哈希表存在哈希冲突的问题,哈希冲突严重,也会降低查询效率
回答
MySQL 会有很多范围查询和排序的场景,虽然哈希表的搜索时间复杂度是 O(1),但是由于哈希表的数据都是通过哈希函数计算后散列分布的,所以哈希表索引不支持范围查询和排序操作,不支持联合索引最左匹配原则,如果重复键值比较多,还容易造成哈希碰撞导致效率进一步降低。而 B+ 树可以满足这些应用场景,因此选择了用 B+ 树索引。
推荐学习
28. B+ 树有什么优点和缺点?
分析
一问到 B+ 树的缺点, 很多人就懵了,因为前面都在说 B+ 树 的各种优点。
B+树最大优点是B+树的叶子节点形成了一个有序链表,可以方便地进行范围查询。相比较而言,B树和二叉树需要在非叶子节点进行回溯才能找到所有满足条件的记录,这会增加额外的开销。
B+ 树缺点是可能会产生大量的随机I/O,每次修改数据都很有可能破坏B+树的约束,我们需要对整棵树进行递归的合并、分裂等调整操作,而不同节点在磁盘上的位置很可能并不是连续的,这就导致我们需要不断地做随机写入的操作(B+树更新操作过多而导致随机I/O这个缺点,被LSM树解决,LSM树是Hbase、LevelDB的数据库索引,感兴趣的同学可以学习一下: 数据结构与算法学习指引-LSM树)
回答
B+树有一个最大的好处是方便范围查询,B+树的叶子节点之间有链表,直接通过叶子节点链表就能方便的完成范围查询的工作,而B树必须用中序遍历的方法来实现范围查询,这会比B+树范围查询涉及更多个节点的磁盘 I/O 操作,因此范围查询效率不如 B+ 树。
B+树最大的性能问题是会产生大量的随机IO,随着新数据的插入,叶子节点会慢慢分裂,逻辑上连续的叶子节点在物理上往往不连续,甚至分离的很远,但做范围查询时,会产生大量读随机IO。对于大量的随机写也一样,举一个插入key跨度很大的例子,如7->1000->3->2000 ... 新插入的数据存储在磁盘上相隔很远,会产生大量的随机写IO。
推荐学习
29. 聚簇索引和非聚簇索引(二级索引)有什么区别?
分析
先说聚簇索引和非聚簇索B+树叶子节点存放内容的区别,然后再引出回表查询和覆盖索引查询。

回答
聚簇索引和非聚簇索(二级索引)引最主要的区别是 B+树叶子节点存放的内容不同:
聚簇索引的 B+树叶子节点存放的是主键值+完整的记录;
非聚簇索引的 B+树叶子节点存放的是索引值+主键值;
如果查询语句的查询条件用了二级索引,但是查询的数据不是主键值,也不是二级索引值,这时在二级索引找到主键值后,就需要回表才能查找到数据,,需要扫描两次B+树。如果查询的列是主键值和二级索引值时,因为只在二级索引就能查询到,这时候就会用到覆盖索引,不需要回表,只需要扫描一次B+树。
推荐学习
-1. 聚簇索引和非聚簇索引在回表时有什么区别?
聚簇索引和非聚簇索引在回表上的本质区别在于:聚簇索引的叶子节点直接挂载完整行数据,查询时一次索引遍历即完成,完全不需要回表;而非聚簇索引的叶子节点只存储索引列和主键值,若查询列未被索引覆盖,就必须用主键值再次到聚簇索引中检索完整行,这个二次检索就是回表。
回表最大的性能隐患是随机 I/O,因为回表的主键往往是不连续的,会导致频繁的数据页跳转。在高并发场景下,大量回表会显著增加磁盘压力。优化的核心手段是覆盖索引,即让 SQL 查询的所有列都包含在二级索引中,从而让索引直接返回数据,彻底避免回表。此外,InnoDB 的 MRR 优化会将回表的主键排序后再批量读取,在一定程度上缓解随机 I/O 问题。
总而言之,设计索引时应尽量利用覆盖索引减少回表次数,这是提升查询性能的关键实践。而聚簇索引本身由于省去了回表环节,在主键等值查询和范围查询上具有天然的性能优势。
30. 什么是覆盖索引?
分析
二级索引的叶子节点存放的是索引+主键 id,如果查询的列能够在二级索引中全部查询到,那就不需要回到主键索引去查行记录了,这种不需要回表的过程,就叫覆盖索引,效率会比较高。
假设有联合索引(a,b),当执行以下查询的时候,都会发生覆盖索引:
select a, b, id from table where a= ? and b =?;
select a, b from table where a= ? and b =?;
select a, id from table where a= ? and b =?;
select b, id from table where a= ? and b =?;
select a from table where a= ? and b =?;
select b from table where a= ? and b =?;
select id from table where a= ? and b =?;
我们也可以通过 explian 命令 来确认查询是否用到了覆盖索引。

可以看到 extra 信息显示了“using index”,就代表查询用到了覆盖索引,不涉及回表的过程。
回答
当查询的数据是能在二级索引的叶子节点里查询到的话,这时就不用再回主键索引查了,那就不需要回到主键索引去查行记录了,这种不需要回表的过程,就叫覆盖索引,这种查询方式效率会比较高,只需要查二级索引这一棵 B+ 树。
推荐阅读
31. 什么情况下会回表?
分析
在使用二级索引进行查询的时候,如果查询的列,不能在二级索引中全部查询到,那么就需要回到主键索引去查完成的行记录了,这种二级索引通过主键索引进行再一次查询的操作叫作「回表」。
我这里将商品表中的 product_no (商品编码)设置为二级索引,那么这个二级索引的**索引键值是product_no,B+ 树会根据product_no索引键值的顺序来存储数据,**那么二级索引的 B+ 树如下图:

上图中,非叶子节点的索引键值是 product_no(图中红色部分),叶子节点存储是索引键值(图中红色部分)+主键值(图中绿色部分)。
如果我用 product_no 二级索引查询商品,如下查询语句:
select * from product where product_no = '0008';我们用 explain 命令来看看这个语句的执行情况,可以看到 key 不是NULL,而是idx_product_no,说明查询走了二级索引。

查询过程是这样的,先从二级索引 B+ 树自顶向下逐层进行查找:
将 0008 与根节点的索引 (0003,0006,0009) 比较,0008 大于索引值 0006,说明 0008 肯定不在叶子节点 2,因为索引值 6 是叶子节点 2 中最大的索引值,0008 小于索引值 0009,这表示小于 0009 的数据在地址指向的下一层节点中,根节点索引键 0009 的指针地址指向的是「叶子节点 3」,因此这里定位的是 0008 在「叶子节点 3」 中
在「叶子节点 3 」中根据二分查找算法就能找到索引键值为 0008 的数据,但是这时候只能查到主键值(id = 8),而无法查询到完整的行记录,因为二级索引并不会存储完整的行记录,所以需要额外再通过主键索引到主键索引中查询到对应的叶子节点,这时候才能获取完整的行记录,也就是说要查两个 B+ 树才能查到最终的结果
这种二级索引通过主键索引进行再一次查询的操作叫作「回表」,你可以通过下图理解二级索引的查询过程:

回答
在使用二级索引进行查询的时候,如果查询的列,不能在二级索引中全部查询到,那么就会发生回表的过程,先通过二级索引的值查到聚簇索引值(即主键 id),再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低。
推荐学习
32. insert 操作对 B+ 树结构的改变是怎么样的?
分析
要说出页分裂问题,以及指出主键id要是顺序递增,如果是随机值(比如UUID),就可能会频繁出现页分裂的现象,会严重影响性能。
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
举个例子,假设某个数据页中的数据是1、3、5、9,且数据页满了,现在准备插入一个数据7,则需要把数据页分割为两个数据页:

出现页分裂时,需要将一个页的记录移动到另外一个页,性能会受到影响,同时页空间的利用率下降,造成存储空间的浪费。
而如果记录是顺序插入的,例如插入数据11,则只需开辟新的数据页,也就不会发生页分裂:

回答
B+ 树的数据都是有序的,所以:
如果我们使用主键是顺序递增,那么每次插入的新数据就会顺序插入到叶子节点最右边的节点里,如果该页面满了,就会自动开辟一个新页面,将新数据插入到新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
如果我们使用主键不是顺序递增,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这时候为了保证B+ 树的有序性,要移动其它数据来满足新数据的插入。如果该页面满了,就发生页分裂,这时候要从一个页面复制数据到另外一个页面,目的是保证后一个数据页中的所有行主键值比前一个数据页中主键值大,页分裂可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
所以,我们在设计主键的时候,最好采用自增的方式,或者顺序递增主键值。
推荐学习
33. 假如一张表有两千万的数据,B+树的高度是多少?怎么算的?
分析
假设
非叶子节点内指向其他页的数量为 x
叶子节点内能容纳的数据行数为 y
B+ 数的层数为 z
表总数会等于 x 的 z-1 次方 与 Y 的乘积:
$Total = x^{(z-1) } * y$

回答
具体要看数据库表的字段多不多,以及字段类型,假设一行记录是 1KB大小,那么 2000 万的数据表, B+ 树大概是三层高度。
MySQL 数据页的大小是 16 KB,去掉一些头信息,大概有15KB是可以存储数据。
在索引页中主要记录的是主键与页号,假设是主键 id 类型是 bigint,那就是 8 字节, 页号固定为 4 字节, 那么索引页中的一条数据也就是 12byte。那么一个索引页可以存储 15*1024/12≈1280 个页号。
叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如字字段的类型,字段的数量。每行数据占用空间越大,页中所放的行数量就会越少,假设按一条行数据 1KB 来算,那一页就能存下 15 条,15KB/1Kb = 15
根据的公式,$Total = x^{(z-1) } * y$,已知 x=1280,y=15,假设 B+ 树是三层,那就是 z = 3,Total = (1280 ^2) *15 = 24576000 (约 2.45kw)
推荐学习
——为什么网上说超过2000w行就要分表
其实这个两千万的数字,纯粹就是早年大佬们拿着白纸靠初中数学算出来的一个 “3层 B+ 树爆满极限值”。我跟您盘算一下它的推导过程您就懂了: 在 InnoDB 里默认的每一块页大小是 16KB。 对于顶层的一个非叶子枝干节点,它通常塞的是:我们的主键(比如占 8 byte 的 BigInt)+ 以及一个指向下页地址的指针(占 6 byte)。加起来才区区 14 个字节。 所以 16KB / 14 Byte,一个极其普通的节点可以密密麻麻塞下大约 1170 个小弟的指针。 如果这是一个纯粹的 3 层 B+ 树,前两层是引路的枝干组合,下面拖着的那个海量叶子才装真实的数据! 假如我们的一行业务数据长度是 1KB,那一页可以装 16 行。 最后算一式乘法:第一层(1170) × 第二层(1170) × 第三层的实际数据(16) ≈ 2190 万!
这才是神话的由来:一旦到了大概两千万,为了装这点数据这棵树就必须再往上顶高一层,变成了 4 层结构。多一层树高,就意味着在内存没能全部覆盖这帮冷门数据时,可能要凭空多熬多等一次硬盘寻道的随机 IO。硬盘寻道,那是出了名的乌龟。
结合项目的实战折腾和一点感悟: 但在真正的业务重构架构里,只要是稍微见多识广点的一线开发,对哪怕表冲上 3000 万了也不会上来就叫唤着立马搞 Sharding-JDBC 去动那种吃力不讨好的分布式事务去“分表”。 首先,现代云服务器挂那可全是恐怖的 NVMe 企业级固态。这种级别的 SSD,多一层 IO 耗时几乎无感。 再者,这个极限可是基于 1KB 一条的设定。如果要我优化的业务就是一张干净又精瘦的小型字典级日志表,单行才区区 100 字节,那这个破树就算挂了 1 亿条数据,它照样还稳稳停留在不用跨层的 3 层以内! 所以在我们处理大表慢速时,我第一直觉永远不是“该分表了”,而是: 能不能利用大内存配属提高 Buffer Pool 的命中率?能不能用上联合索引做全覆盖免回表?能不能把那些吃空间的老黄历长篇大论用 “冷热分离档案表” 给甩出去瘦身降高层级? 如果常规的数据库压榨打法真的走到走投无路的穷途末路了,最后一道才是痛下杀手横向分库。
索引应用(重要)
34. MySQL 有哪些索引?
分析
主键索引、唯一索引、普通索引、前缀索引、联合索引。
主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
联合索引:通过将多个字段组合成一个索引,该索引就被称为联合索引。
回答
我了解到 MySQL 有主键索引、唯一索引、普通索引、前缀索引、联合索引这几种索引。Innodb 引擎会要求每一张数据库表都必须要有一个主键索引,比如表里的 id 字段就是主键索引。
然后针对查询比较频繁的字段,我们可以对这个字段建立普通索引,如果是多个字段的话,可以考虑建立联合索引,利用索引覆盖的特性提高查询效率。
对于长文本、字符串等类型的字段,比如文章标题、商品名称等,我们可以只对这些字段的前缀部分建立索引,也就是建立前缀索引,这样可以减少索引的存储空间。
推荐学习
35. MySQL主键是聚簇索引吗?
分析
聚簇索引就是按照每张表的主键构造一棵 B+ 树,同时叶子节点中存放的是整张表的行记录数据,就好像把数据和索引聚集在了一棵 B+ 树上,所以这种数据组织形式的索引叫聚簇索引
每张表只能拥有一个聚簇索引,因为数据库表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
如果定义了主键,默认会使用主键作为聚簇索引的索引键;
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id (row_id)列作为聚簇索引的索引键;
回答
是的,主键是聚簇索引,我们创建数据库表的时候,通常都会对 id 字段设置为主键索引,InnoDB 在创建聚簇索引时,默认会使用主键作为聚簇索引的索引键。
推荐学习
36. 主键为什么不推荐有业务含义?
分析
因为任何有业务含义的列都有改变的可能性,主键一旦带上了业务含义,那么主键就有可能发生变更。主键一旦发生变更,该数据在磁盘上的存储位置就会发生变更,有可能会引发页分裂,产生空间碎片。
还有就是,带有业务含义的主键,不一定是顺序自增的。那么就会导致数据的插入顺序,并不能保证后面插入数据的主键一定比前面的数据大。如果出现了,后面插入数据的主键比前面的小,就有可能引发页分裂,产生空间碎片。
回答
我觉得原因两个:
第一个业务会有变动的可能性,我们谁也无法预测 在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现,等需要变动的时候,去更改主键是成本很高的一件事情,不如设计阶段就规避不用有业务含义的主键。
第二个是业务含义的主键可能不是顺序自增的,有可能会发生页分裂问题,从而影响性能
推荐学习
37. 主键是用自增还是UUID?
分析
在《阿里巴巴 Java 开发手册》第五章 MySQL 规定第九条中,强制规定了单表的主键 id 必须为无符号的 bigint 类型,且是自增的。为什么会这样强制规定呢?

通常主键 id 的数据类型有两种选择:字符串或者整数,主键通常要求是唯一的,如果使用字符串类型,我们可以选择 UUID 或者具有业务含义的字符串来作为主键。
对于 UUID 而言,它由 32 个字符+4 个’-‘组成,长度为 36,虽然 UUID 能保证唯一性,但是它有两个致命的缺点:
不是递增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。
对于整数的数字类型,MySQL 中主要有 int 和 bigint 类型。其中 int 占用 4 个字节,bigint 占用 8 个字节,这和 Java 中的 int 和 long 对应。
如果使用无符号的 int 类型作为主键,那么主键的最大值为 2^32-1,即 4294967295,这个值不到 43 亿,似乎有点太小了。虽然一张表的数据,我们不可能让其达到 43 亿条(太大会影响性能),但是对于频繁进行插入、删除的表来说,43 亿这个值是可以达到的。
而如果使用无符号的 bigint 类型的话,主键的最大值可以达到 2^64-1,这个数足够大了,如果以每秒插入 100 万条数据计算的,58 万年以后才能达到最大值。所以 bigint 作为主键的数据类型,完全不用担心超过最大值的问题。
而强制要求主键 id 是自增的,则是为了在数据插入的过程中,尽可能的避免索引树上页分裂的问题。
回答
用自增 id 比较好,因为UUID是随机值,在数据插入的过程中,会导致索引树发生页分裂的问题,会影响性能,而且UUID是字符串类型,长度比较长,占用内存比较大,而页的大小是固定的,这样会导致索引树的高度越高,查询的时候会发生的磁盘 IO 次数也越多,性能也就更低。
但是自增 id 在分库分表环境下就不适用了,因为没办法保证全局唯一,这时候就需要考虑用雪花算法来作为主键了。
推荐学习
38. 普通索引和唯一索引有什么区别?哪个更新性能更好?
分析
哪个更新性能更好,要从 InooDB 引擎的 change buffer 的角度去分析。
回答
普通索引列的值是可以重复的,而唯一索引列的值是必须唯一的,当我们对唯一索引插入了一条重复的值,会因为唯一性约束而报错。
我认为普通索引的更新性能会更好,因为普通索引在更新的时候,如果更新的数据页不在内存的话,可以直接把更新操作缓存在 change buffer 中,更新操作就结束了,但是,唯一索引因为需要有唯一性约束,需要先读取对应的数据判断是否有冲突,如果更新的数据页不在内存的话,需要从磁盘读取对应的数据页到内存,判断到没有冲突,这里会涉及磁盘随机 IO 的访问。
普通索引因为能使用 change buffer 特性,所以普通索引的更新相比于唯一索引,减少了随机磁盘访问,所以更新性能更好。
推荐资料
39. 主键怎么设置?追问:假如你不设置会怎么样?
分析
可以在创建表时,将某一列定义为主键(PRIMARY KEY)。例如:
CREATE TABLE table_name (
id INT PRIMARY KEY,
column1 datatype,
column2 datatype,
...
);InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
如果有主键,默认会使用主键作为聚簇索引的索引键;
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
回答
在创建表的时候,对id 列设置为 PRIMARY KEY,那么 id 列就是主键索引了。
追问回答
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键,如果这个条件也没有达成的话,InnoDB 将自动生成一个隐式 rowid 列作为聚簇索引的索引键。
推荐资料
40. 介绍一下什么是外键约束?
分析
先来看看什么是外键?
假设我们有 2 个表,分别是表 A 和表 B,它们通过一个公共字段“id”发生关联关系,我们把这个关联关系叫做 R。如果“id”在表 A 中是主键,那么,表 A 就是这个关系 R 中的主表。相应的,表 B 就是这个关系中的从表,表 B 中的“id”,就是表 B 用来引用表 A 中数据的,叫外键。所以,外键就是「从表」中用来引用「主表」中数据的那个公共字段。
如图所示,在关联关系 R 中,公众字段(字段 A)是表 A 的主键,所以表 A 是主表,表 B 是从表。表 B 中的公共字段(字段 A)是外键。
什么是外键约束?
在 MySQL 中,外键是通过外键约束来定义的。外键约束就是约束的一种,它必须在从表中定义,包括指明哪个是外键字段,以及外键字段所引用的主表中的主键字段是什么。MySQL 系统会根据外键约束的定义,监控对主表中数据的删除操作。如果发现要删除的主表记录,正在被从表中某条记录的外键字段所引用,MySQL 就会提示错误,从而确保了关联数据不会缺失,保证了 2 个表中数据的一致性。
回答
外键就是「从表」中用来引用「主表」中数据的那个公共字段,外键约束确保了数据的引用完整性,也就是「从表」中的外键必须存在于「主表」的主键中,如果发现要删除的主表记录,正在被「从表」中某条记录的外键字段所引用,MySQL 就会提示错误,从而保证了 2 个表中数据的一致性。
推荐学习
41. 外键有什么优劣势?
分析
MySQL 外键最大的作用就是有助于维护数据的一致性和完整性。
一致性:如果一个订单表引用了一个客户表的外键,外键可以确保订单的客户 ID 存在于客户表中,从而保持数据的一致性。
完整性:外键可以防止在引用表中删除正在被其他表引用的记录,从而维护数据的完整性。
但是,其实在很多大型互联网公司中,很少用外键的,甚至阿里巴巴Java开发手册中明确规定了:「不要使用外键约束,如果数据存在外键关系,请在程序层面实现」

那么,使用外键会带来哪些问题呢?
- 性能问题
定义外键之后,数据库的每次操作都需要去检查外键约束,检查关联表是否已经存在数据。硬性保持数据一致性。这些操作会占用数据库的计算资源,如果一条记录中存在多个外键,这样的buff还将会被叠加(性能损耗加成)。
对于插入来说,数据库需要执行数据一致性检查以确保引用的数据在表中的存在,会影响了插入速度;对于更新来说,级联更新是强阻塞,存在数据库更新风暴(Database Update Storm)的风险。
所谓 Database Update Storm,指的是在高并发环境下,多个客户端同时对数据库进行大量的更新操作,存在锁竞争问题甚至死锁,从而导致数据库性能急剧下降或完全崩溃。
因此,对于大并发的 SQL 操作,有可能会不适合用外键,比如大型网站的中央数据库,可能会因为外键约束的系统开销而变得非常慢。
- 锁竞争问题
在使用外键的情况下,每次修改数据都需要去检查外键关联表里的数据,这需要额外获取读锁,如果是高并发的情况下,更容易造成死锁。
- 无法适用分库分表场景
在大型项目中,当数据量特别大的时候,一般会采取分库分表来存储数据,但在不同的库中使用相同的外键来维护数据一致性和完整性是非常难的操作,外键难以跨越不同数据库来建立关系。所以在分布式、高并发集群的项目数据库中一般看不到外键的存在。
回答
外键能够保证数据的一致性和完整性,通过设置外键,数据库就会判断数据的完整性,不需要在应用代码里实现。
有了外键之后,每次增删改都需要额外检查外键约束,会占用数据库的计算资源,影响增删改的性能,而且还需要额外获取锁,在高并发场景下很容易发生死锁的问题,另外,外键也不适合分库分表的场景,外键难以跨越不同数据库来建立关系。
因此,基于性能开销、锁竞争、分库分表的考虑,一般项目中,很少用外键约束,都是在应用层面完成检查数据一致性的逻辑,就像大厂会使用RC(读已提交隔离级别)来替代RR(可重复读隔离级别)一样,会尽可能的降低锁的发生,一方面提升性能,一方面降低死锁概率。
推荐学习
42. 为什么要建索引?
分析
考察索引的优点。建索引的三个优点:
索引大大减少了 MySQL 需要扫描的数据量;
索引可以帮助 MySQL 避免外部排序和使用临时表;
索引可以将随机 I/O 变为顺序 I/O;
回答
如果没有建立索引,我们查询数据的话,搜索时间复杂度是 O(n),这样的查询效率还是比较低的,为了提高查询效率,我们可以建立索引。
建立了索引后数据都会按照顺序存储,这时候我们可以利用类似二分查找的方式快速查找数据,B+ 树索引是多叉树,搜索时间复杂度是 O(logdN),这样就提高了查询速度,除此之外还可以避免外部排序和使用临时表等问题,以及将随机 I/O 变为顺序 I/O。
推荐学习
《高性能mysql第三版》(5.2 索引的优点)
43. 我们一般选择什么样的字段来建立索引?
分析
考察索引的使用场景。
适用索引的场景:
字段有唯一性限制的,比如商品编码;
经常用于
WHERE查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。经常用于
GROUP BY和ORDER BY的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
不适合索引的场景:
WHERE条件,GROUP BY,ORDER BY里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
回答
可以对频繁用于 WHERE 查询条件的字段建立索引,这样能够提高整张表的查询速度,如果查询条件不是一个字段,可以考虑建立联合索引。还有对于经常用于排序、分组的字段建立索引,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在 B+树中的数据都是排序好的。
不过,对于一些区分度不高的字段,比如性别字段,只有男女,不建议建立索引,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据,在这种情况下,MySQL 的优化器发现某个值在表中出现的比例很高的时候,它一般会忽略索引,进行全表扫描,这时候建立的索引就没有起到作用,反而还占用了存储空间。
推荐学习
第7章 好东西也得先学会怎么用-B+树索引的使用(7.2 B+树适用索引的条件)
44. 索引越多越好吗?
分析
考察索引的缺点。索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
空间代价:需要占用物理空间,数量越大,占用空间越大;
时间代价:会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
回答
不是的,索引虽然能提高查询效率,但是多建立一个索引,就意味着新生成一个 B+树索引,是需要占用存储空间的,特别是在表数据量非常大的时候,索引占用的空间越大。
还有,索引越多数据库的写入性能会下降,因为每次对表进行增删改操作的时候,都需要去维护各个 B+ 树索引的有序性。
推荐学习
第7章 好东西也得先学会怎么用-B+树索引的使用(7.1 索引的代价)
45. 什么时候不用索引更好?
分析
考察对索引缺点的认识。
回答
建立了索引,虽然能提升查询效率, 但是它带来了两个代价,第一个是空间代价,因为需要多构建一颗 b+树,会占用磁盘空间。第二个更新时间代价,每次增删改索引,都需要动态维护 b+树,以满足 b+树的有序性。
所以, 我认识到如果一张表经常被增删改的话,也就是写多读少的场景下, 不建立索引会更好,因为这时候维护索引的开销可能会超过索引带来的性能提升。
还有一点,如果表中某个列的值高度重复,那么建了索引也没有用,优化器会选择全表扫描,这样建立的索引会占用存储空间,也会影响增删改的效率,选择不用索引会更好。
推荐学习
第7章 好东西也得先学会怎么用-B+树索引的使用(7.1 索引的代价)
46. 字段为什么要定义为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。
回答
如果查询中包含可为NULL的列,对MySQL的优化器来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。
如果某列存在NULL的情况,可能导致 count() 等函数执行不准确,因为 count 不会统计值为 NULL 列。
NULL 值是一个没意义的值,但是它会占用物理空间,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表
推荐学习
47. 索引怎么优化?
分析
几种常见优化索引的方法:
覆盖索引优化:
- 假设我们只需要查询商品名称和价格这两个数据,这时候我们可以对这两个字段建立联合索引,即「商品名称、价格」作为一个联合索引,针对 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 的语句,比如不要对索引进行计算、函数、类型转换操作,联合索引要能正确使用需要遵循最左匹配原则等等。
对于一些大字符串的索引,我们可以考虑用前缀索引只对索引列的前缀部分建立索引,节省索引的存储空间,提高查询性能。
推荐学习
索引常见面试题(索引优化部分)
48. 建立了索引,查询的时候一定会用到索引吗?
分析
两个方向回答:
索引失效的场景
优化器是基于成本考虑,即使查询条件用了索引,如果走索引的查询成本太高,也不会选择走索引。
回答
不是的。
我了解到即使查询使用到了索引,也是可能不走索引的,比如:
当我们查询语句对索引字段进行左模糊匹配、表达式计算、函数、隐式类型转换操作,这时候查询语句就无法走索引了,查询方式就变成了全表扫描的方式。还有我们使用联合索引进行查询的时候,如果没有遵循最左匹配原则,也是会发生索引失效的。
优化器是基于成本考虑来选择查询的方式,在使用二级索引进行查询的时候,优化器会计算回表的成本和全表扫描的成本,如果回表的代价太高,优化器会选择不走索引,而是走全表扫描。
推荐学习
49. 如果我定义了一个varchar类型的日期字段,并且有一个数据是‘20230922’,如果这个日期字段上有索引,那如果我查询的wher条件是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 在遇到字符串和数字比较的时候,会发生隐式类型转换,会将字符串的对象转为数字,这个转换的过程实际上会涉及到函数。你说的这个查询,日期字段是字符串,那么发生隐式类型转换的时候,就会作用在日期这个索引字段上,对索引进行函数计算的话,是会发生索引失效的。
推荐学习
50. MySQL 最新版本解决了索引失效的哪些情况了吗?
分析
MySQL 8.0 新特性:函数索引和索引跳跃扫描机制。
- 函数索引
MySQL 8.0 索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
举个例子,我通过下面这条语句,对 length(name) 的计算结果建立一个名为 idx_name_length 的索引。
alter table t_user add key idx_name_length ((length(name)));然后我再用下面这条查询语句,这时候就会走索引了。

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

但是索引跳跃的使用也是有前提条件的,要满足下面这些条件才能用得到(不需要背,只需简单了解,面试不会问这些条件):
查询只能涉及一张表,多表关联无法使用该特性
查询SQL不能使用 GROUP BY 或者 DISTINCT子句
查询字段必须是索引中的字段(所以,只要涉及回表的查询, 就无法用到索引跳跃)
组合索引形式:([A_1, …, A_k,] B_1, …, B_m, C [, D_1, …, D_n]),A,D 可以为空,但是B ,C 不能为空(这个条件有点抽象,举例子(a,b,c),只要查询的时候,中间的索引字段 b 没用到,就会用不了索引跳跃的优化)
前面 2 个条件好理解,后面 2 个条件,我举一些例子来跟大家说明,假设 test 表有(a, b, c)联合索引。
- select a,c,b from test where a=100 and c=100; 没用到跳跃索引的优化,因为没满足条件 4

- select a,c,b from test where c=100; 没用到跳跃索引的优化,因为没满足条件 4

- select a,c,b from test where b=100 和 select a,c,b from test b= 1 and c=100 都用到了跳跃索引优化,因为满足了所有条件


- select * from test where b= 1 and c=100; 没用到跳跃索引的优化,因为没满足条件 3 (查询字段必须是索引中的字段)

回答
我了解到 MySQL 8.0 可以给字段增加函数索引,这个新特性可以解决对索引使用函数的时候,索引失效的问题。
还有一个新特性是索引跳跃式扫描,5.7 版本之前,使用联合索引的时候,如果不满足最左匹配原则,就会发生索引失效,而 8.0 出了索引跳跃式扫描特性之后,即使没有遵循最左匹配原则,部分场景下,依然可以使用联合索引。
推荐学习
MySQL 8.0 索引跳跃扫描(Index Skip Scan)
51. 什么是最左匹配原则?
分析
要知道联合索引的结构,才能理解最左匹配原则。
如果创建了一个 (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 无法使用这个索引进行查询,因为查询条件不匹配索引的最左边的列。
52. 建立联合索引有什么需要注意的?
分析
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:

比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是"30%")很高的时候,它一般会忽略索引,进行全表扫描。
回答
最好把区分度比较大的字段放在联合索引最左侧,有助于提高索引的过滤效果,比如 UUID 这类字段就比较适合排在联合索引列的靠前的位置。
如果区分度很低的字段放在了联合索引最左侧,有可能会导致查询优化器会选择全表扫描,而不走索引了。
推荐学习
索引常见面试题(联合索引部分)
53. 🌟了解索引下推吗?什么情况下会下推到引擎去处理?
分析
索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。
举一个具体的例子,方便大家理解,这里一张用户表如下,我对 age 和 reward 字段建立了联合索引(age,reward):

现在有下面这条查询语句:
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 的记录后,才去执行回表操作获取整个记录。相比于没有使用索引下推,节省了很多回表操作。
当你发现执行计划里的 Extr 部分显示了 “Using index condition”,说明使用了索引下推。

回答
索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。
举个例子,联合索引(a,b,c),查询条件为 a=? and c=? 的时候,由于联合索引的最左匹配原则,c 是无法走索引的,在没有索引下推机制之前,查询语句走二级索引的时候,需要回表读取 c 的值,然后在 server 层判断是否符合c=?进行过滤,有了索引下推机制后,即使 c 无法走索引,但是由于 c 在二级索引里,那么将过滤 c 的工作从 server 层下推到存储引擎层,这样直接在二级索引里过滤满足 c 条件的记录,减少了回表的次数。
推荐学习
54. 🌟联合索引 (a,b,c),下面的查询语句会不会走索引?如果走具体是哪些字段能走?
select * from T where a=1 and b=2 and c=3;
select * from T where a=1 and b>2 and c=3;
select * from T where c=1 and a=2 and b=3;
select * from T where a=2 and c=3;
select * from T where b=2 and c=3;
select (a,b) from T where a=1 and b>2
分析
大厂面试的时候,喜欢出这种题目,列几条 SQL 语句让你肉眼判断走不走索引,其实也是在考察你对最左匹配原则的理解。
回答
遵循最左匹配原则,所以 abc 三个字段都可以走索引,查询方式是在联合索引找到主键值后,会回主键索引找完整的数据行。
根据最左匹配原则,范围查询后面的字段无法使用索引,所以 ab 可以走索引,c 无法走索引,不过 c 可以进行索引下推。
abc都能走索引,因为 where 查询条件字段的顺序并不会影响,MySQL 优化器会帮我们调整字段的查询顺序,所以也是符合最左匹配原则的。
a 能走索引,根据最左匹配原则,c 无法走索引,但是 c 可以被索引下推
根据最左匹配原则,bc都无法走索引。
a 和 b 都能走索引,查询方式是覆盖查询,不需要回表。
推荐学习
索引常见面试题(联合索引部分)
执行一条 select 语句,期间发生了什么?(索引下推的概念在执行器有讲解)
55. where a>1 and b = 2 and c <3怎么建立索引?
分析
这题属于上一题的反向思维,根据查询条件,创建联合索引,来提高这条语句的查询效率,所以就要创建一个能让更多字段能走索引的联合索引。
假设:
创建(abc)、(acb)、(ab)、(ac)联合索引,只有 a 能索引
创建(cab)、(cba)、(ca)、(cb)联合索引,只有 c 能索引
创建(ba)联合索引,b 和 a 都能走索引
创建(bc)联合索引,b 和 c 都能走索引
创建 (bac) 联合索引,b 和 a 都能走索引,但比 (ba)联合索引多了一个好处,c 字段能索引下推,会减少回表的次数;
创建 (bca) 联合索引,b 和 c 都能走索引,但比 (bc)联合索引多了一个好处,a 字段能索引下推,会减少回表的次数;
回答
我会创建(bac)联合索引或者(bca)联合索引,因为这两种联合索引都可以有 2 个字段走索引。比如
创建 (bac) 联合索引,b 和 a 都能走索引,c 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数;
创建 (bca) 联合索引,b 和 c 都能走索引,a 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数;
推荐学习
索引常见面试题(联合索引部分)
执行一条 select 语句,期间发生了什么?(索引下推的概念在执行器有讲解)
56. where a=? And b=? order by c 怎么建立索引?
分析
这里有 order by 排序,我们尽量要用索引来避免额外排序的操作,可以考虑建立 (a,b,c) 联合索引,因为 c 有序的前提是建立在 a=? And b =? 的场景下,刚好符合这个查询条件,这样 c 就不需要额外排序了,天然利用了索引的有序性。
回答
可以建立(a,b,c)联合索引,这样 c 排序的时候,就能利用索引的有序性,避免 using filesort 了。
推荐学习
索引常见面试题(联合索引部分)
57. where a>100 and b=100 and c=123 order by d 怎么建立联合索引?
分析
如果是 bcad 联合索引的话,虽然 bca 能走索引,但是排序 d 无法利用索引,会发生 file sort(因为 a>100范围查询后获得的记录,d 并不一定是有序的了,所以需要额外排序,d有序的前提是a相等的情况下)

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

回答
我觉得建立 bcda 顺序的联合索引比较好,这时候 b 和 c 字段都能走索引,而且 d 能利用索引有序性,避免 filesort,最后的 a 字段虽然无法走索引,但是可以利用索引下推,减少回表的次数。
推荐学习
索引常见面试题(联合索引部分)
执行一条 select 语句,期间发生了什么?(索引下推的概念在执行器有讲解)
58. select b from table where a = 10 and c>20 怎么创建索引?
分析
优先考虑能让 where 查询中的字段能走索引,所以可以考虑(a,c) 联合索引,这样查询的时候,a 和 c 都能走联合索引,然后再考虑 select 的列是否能索引覆盖,很明显这个查询场景,只需要查询 b 列,那么我们可以考虑创建(a,c,b) 联合索引,这时候查询的时候,a 和 c 既能走索引,也能索引覆盖,避免了回表。
回答
可以考虑创建(a,c,b)顺序的联合索引,这时候查询的时候,a 和 c 既能都走索引,也能利用索引覆盖的特性,避免了回表。
59. select id, name from XX where age > 10 and name like ‘xx%’,有联合索引(name,age),说一下查询过程
分析
有三点需要说出来:
能不能走索引?哪些字段能走索引?能走索引,name 能走索引,age 不能走索引。
哪个字段能索引下推? age 字段能索引下推
查询需不需要回表?不需要回表,索引覆盖查询
回答
联合索引的顺序是先 name,再age,结构上是先根据 name 排序,nam 相等的情况下再根据 age 排序。所以优化器需要先匹配 name,name 这时候是右模糊查询,并不会发生索引失效,所以这条 sql 是能走联合索引的,具体的话,只有 name 能走索引,这是因为由于 name 右模糊查询后,age 字段的值并不是有序的,因此 age 无法走索引,但是 age 可以进行索引下推。
最后查询的字段是 id 和 name,这两个字段都能在联合索引上查找到,所以不需要回表,是索引覆盖查询。
推荐学习
索引常见面试题(联合索引部分)
执行一条 select 语句,期间发生了什么?(索引下推的概念在执行器有讲解)
60. where id NOT IN (?, ?, ?) 会走索引吗?
分析
in 能不能走索引,关键是看查询成本,没有绝对说 in 会发生索引失败,也没有绝对说 in 一定能走索引。
回答
要看查询成本,如果走某个索引花费的随机 I/O 比从聚簇索引顺序查(顺序I/O)的成本都还要高,那还不如直接去全表扫描。
举例: num 字段(非唯一二级索引)只包含 3 个值,1、2、3,3 只有几行,而 1、2 各有 100w 行,如果查询条件是 NOT IN (1, 2) 会走索引,如果查询条件是 NOT IN (3) 不会走索引。
61. 如果查询条件中包含索引列和非索引列,MySQL的具体查询流程是什么样的?
分析
假设 a 是索引列,d 是非索引列,select a from test where a = ? and d = ? 查询过程先按索引去查,然后回表再过滤非索引列,会涉及回表的过程。


从上图的执行计划,也可以看到,exta 没有显示 using index,代表没有覆盖索引,然后走了二级索引,所以查询过程有发生回表。
回答
查询过程先按索引去二级索引B+树查,然后拿到主键 id,回表到主键索引B+树再过滤非索引列,查询过程会查 2 个 b+树,涉及回表的过程。
-1. MySQL 索引在什么情况下会失效?使用LIKE 进行模糊查询时,索引什么情况下会失效?
MySQL 索引失效的核心逻辑在于:优化器认为使用索引的代价高于全表扫描时,就会放弃索引。而针对 LIKE 查询,失效规则完全取决于 % 通配符的位置。
一、索引失效的常见通用场景(B+树特性)
违背最左前缀法则(联合索引):
(a,b,c)索引,条件为where b=1或where a=1 and c=1(跳过 b),索引失效。在索引列上做了“隐式类型转换”:如
varchar字段phone用WHERE phone = 1380000,MySQL 会转为数字比较,等同于调用了CAST(phone AS SIGNED),导致失效。对索引列进行函数或表达式计算:
WHERE DATE(create_time) = '2026-01-01'或WHERE salary + 1000 > 5000。使用
OR连接非全部索引列:WHERE indexed_col = 1 OR non_indexed_col = 2,因为要回表全扫,优化器大概率走全表。优化器认为“不值得”:当索引选择性极差(如性别字段),或者表数据量极小,走全表扫描反而更快。
二、LIKE 模糊查询的失效铁律
B+树索引是按从左到右的顺序排列字符串的,因此:
✅ 索引生效(范围扫描):
LIKE '张%'(前缀匹配)。MySQL 能快速定位到张开头的区间,利用索引有序性高效查询。❌ 索引完全失效(全表扫描):
LIKE '%张'或LIKE '%张%'(后缀或中缀匹配)。因为字符串最左侧的字符不确定,B+树的排序规则无法定位起始边界,只能逐行扫描。
面试精简回答(260字)
MySQL 索引失效大多是因为破坏了 B+树的有序前缀查找规则。常见场景包括:联合索引跳过了中间列、索引列被函数或类型转换污染、
OR条件未完全索引,以及优化器因低选择性而选择全表扫描。针对
LIKE模糊查询,规则极其明确:%放在后面(LIKE '张%')走索引,因为 B+树能快速按前缀定位区间;%放在前面或两边(LIKE '%张'或'%张%')索引失效,因为无法确定起始边界,只能全表扫。在实际 Go 开发中,我遵循两条铁律:第一,绝对不允许核心接口直接传参执行
LIKE '%xxx%',必须在 API 层拦截或由前端强制输入前缀;第二,如果业务确实需要中缀搜索(如日志检索),我会在架构层引入 Elasticsearch 的倒排索引来解决,而不是试图给 MySQL 打补丁。如果 DBA 强制要求优化,针对后缀查询可以建一个REVERSE(col)反转索引列来间接利用 B+树前缀匹配。
事务(重要)
62. MySQL 事务有什么特性?
分析
考察事务的 ACID 特性。
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
回答
MySQL 事务有 ACID 四大特性,分别是原子性、一致性、隔离性、持久性。
原子性的意思是事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个环节,原子性是由 undo log 日志保证的;
一致性的意思是事务执行前后,数据库的状态必须保持一致性,一致性是由通过持久性+原子性+隔离性这三个共同保证的;
隔离性的意思是许多个事务并发读写数据库,可以防止多个事务并发读写同一个数据的时候,导致数据不一致问题的发生,隔离性是由 MVCC 和锁保证的;
持久性的意思是保证事务完成后对数据的修改就是永久的,不会因为系统故障而丢失,持久性是由 redo log 日志保证的;
推荐学习
63. 事务的隔离性如何保证?
分析
先说是由 MVCC 和锁实现的,再说一下为什么用 MVCC 和锁能实现隔离性。
回答
事务的隔离性是由 MVCC 和锁保证的。
可重复读隔离级别下的快照读(普通select),是通过 MVCC 来保证事务隔离性的,当前读(update、select ... for update)是通过行级锁来保证事务隔离性的。
推荐学习
64. 事务的持久性如何保证?
分析
先说是由 redo log 实现的,再说一下为什么用 redo log 能实现持久性。
回答
事务的持久性是由 redo log 保证的,因为 MySQL 通过 WAL (先写日志再写数据)机制,在修改数据的时候,会将本次对数据页的修改以 redo log 的形式记录下来,这个时候更新就算完成了,Buffer Pool 的脏页会通过后台线程刷盘,即使在脏页还没刷盘的时候发生了数据库重启,由于修改操作都记录到了 redo log,之前已提交的记录都不会丢失,重启后就通过 redo log,恢复脏页数据,从而保证了事务的持久性。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
65. 事务的原子性如何保证?
分析
先说是由 undo log 实现的,再说一下为什么用 undo log 能实现原子性。
回答
事务的原子性是通过 undo log 实现的,在事务还没提交前,历史数据会记录在 undo log 中,如果事务执行过程中,出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据,将数据恢复到事务开始之前的状态,从而保证了事务的原子性。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
66. MySQL事务和Redis 事务有什么区别?
分析
Redis事务没保证原子性和持久性。
原子性:Redis 事务没有回滚功能,没办法实现跟MySQL事务一样的原子性,就是没办法保证事务执行期间,要不全部失败,要不全部成功,如果Redis事务执行过程中,中间有命令是错误的,不会停止执行和回滚,这时候事务的执行会出现半成功的状态。
持久性:如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况(为什么 always 也会丢失看这篇:redis能保证数据100%不丢失吗? ),所以,事务的持久性属性也还是得不到保证。所以,不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的。
回答
MySQL事务能够实现ACID四大特性,而Redis事务没保证原子性和持久性。
Redis 事务没有回滚功能,没办法实现跟MySQL事务一样的原子性,就是没办法保证事务执行期间,要不全部失败,要不全部成功,Redis 事务执行过程中,如果中途有命令执行出错了,不会停止和回滚,而是继续执行,那么就可能出现半成功的状态。
Redis 不管是 AOF 模式,还是 RDB 快照,都没办法保证数据不丢失,所以 Redis 事务不具有持久性。
推荐学习
67.🌟 MySQL 事务隔离级别有哪些?分别解决哪些问题?
分析
MySQL 共有四个隔离级别如下,按隔离水平高低排序如下:
读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;(无锁导致脏读)
读已提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;(行锁解决了脏读)
可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;(解决了幻读和不可重复读)
串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
针对不同的隔离级别,并发事务时可能发生的现象也会不同。

脏读、不可重复读、幻读的意思:
脏读是指一个事务读取了另一个事务还未提交的数据,如果另一个事务回滚,则读取的数据是无效的。脏读可能导致数据的不一致性。
不可重复读是指一个事务多次读取同一条记录,但是在此期间另一个事务修改了该记录,导致前后读取的数据不一致。不可重复读可能导致数据的不一致性。
幻读是指一个事务多次执行同一个查询,但是在此期间另一个事务插入了符合该查询条件的新数据,导致前后查询的结果不一致。幻读可能导致数据的不完整性。
回答
MySQL 默认隔离级别是可重复读,除此之外, MySQL 还支持读未提交、读提交、串行化 这三个隔离级别。
我了解到事务并发问题存在脏读、不可重复读、幻读这三种,不同的隔离级别,解决的问题也各不同的。
读未提交一个问题都没有解决。
读已提交避免了脏读问题,但是还存在不可重复读和幻读这两个问题。
可重复读避免了脏读和不可重复读的问题,不过对于幻读问题是很大程度上避免了,没有完全避免。
串行化是所有问题都可以避免,但是事务的并发性能是最差的。
推荐学习
68. 串行化隔离级别是通过什么实现的?
分析
串行化隔离级别是安全性最高的隔离级别,但是也是性能最差的隔离级别,读、写操作都采用加行级别锁的方式来解决脏读、不可重复读、幻读的问题,现实中基本不会用到串行化隔离级别,因为性能太差了,没有MVCC机制,读写操作没办法并发。
回答
串行化隔离级别所有SQL都会加行级锁,包括普通的 select 查询,都会加 S 型的 next-key 锁。其他事务就没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读现象,性能是隔离级别中最差的,没有MVCC机制,读写操作没办法并发。
**推荐学习 **【mysql】串行化隔离级别
69. 脏读和幻读有什么区别?
分析
脏读是一个事务读到了另一个未提交事务修改过的数据。

在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

回答
脏读是一个事务读到了另一个未提交事务修改过的数据,如果另外一个事务回滚了,刚才读到的数据就与数据库里的数据不一致了。
幻读是前后两次的查询的结果集的数量是不同,比如,如果 select 执行了两次,但第二次返回了第一次没有返回的行数据,则该行是“幻像”行。
推荐学习
70. MySQL默认的隔离级别是什么?怎么实现的?
分析
考察可重复度的实现原理。
回答
MySQL默认的隔离级别是可重复读。
select 查询是通过 MVCC 实现的,在 MVCC 实现中,每条记录都会保存多个版本,每个版本都有一个版本号,事务在读取数据时,会根据事务开始时的版本号来读取数据,从而保证了事务的隔离性。可重复读隔离级别是在开启事务后,执行一条 select 语句的时候, 会生成一个 Read View(快照读),后续事务查询数据的时候都在复用 Read View,所以保证了事务期间多次读到的数据都是一致的。
推荐学习
-1. 介绍一下 MVCC
分析
从 MVCC 是什么?解决了什么问题?MVCC 实现原理?这三个方向回答。
注意,不用展开讲解可见性规则的判断,不然这个问题要回答很长时间,面试官可能会不耐烦,如果他追问,才去回答。
mvcc全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undolog日志、readView。
• 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select... lock in share mode(共享锁),select ..for update、update、insert、delete(排他锁)都是一种当前读。
• 快照读
简单的select(不加锁)就是快照读**,快照读,读取的是记录数据的可见版本**,有可能是历史数据,不加锁,是非阻塞读。
.Read Committed:每次select,都生成一个快照读。
. Repeatable Read:开启事务后第一个select语句才是快照读的地方。
. Serializable:快照读会退化为当前读。
当前读和快照读举个例子:两个终端1,2同时开启事务,1使用正常的select 查询得到id=1的name为bob,然后2在事务中修改bob为tom,无论提不提交事务,1再次在事务中查询仍是bob,因为普通的select使用的是快照读,如果在select加上for update这种,就会成为当前读,得到正确的tom.

undo log
回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

DB_TRX_ID为1的记录中DB_ROLL_PTR 为null是因为这是插入的一条语句,在这行数据上没有更早的版本了,所以会滚指针为null
readview



回答
MVCC 是多版本并发控制,是通过记录历史版本数据,解决读写并发冲突问题,避免了读数据时加锁,提高了事务的并发性能。
MySQL将历史数据存储在 undo log 中,结构逻辑上类似一个链表,MySQL数据行上有两个隐藏列,一个是事务ID,一个就是指向 undo log 的指针。
事务开启后,执行第一条 select 语句的时候,会创建 ReadView ,ReadView 记录了当前未提交的事务,通过与历史数据的事务 ID 比较,就可以根据可见性规则进行判断,判断这条记录是否可见,如果可见就直接将这个数据返回给客客户端,如果不可见就继续往undo log 版本链查找第一个可见的数据。需要展开说说可见性规则吗?
推荐学习
-1. MVCC是如何解决幻读的
MVCC,即多版本并发控制,是InnoDB实现高并发、解决读写冲突的核心技术。它通过为数据行保留多个历史版本,实现了读操作不加锁、不阻塞写操作的高效并发模型。
InnoDB的MVCC依赖于三个核心组件:隐藏列(DB_TRX_ID事务ID和DB_ROLL_PTR回滚指针)、由Undo Log构成的版本链以及Read View(读视图)。当事务进行快照读时,会基于Read View的可见性规则,沿着版本链找到对其可见的历史版本。
在解决幻读问题上,InnoDB在可重复读隔离级别下采取了“MVCC + Next-Key Lock”的组合策略。MVCC保障了快照读(普通SELECT)不会出现幻读;而Next-Key Lock(行锁+间隙锁)则为当前读(如SELECT ... FOR UPDATE)提供保护,通过锁定扫描的索引记录及其间的间隙,阻止其他事务插入新数据,从而避免了幻读。
72. MVCC的如何判断行记录对某一个事务是否可见
分析
Read View 有四个重要的字段:

m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
creator_trx_id :指的是创建该 Read View 的事务的事务 id。
聚簇索引记录中都包含下面两个隐藏列:

trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
如果记录的 trx_id 值小于 Read View 中的
min_trx_id值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果记录的 trx_id 值大于等于 Read View 中的
max_trx_id值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。如果记录的 trx_id 值在 Read View 的
min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中:如果记录的 trx_id 在
m_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。如果记录的 trx_id 不在
m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
回答
我们每一条记录都有两个隐藏列,一个是事务 id,一个是指向历史数据 undo log 的指针,然后 Read View 有四个字段,分别是创建 Read View 的事务 id、活跃事务 id 列表、活跃事务 id 列表中最小的 id、下一个事务的 id。主要有这几种判断规则:
如果记录的事务 id 小于活跃事务 id 列表中最小的 id,就说明该记录是在创建 Read View 前就生成好了,所以该记录是当前事务是可见的。
如果记录的事务 id 大于下一个事务的 id,就说明该记录是在创建 Read View 后才生成的,所以该记录是当前事务是不可见的。
如果记录隐藏列的事务 id 在最小的 id 和下一个事务的 id 之间,这时候就需要看记录的事务 id 是否在活跃事务 id 列表中:
如果记录的事务 id 在活跃事务 id 列表中,说明修改该记录的事务还没提交,所以该记录是不可见的。
如果记录的事务 id 不在活跃事务 id 列表中,说明修改该记录的事务已经提交了,那么该记录就是可见。
推荐学习
73. 读已提交和可重复读隔离级别实现 MVCC 的区别?
分析
生成 readview 的时机不同
回答
读已提交和可重复读隔离级别都是由 MVCC 实现的,它们的区别在于创建 Read View 的时机不同。
读已提交隔离级别在事务开启后,每次执行 select 都会生成一个新的 Read View,所以每次 select 都能看到其他事务最近提交的数据。
可重复读隔离级别在事务开启后,执行第一条 select 时生成一个 Read View,然后整个事务期间都在复用用这个 Read View,所以一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。
推荐学习
74. 为什么互联网公司用读已提交隔离级别?
分析
读已提交并发性能更高,因为读已提交没有间隙锁,只有记录锁,而可重复读是会有记录锁和间隙锁,所以读已提交隔离级别发生死锁的概率比较小。
回答
读已提交的并发性能更好,因为读已提交没有间隙锁,只有记录锁,发生死锁的概率比较低。然后互联网业务对于幻读和不可重复读的问题都是能接受的,所以为了降低死锁的概率,提高事务的并发性能,都会选择使用读已提交隔离级别。
推荐学习
75. 可重复读隔离级别是如何解决不可重复读的?
分析
分两种查询来回答
快照读,靠MVCC解决不可重复读
当前读,靠行级锁中的记录锁解决不可重复读
回答
MySQL 提供了两种查询方式,一种是快照读,就是普通 select 语句,另外一种是当前读,比如 select for update 语句。不同的查询方式,解决不可重复读问题的方式是不一样的。
针对快照读的话,是通过 MVCC 机制来解决的,在可重复读隔离级别下, 第一次select查询的时候,会生成 readview,在第二次执行select查询的时候,会复用这个readview,这样前后两次查询的记录都是一样的,不会读到其他事务更新的操作,这样就不会发生不可重复读的问题了。
针对当前读的话,是靠行级锁中的记录锁来实现的,在可重复读隔离级别下,第一次 select for update 语句查询的时候,会对记录加next-key 锁,这个锁包含记录锁,这时候如果其他事务更新了加了锁的记录,都会被阻塞住,这样就不会发生不可重复读的问题了。
推荐学习
76. 可重复读隔离级别是怎么解决幻读的?
分析
分两种查询来回答
快照读,靠MVCC解决幻读
当前读,靠行级锁中的间隙锁解决幻读
回答
MySQL 提供了两种查询方式,一种是快照读,就是普通 select 语句,另外一种是当前读,比如 select for update 语句。不同的查询方式,解决幻读问题的方式是不一样的。
针对快照读的话,是通过 MVCC 机制来解决的,在可重复读隔离级别下, 第一次select查询的时候,会生成 readview,在第二次执行select查询的时候,会复用这个readview,这样前后两次查询的结果集都是一样的,不会读到其他事务新插入的记录,这样就不会发生幻读的问题了。
针对当前读的话,是靠行级锁中的间隙锁来实现的,在可重复读隔离级别下,第一次 select for update 语句查询的时候,会对记录加next-key 锁,这个锁包含间隙锁,这时候如果其他事务往这个间隙插入新记录的话,都会被阻塞住,这样就不会发生幻读的问题了。
推荐学习
77. 可重复读隔离级别解决了什么问题?有没有完全解决幻读?
分析
要强调可重复读隔离级别是很大程度上解决了幻读,并没有完全解决幻读。
回答
可重复读隔离级别解决了脏读、不可重复读问题,幻读也很大程度上避免了,但是我觉得并没有完全解决幻读,在一些特殊的场景,还是会发生幻读的问题,需要我展开说下吗?
推荐学习
78. 可重复读隔离级别为什么不能完全避免幻读?什么情况下出现幻读?
分析
可重复读隔离级别场景下。
发生幻读的第一个场景:

数据库表不存在 id=5 的记录,事务 a 执行第一次查询的时候,读不到该记录,接着事务 b 插入了 id=5 的新记录。
事务 a 的更新语句更新了事务 b 刚插入的 id =5 的这条记录,由于更新操作是当前读,所以事务 a 的更新操作能读到 id=5 的记录并进行更新,更新的时候,会把id=5 这条记录隐藏列的事务 id 变为事务 a id,代表是事务 a 修改的。
然后事务 a 第二次查询的时候,发现 id=5 的事务 id,跟本事务的 id 是一样的,就会认为是可见的,那么就能读到id=5 的数据了, 此时也就发生了幻读现象。
发生幻读的第二个场景。
T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
回答
在可重复读隔离级别场景下,当先快照读再当前读的场景下可能会出现幻读的问题。
比如说这个场景,事务 A 通过快照读的方式查询 id = 5 的记录,此时数据库没有这条记录,然后事务 B 向这张表中新插入了一条 id = 5 的记录并提交了事务。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录隐藏列中的事务id就变成了事务 A 的事务 id,这时候事务 A 再使用 select 语句去查询这条记录时就可以看到这条记录了,这里事务 A 前后两次查询的结果集合数不一样了,于是就发生了幻读。
我还知道另外一个场景,事务 A 通过快照读的方式查询 id 大于 100 的记录,假设这时候有 1 条记录,然后事务 B 插入了 id = 200 的记录并提交了事务,接着事务 A 通过当前读的方式查询 id 大于 100 的记录,这时候就会得到 2 条记录,事务 A 前后两次查询的结果集合数不一样了,就发生了幻读。
我觉得上面这两种发生幻读的场景,也是可以避免的,就是尽量在开启事务之后,马上执行 select ... for update 语句,因为它会对记录加临键锁(next-key 锁),这样就可以避免其他事务插入一条新记录,就避免了幻读的问题。
推荐学习
79. 可重复读隔离级别,MVCC完全解决了不可重复读问题吗?
分析
不可重复读,代表前后两次查询的记录的值不一样了。
比如表里现在有 id=1,value=1 的记录。
事务 a,先执行 select,查询到 id=1 的 value 是 1
事务 b,更新 id=1的 value为 2,然后提交事务。
事务 a,执行 select for update,当前读,然后就读到 id = 1,value=2 的记录了,意味着发生了不可重复读。
回答
如果前后两次查询都是快照读,就是普通的 select 的话,那就不会产生不可重复读的问题的。但是如果第一次查询是快照读,第二次查询是当前读,那么就可能会发生不可重复读的问题。
80. 一个事务里有特别多SQL的弊端?
分析
一个事务如果有特别多SQL,那么这个事务就会被称为长事务(大事务)。
长事务有什么影响?
锁定数据过多,容易造成大量的死锁和锁超时:锁是事务提交的时候才释放的,那么长事务会导致锁持久的时间过长,很容易导致大量的死锁和锁超时的问题
**回滚记录占用大量存储空间,事务回滚时间长:**每条记录在更新的时候都会同时记录一条回滚操作,会产生 undo 日志,而 undo 日志是事务提交并且没有事务依赖的时候才会被清理,那么长事务就会导致 undo 日志堆积很多,占用存储空间,也会导致回滚的时间过长。
**执行时间长,容易造成主从延迟:**因为主库上必须等事务执行完成才会写入binlog,再传给备库。所以,如果一个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟
并发情况下数据库连接池容易被撑爆:在长事务中,连接可能会被持续打开,这会占用数据库连接池的资源,可能导致连接池被占满
怎么查找数据库中的长事务?
可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx
where TIME_TO_SEC(timediff(now(),trx_started))>60回答
锁是事务提交的时候才释放的,那么长事务会导致锁持久的时间过长,容易导致大量的死锁和锁超时的问题
执行事务中每条增删改SQL会产生 undo 日志,那么长事务就会导致 undo 日志堆积很多,占用存储空间,也会导致回滚的时间过长。
长事务执行时间长,容易造成主从延迟**,**如果一个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟
在长事务中,连接可能会被持续打开,这会占用数据库连接池的资源,可能导致连接池被占满
推荐学习
锁
81. 详细说一下 MySQL数据库中锁的分类(重要)
分析
不一定要把每个锁都说出来,要重点表达自己用过或者熟悉哪些锁,也要强调只有 Innodb 存储引擎实现了行级锁,不过行级锁有哪些还是要说出来。
全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
表级锁:MySQL 里面表级别的锁有这几种:
表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
插入意向锁,当插入位置的下一条记录有间隙锁,那么就会生成插入意向锁,然后进入阻塞状态
回答
根据锁粒度的不同, MySQL 的锁可以分为全局锁、表级锁、行级锁。
我比较熟悉的是表级锁和行级锁,比如我们对一张表结构进行修改的时候,MySQL 就会对这张表加一个元数据锁,元数据锁是属于表级锁的。
行级锁目前只有 Innodb 存储引擎实现了,MyISAM 存储引擎是不支持行级锁的,只有表锁。Innodb 存储引擎实现的行级锁主要有记录锁、间隙锁、临键锁、插入意向锁这些,当我们对表记录进行 select for update,或者增删改的时候,都会对记录加行级锁。
推荐学习
82. MySQL 怎么实现乐观锁?(重要)
分析
可以基于版本号来实现乐观锁,修改数据的时候带上版本号(或者时间戳):
UPDATE student SET name = ‘小李’, version= 2 WHERE id= 100 AND version= 1
回答
可以在数据库表增加一个版本号字段,利用这个版本号字段在数据库中实现乐观锁。
具体的实现,每次更新数据的时候,都要带上版本号,同时将版本+1,比如现在要更新id=1,版本号为2的记录。这时候先要获取id=1的版本号,然后更新语句写成 update table set name = “小明”, version = version+1 where id = 1 and version = 2。
如果这个版本号与表记录中的版本号一致的话,就能更新成功,如果不相等则不进行更新,然后需要重新获取该记录的最新版本号,然后再尝试更新数据。
推荐学习
83. 在线上修改表结构,会发生什么?
分析
考察表级锁:元数据锁。
回答
线上环境可能存在很多事务都在读写这张表,如果对这张表进行了表结构修改,就会发生阻塞,原因是有事务对这张表进行读写操作的时候,会生成元数据读锁,而修改表结构的时候,会生成元数据写锁,这时候就产生了读写冲突,所以修改表结构的操作就会阻塞,并且后续事务的增删查改操作都会阻塞。
推荐学习
84. 创建索引的时候会锁表吗?
分析
对表结构修改,增加字段,删除字段,增加索引,删除索引,更改索引名字,更改字段名字等等,这些操作都会加MDL写锁(元数据写锁),属于表级锁,会和 MDL 读锁发生读写冲突。
回答
会的,创建索引的时候会加MDL写锁,如果这时候有其他事务对这张表进行增删查改的话,这些事务就都会被阻塞,原因是有事务对这张表进行读写操作的时候,会生成MDL读锁,这时候就产生了读写冲突。
推荐学习
85. Innodb 存储引擎中的行级锁有哪些?(重要)
分析
主要是有「记录锁、间隙锁、临键锁、插入意向锁」。
记录锁
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
举个例子,当一个事务执行了下面这条语句:
mysql > begin;
mysql > select * from t_test where id = 1 for update;就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。
当事务执行 commit 后,事务过程中生成的锁都会被释放。
间隙锁
Gap Lock 称为间隙锁,只存在于可重复读隔离级别和串行化隔离级(读已提交隔离级别不存在间隙锁),目的是为了解决可重复读隔离级别下幻读的现象。
假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
临键锁
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。
虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。
插入意向锁
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
举个例子,假设事务 A 已经对表加了一个范围 id 为(3,5)间隙锁。

当事务 A 还没提交的时候,事务 B 向该表插入一条 id = 4 的新记录,这时会判断到插入的位置已经被事务 A 加了间隙锁,于是事物 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),此时事务 B 就会发生阻塞,直到事务 A 提交了事务。
插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。
如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。
插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
回答
Innodb 实现的行级锁有记录锁、间隙锁、临键锁、插入意向锁。我们在使用增删改或者锁定读语句的时候,都会对记录加行级锁。
记录锁,可以避免其他事务对该记录进行删除和更新操作
间隙锁,可以避免其他事务往间隙里插入新记录
临键锁,是记录锁和间隙锁的组合,所以它既可以免其他事务对该记录进行删除和更新操作,也可以避免其他事务往间隙里插入新记录
插入意向锁,插入意向锁和间隙锁是互斥的关系,其他事务插入的时候,发现插入位置的下一条记录有间隙锁的话,才会生成的插入意向锁,并且这时候锁的状态是阻塞状态,目的是告诉用户插入的位置存在间隙锁
推荐学习
86. 间隙锁的工作原理是什么?
分析
间隙锁是可重复读隔离级别和串行化隔离级别下才有的锁(读已提交隔离级别只有记录锁,没有间隙锁),主要是为了防止幻读的问题,它可以阻止其他事务往间隙里插入新记录。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
回答
间隙锁防止其他事务往间隙插入新记录,从而可以避免幻读的问题,具体的原理是当其他事务插入记录的时候,当发现插入位置的下一条记录有间隙锁,就会生成插入意向锁,然后锁设置为阻塞状态,目的是告诉用户插入的位置存在间隙锁
推荐学习
87. 一条Update语句没有带where条件,加的是什么锁?
分析
Innodb 加锁是索引加锁,可重复读级别下,加锁的基本单位是next-key锁。读已提交隔离级别下,加锁的基本单位是记录锁。
更新没有带 where 条件,会全表扫描,会对每一条记录都加锁。
回答
可重复读级别下,更新没有带 where 条件,会全表扫描,会对每一条记录都加next-key锁,相当于锁住了全表。
读已提交隔离级别下, 没有间隙锁,更新没有带 where 条件,是全表扫描,那么会对每一条记录都加记录锁。
推荐学习
88. 带了where条件没有命中索引,加的是什么锁?
分析
同上
回答
没有命中索引,是全表扫描,那么:
在可重复读级别下,全表扫描的话,会对每一条记录都加next-key锁。
在读已提交隔离级别下, 因为没有间隙锁,全表扫描的时候,会对每一条记录都加记录锁。
推荐学习
89. 两条更新语句更新同一条记录,加的是什么锁?
分析
在可重复读级别下,加锁的基本单位是next-key锁,但是在一些场景下,会退化成记录锁或者间隙锁。
这个题目更新同一条记录,就认为是等值查询的场景。要考虑这几种情况:
- 第一种情况:如果更新条件的字段是唯一索引,加什么锁?

- 第二种情况:如果更新条件的字段是非唯一索引,加什么锁?

- 第三种情况:如果更新条件的字段是没有索引,加什么锁?

回答
在可重复读级别下,可能有这些情况:
如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
如果存在,那么这条记录加的记录锁,只锁住该条记录;
如果这条记录不存在,则加间隙锁。
如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加next-key锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加next-key锁。
推荐学习
90. 两条更新语句更新同一条记录的不同字段,加的是什么锁?
分析
Innodb 加锁是加在行记录索引上的,不是针对更新的字段加锁。所以是不是更新同一个字段,没有关系。只要在这条记录上有更新操作,就会对这条记录加锁。所以这题的加锁的情况,也跟上一题一样。
回答
在可重复读级别下,可能有这些情况:
如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
如果存在,那么这条记录加的记录锁,只锁住该条记录;
如果这条记录不存在,则加间隙锁。
如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加next-key锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加next-key锁。
推荐学习
91. 可重复读场景,下面的场景会发生什么?

分析
这题要注意的是事务A和事务更新语句,都是更新不存在的id,这时候会加间隙锁,有了间隙锁就会阻止插入操作。
事务 A 和 事务 B 都在执行 insert 语句后,都陷入了等待状态,都在相互等待对方释放间隙锁,于是就发生了死锁。

回答
事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
推荐学习
92. 了解过 MySQL 死锁问题吗?
分析
解释 MySQL 死锁是如何发生的。
回答
了解过,在并发事务中,当两个事务出现循环资源依赖,这两个事务都在等待别的事务释放资源时,就会导致这两个事务都进入无限等待的状态,这时候就发生了死锁。
推荐学习
93. MySQL 怎么排查死锁问题?
分析
获取死锁日志,分析死锁日志
回答
在遇到线上死锁问题时,我们应该第一时间获取相关的死锁日志。我们可以通过 show engine innodb status 命令来获取死锁信息。
然后就分析死锁日志。死锁日志通常分为两部分,上半部分说明了事务1在等待什么锁,下半部分说明了事务2当前持有的锁和等待的锁。
通过阅读死锁日志,我们可以清楚地知道两个事务形成了怎样的循环等待,然后根据当前各个事务执行的SQL分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。
推荐学习
94. MySQL 怎么避免死锁?
分析
要明确 MySQL 死锁是不能完全避免的,得通过一些手段降低死锁的概率。
缩短锁持久的时间,来降低死锁的概率
通过减少间隙锁,来降低死锁的概率
通过减少加锁范围,来降低死锁的概率
通过MySQL参数设置,来降低死锁的概率
设置锁等待超时参数:
innodb_lock_wait_timeout开启主动死锁检测:
innodb_deadlock_detect
回答
实际上死锁是不能完全避免的,只要会加锁,在并发的场景就会发生死锁,但是我们可以通过一些手段,降低发生死锁的概率。
MySQL 的锁是在事务提交的时候才会释放的,所以可以通过缩短锁持久的时间,来降低死锁的概率,比如:
如果事务中需要锁多个行,要把最可能造成锁冲突的锁的申请时机尽量往后放,这样事务的持久锁的时间就会比较短。
避免大事务,尽量将大事务拆成多个小事务来处理,因为大事务占用耗时长,意味着占用锁占用时间长,与其他事务冲突的概率也会变高;
可以通过减少间隙锁,来降低死锁的概率:
- 如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,因为 RC 隔离级别没有间隙锁,可以避免间隙锁导致的死锁;
可以通过减少加锁范围,来降低死锁的概率:
- 给表添加合理的索引,如果不走索引将会为表的每一行记录加行级锁,死锁的概率就会大大增大;
可以通过MySQL参数设置,来降低死锁的概率:
设置合适的锁等待超时阈值,当一个事务的等待时间超过该值后,将回滚当前语句 (而不是整个事务),如果要回滚整个事务,请使用“innodb_rollback_on_timeout” 开启值为:ON,开启这个参数之后,锁超时就会对这个事务进行回滚,于是锁就释放了。
开启主动死锁检测,主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
推荐学习
日志
95. MySQL三大日志是什么?(重要)
分析
说出 undolog(回滚日志)、redolog(重做日志)、binlog 三种日志的作用。
回答
undo log是Innodb存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和MVCC。在事务没提交之前,Innodb会先记录更新前的数据记录 undo log中,回滚时利用 undo log 来进行回滚。
redo log 也是Innodb存储引擎层的日志,属于物理日志,记录了某个数据页做了什么修改,实现了事务的持久性,主要用于掉电等故障恢复。比如某个事务提交了,脏页数据还没有刷盘,如果 MySQL 机器断电了,脏页的数据就丢失了,MySQL 重启后可以通过redolog日志,可以将已提交事务的数据恢复回来。
binlog 是 Server 层生成的日志,主要用于数据备份和主从复制。在完成一条更新操作后,Server 层会生成一条 binlog,等之后事务提交的时候,会将该事务执行过程中产生的所有 binlog 统一写入 binlog 文件。binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
-1. 请解释一下 Redo Log 和 Undo Log 的作用及其底层实现原理
Redo Log 和 Undo Log 是 InnoDB 事务日志的“一体两面”,共同保证了事务的 持久性(Durability) 和 原子性(Atomicity),并支撑了 MVCC 的实现。它们都遵循 WAL(Write-Ahead Logging,预写日志) 原则,即日志先落盘,数据页后写入磁盘。
一、Redo Log(重做日志):保障持久性
1. 核心作用
Redo Log 用于在数据库崩溃恢复时,重做已提交事务对数据的修改,确保事务的持久性。它解决了“数据页在内存中修改后,尚未刷入磁盘时 MySQL 宕机”的数据丢失问题。
2. 底层实现原理
物理日志(Physical Log):Redo Log 记录的是对数据页的物理修改,例如“在表空间 ID 为 X、页号为 Y 的偏移量 Z 处,写入特定字节”。这种格式不依赖于 SQL 语义,恢复时直接应用物理变更,效率极高。
固定大小、循环写入:Redo Log 由一组固定大小的文件(如
ib_logfile0、ib_logfile1)组成,采用循环写入策略。write pos记录当前写入位置,checkpoint记录已刷盘的位置。当write pos追上checkpoint时,InnoDB 会触发强制刷盘,将脏页落盘并推进checkpoint,从而覆盖旧日志。
二、Undo Log(撤销日志):保障原子性与 MVCC
1. 核心作用
Undo Log 用于回滚未提交事务对数据的修改,保证原子性。同时,它为 MVCC 提供一致性读视图,让读操作能访问到历史版本的数据。
2. 底层实现原理
逻辑日志(Logical Log):与 Redo 不同,Undo Log 记录的是反向操作。例如,
INSERT对应DELETE操作(记录主键值);DELETE对应INSERT(记录完整行数据);UPDATE对应反向的UPDATE(记录被修改列的旧值)。版本链串联:每条 Undo Log 都通过回滚指针(
DB_ROLL_PTR)与数据行关联,多个历史版本通过指针串联成版本链。新事务修改数据时,会将旧版本数据写入 Undo Log 并挂到版本链头部。独立表空间存储:Undo Log 存放在独立的 Undo 表空间中(MySQL 5.6 后),通过回滚段(Rollback Segment)进行管理,每个回滚段内部又有多个 Undo 槽位。
三、两者的协作关系与崩溃恢复流程
关键点:Undo Log 本身也是数据,它的生成也需要记录 Redo Log。这是因为 Undo Log 存放在独立的表空间中,内存中的 Undo 页也需要在宕机后恢复。
崩溃恢复的完整流程(InnoDB 启动时):
应用 Redo Log(前滚):扫描 Redo Log,将所有已落盘的日志(包括已提交和未提交事务的修改)重放到数据页中,将数据库恢复到宕机前的物理状态。
回滚未提交事务(后滚):此时数据库处于一致物理状态,但存在未提交事务。InnoDB 会利用 Undo Log 将这些未提交事务的所有修改进行回滚撤销。
四、面试精简回答(200-300 字)
Redo Log 和 Undo Log 是 InnoDB 事务日志的基石,Redo 保证持久性,Undo 保证原子性并支撑 MVCC。
Redo Log 是物理日志,记录数据页的字节级修改。它采用固定大小文件循环写入,配合 LSN 序列号实现顺序追加。通过 WAL 预写机制和组提交优化,确保事务提交前日志落盘,崩溃时能重做已提交修改。
Undo Log 是逻辑日志,存储反向操作(INSERT 对应 DELETE,UPDATE 记录旧值)。它被组织成版本链,通过回滚指针串联。事务回滚时,利用 Undo Log 逆向恢复;一致性读时,沿版本链查找符合可见性规则的历史版本。事务提交后,Purge 线程会异步清理不再需要的 Undo 版本。
两者相互协作:Undo 自身的变更也要记录 Redo。崩溃恢复时,先利用 Redo 前滚所有修改,再通过 Undo 回滚未提交事务,最终保证数据库既完整又一致。这种设计实现了高并发下的事务安全,同时兼顾了恢复效率。
96. redo log 和 binlog 的区别和应用场景?
分析
redo log 和 binlog 有4个区别的地方。
适用对象不同:
binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
redo log 是 Innodb 存储引擎实现的日志;
文件格式不同:
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
写入方式不同:
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
用途不同:
binlog 用于备份恢复、主从复制;
redo log 用于掉电等故障恢复。
面试的时候,重点说出两个日志的内容和应用场景的区别。
回答
redo log 是 InnoDB 引擎实现的日志,属于物理日志,记录了 Innodb 存储引擎对数据页所做的修改操作,主要用于崩溃恢复,比如某个事务提交了,脏页数据还没有刷盘,如果 MySQL 机器断电了,脏页的数据就丢失了,MySQL 重启后可以通过重做日志,可以将已提交事务的数据恢复回来。redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志.
binlog 是 server 层实现的日志,保存了所有对数据库的增删改操作,binlog 有三种日志格式,日志的内容可能是 SQL 语句、数据本身或两者的混合,主要用于数据库备份和归档,也用于主从复制。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
97. redo log 和 binlog 在恢复数据库有什么区别?
分析
考察 redo log 和 binlog 应用区别。
回答
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存了所有对数据库的更新操作,可以用来恢复数据库某个时刻的数据或者全量恢复数据库数据。
redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存的是 Innodb 存储引擎对数据页所做的修改操作,用来恢复因中途 MySQL 断电丢失的脏页数据。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
98. 为什么崩溃恢复不用binlog 而用redolog?
分析

回答
binlog 是 server 层的日志,不会记录 innodb 存储引擎层中有哪些数据页没有被刷盘,redolog 是 innodb 层的日志,可以记录哪些脏页没有被刷盘,崩溃恢复的时候,恢复的粒度更细粒,可以精确到需要恢复的数据页,而 binlog 保存的是全量日志,没办法做到这一点,所以崩溃恢复用的是redolog
推荐资料
为什么 redo log 具有 crash-safe 的能力,是 binlog 无法替代的?
99. binlog的三种格式是什么?
分析
分别说出 binlog 的 STATEMENT、ROW、MIXED 格式的特点即可。
回答
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中,主从复制中 slave 端再根据 SQL 语句重现。缺陷:STATEMENT 有动态函数的问题,比如用了 uuid 或者 now 这些函数,在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致
ROW:记录行数据最终被修改成什么样了,不会出现 STATEMENT 下动态函数的问题。缺陷:但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句。
MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式。
推荐学习
100. redo log 是怎么实现持久化的?
分析
先说明没有 redo log 前,数据库发生宕机时,脏页数据可能会发生丢失的问题。
再说明引入了 redo log 后,是怎么实现持久化的。
回答
事务执行过程更新的数据,并不是在事务提交的时候,就把修改的数据刷入磁盘的,而是修改 buffer pool 中数据页,并标记为脏页,然后后台再找合适的时间刷盘。
如果事务提交了,脏页数据没有刷盘时,数据库发生宕机,这就会导致事务修改的数据丢失了。
所以 MySQL 就引入了 redo log, redo log 保存的内容是物理日志,主要是记录Innodb对某个数据页的修改操作,当事务提交的时候,redo log 会先刷入磁盘,因为 redo log 保存了数据页的修改操作,即使脏页数据没有刷盘时数据库发生宕机了,重启后 MySQL 通过重放 redo log ,就能恢复未刷盘的脏页,保证了数据的持久化。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
101. redo log除了崩溃恢复还有什么其他作用?
分析
写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。针对「顺序写」为什么比「随机写」更快这个问题,可以比喻为你有一个本子,按照顺序一页一页写肯定比写一个字都要找到对应页写快得多。
可以说这是 WAL 技术的另外一个优点:MySQL 的写操作从磁盘的「随机写」变成了「顺序写」,提升语句的执行性能。这是因为 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。
针对为什么需要 redo log 这个问题我们有两个答案:
实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
回答
写 Redolog 日志是追加的形式,所以 redolog 写磁盘是一个顺序写的过程,而数据页写磁盘是一个随机写的过程,顺序写的性能是比随机写性能高的,事务在提交的时候,是先写日志再写数据的机制,相当于把 MySQL 写入磁盘的操作从磁盘随机写成了顺序写,所以 redo log 还可以起到提升 MySQL 写入磁盘性能的作用。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
102. 为什么需要两阶段提交?
分析
先说结论,再分析没有两阶段提交会有什么问题?
回答
两阶段提交是为了保证 redo log 和 binlog 逻辑一致,从而保证主从复制的时候不会出现数据不一致的问题。
事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,比如在主从复制的场景下,如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入磁盘,这时候主库是最新的数据,而从库是旧数据,这样就造成两份日志之间的逻辑不一致。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
103. 两阶段提交的过程?
分析

回答
两阶段提交把事务的提交拆分成了 2 个阶段,分别是准备阶段和提交阶段。
准备阶段会将 redo log 状态设置为 prepare 状态,然后将 redo log 刷入磁盘;
提交阶段会将 binlog 刷入磁盘,然后设置 redo log 设置为 commit 状态,到这里两阶段就已经完成了。
在两阶段提交中,是以 binlog 刷入磁盘时机作为事务提交成功的标志的:
如果 binlog 还没刷入磁盘的时候,MySQL 就发生了崩溃,MySQL 重启的时候就需要回滚事务;
如果 binlog 刷入磁盘,即使 redo log 没有设置 commit 状态,MySQL 就发生了崩溃,MySQL 重启的时候就会提交事务。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
104. Redolog 刷盘策略有哪三种?
分析
单独执行一个更新语句的时候,InnoDB 引擎会自己启动一个事务,在执行更新语句的过程中,生成的 redo log 先写入到 redo log buffer 中,然后等事务提交的时候,再将缓存在 redo log buffer 中的 redo log 按组的方式「顺序写」到磁盘。
上面这种 redo log 刷盘时机是在事务提交的时候,这个默认的行为。除此之外,InnoDB 还提供了另外两种策略,由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:
当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache(如果你想了解 Page Cache,可以看这篇 ),Page Cache 是专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。
我画了一个图,方便大家理解:

innodb_flush_log_at_trx_commit 为 0 和 2 的时候,什么时候才将 redo log 写入磁盘?
innodb 的后台线程每隔 1 秒:
针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用
write()写到操作系统的 Page Cache,然后调用fsync()持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;针对参数 2 :调用 fsync,将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
加入了后台现线程后,innodb_flush_log_at_trx_commit 的刷盘时机如下图:

这三个参数的数据安全性和写入性能的比较如下:
数据安全性:参数 1 > 参数 2 > 参数 0
写入性能:参数 0 > 参数 2> 参数 1
所以,数据安全性和写入性能是熊掌不可得兼的,要不追求数据安全性,牺牲性能;要不追求性能,牺牲数据安全性。
在一些对数据安全性要求比较高的场景中,显然
innodb_flush_log_at_trx_commit参数需要设置为 1。在一些可以容忍数据库崩溃时丢失 1s 数据的场景中,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。
安全性和性能折中的方案就是参数 2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。
回答
Redolog 刷盘策略主要有三种:
当刷盘策略配置为参数 0 的时候,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作,后续由 innodb 后台线程把缓存在 redo log buffer 中的 redo log,写入到操作系统 pagecache 缓存并持久化到磁盘。
当刷盘策略配置为参数 1 的时候,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘
当刷盘策略配置为参数 2 的时候,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log **写到 操作系统的 pagecache 缓存,**但是并不会执行刷盘操作,后续由 innodb 后台线程来执行刷盘操作。
这三种刷盘策略,参数 1 的模式是数据安全性最高的,但是也是写入性能最差的,而参数 0 是数据安全性最差的,但是是写入性能最好的。
推荐学习
MySQL 日志:undo log、redo log、binlog 有什么用?
性能调优(重要)
105. 怎么查看一条语句是否走了索引?
分析
考察 explain 执行计划输出的信息。

执行计划,参数有:
possible_keys 字段表示可能用到的索引;
key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
key_len 表示索引的长度;
rows 表示扫描的数据行数。
type 表示数据扫描类型,我们需要重点看这个。
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
All(全表扫描):在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。
index(全索引扫描):index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。
range(索引范围扫描):range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
ref(非唯一索引扫描):ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
eq_ref(唯一索引扫描):eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。
const(结果只有一条的主键或唯一索引扫描):const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。需要说明的是 const 类型和 eq_ref 都使用了主键或唯一索引,不过这两个类型有所区别,const 是与常量进行比较,查询效率会更快,而 eq_ref 通常用于多表联查中。
extra 显示的结果,这里说几个重要的参考指标:
**Using filesort **:当查询语句中包含 order by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。
回答
可以通过 explain 查看 SQL 的执行计划,关注 type 字段,这个字段表明 SQL 扫描的方式,如果 type 字段不是 all 或者 index 就代表是索引扫描的方式,这种情况就代表 SQL 走了索引,并且我们还可以通过 key 字段,看这条查询用了哪个索引字段来走索引,如果 key 为 null,也代表没有走索引。
推荐学习
106. extra 字段中的 using index 和 using where 的区别?
分析
考察 explain 执行计划输出的信息。
Using index:这意味着MySQL能够使用覆盖索引(Covering Index)来避免访问表的行。覆盖索引是指一个查询的所有列都包含在索引中,因此查询可以仅通过查看索引来获取所需的信息,无需再去访问表的行。这通常能提高查询的性能。
Using where:这意味着MySQL服务器将在存储引擎检索行后再进行过滤。换句话说,存储引擎返回的行并不一定满足WHERE子句的条件,MySQL服务器需要对这些行进行额外的检查。
这两者并不互斥,可以同时出现在"Extra"字段中,这取决于查询的具体情况。
回答
using index 表示查询使用了索引覆盖,不会回表,这个可以提高查询效率。
using where 表示MySQL的存储引擎返回给 server 层的数据并不一定满足 where 子句的条件,所以MySQL从存储引擎拿到的数据,还得在 server 层进行了 where 子句的条件判断,来过滤出最终 sql 所需要查询的数据。
推荐学习
107. 怎么找到慢 SQL?
分析
考察慢查询日志的应用
回答
可以开启慢查询日志,MySQL 就会自动将执行比较慢的 SQL 语句记录在慢查询日志中,具体多慢我们可以自己设置的,比如设置 3 秒,那么 MySQL 就会将执行超过 3 秒的 SQL 语句记录在慢查询日志中。
推荐学习
108. 如何优化慢 SQL?
分析
常见 SQL 优化的方法。
优化数据访问:limit 子句缩减数据行数、避免 select *
拆分查询:分而治之的思想,将一个大查询拆分多个小查询,每个小查询只返回一部分查询结果。、
覆盖索引:当索引中的列包含所有查询中需要使用的列的时候,可以避免回表
避免索引失效:检查 SQL 是否因为写的不合理,导致索引失效。
分解联表查询:让业务层分多个查询来聚合,或者增加冗余字段减少联表查询
排序优化:对于有排序场景,如果 extra 显示 filesort,这时候就需要考虑对排序的字段建立索引,避免文件排序
回答
优化数据访问:要先确认这条查询语句是否查询了不必要的数据行,可以通过 limit 子句来缩减查询返回的数据行数,如果查询语句用了 select *,需要改进 SQL 语句,只返回需要查询的列。
切分查询:针对一个大查询可以拆分多个小查询,每个小查询只返回一部分查询数据。比如删除一千万行数据,可以改进成分批删除,每一次只删除一批数据,然后睡眠一下,再删除下一批,这样可以将一次性的压力分散到一个很长的时间段中,不仅可以降低对服务器的性能影响,还可以大大减少删除时锁的持续时间。
覆盖索引:如果没有索引字段的话,就需要考虑建立索引,或者建立联合索引,通过覆盖索引的查询,这样就避免回表查询,可以提高查询性能。
避免索引失效:检查 SQL 语句有没有问题,比如对索引进行了计算和函数操作、联合索引没有遵循最左匹配原则等,这些场景都会导致索引失效,这时候就需要修改 SQL 避免索引失效的发生。
分解联表查询:针对联表查询的 SQL 语句,可以将联表查询分解成多个单表查询的语句,然后在业务层来聚合数据,或者增加冗余字段减少联表查询。
排序优化:针对 order by 排序操作,如果执行计划的 extra 显示了文件排序,这时候我们可以对排序字段和其他字段建立联合索引,因为索引数据是天然有序的,这样对索引字段进行排序操作的时候,就不需要文件排序了,提高了查询性能。
推荐学习
《高性能mysql第三版》(看第六章)
109. 深分页场景如何优化?
分析
在系统需要实现分页操作的时候,通常都是用 limit 加上偏移量实现的,如果偏移量太大,就存在性能问题,比如 limit 10000,20 这样的查询,这时候 MySQL 最左叶子节点开始向右扫描 10020 条记录,时间复杂度为O(n),然后只返回 20 条给客户端,前面 10000 条记录都将被抛弃。
如果是使用了二级索引,这种场景性能损失会加剧,因为对于前10000个不需要的数据,MySQL每次也要回表去查找,这就导致了10000次随机IO,会慢成狗。
select * from t_player order by score desc limit 10000,20优化的方式:
- **减少扫描次数:**从业务上改进,将"第几页"改成"下一页”,先记录上一页的最后一条记录的id,然后下次就直接从该记录的位置开始扫描,这样就避免 MySQL 扫描大量不需要的行然后再抛弃掉的问题。方案如下:
---记录score为prev_score
select score from t_player order by score desc limit 20
-- 下一页
select score from t_player where score > prev_score order by score desc limit 20- **减少回表:**如果要遵循第几页的方案,可以通过 SQL 的拆分,来达到目的,思路如下。这句话是说,先从条件查询中,查找数据对应的数据库唯一id值,因为主键在辅助索引上就有,所以不用回归到聚簇索引的磁盘上拉取。如此一来,offset部分均不回表查聚蔟索引,只有limit出来的20个主键id会去查询聚簇索引,这样只会10次IO。
select * from t_player id in
(select id from t_player order by score limit 10000, 20)之前有同学做过实验,5000w 数据的场景下,针对二级索引分页的场景,如果是用 limit n, m 分页的方式,查询速度是286 秒,如果采用方式二来优化,通过子查询来减少回表次数,查询速度只需要 0.7 秒,实验数据参考文档: MySQL优化实战篇-分页优化
回答
分页最简单的实现是使用 limit 字节句,比如每页显示10条内容的话,第一页就是 limit 10、第二页就是 limit 10, 10、第三页20,10。但是这种方式,在深分页的场景,存在严重的性能问题,比如 limit 10000,20 这样的查询,这时候 MySQL 最左叶子节点开始向右扫描 10020 条记录,时间复杂度为O(n),然后只返回 20 条给客户端,前面 10000 条记录都将被抛弃。如果是使用了二级索引,这种场景性能损失会加剧,因为对于前10000个不需要的数据,MySQL每次也要回表去查找,这就导致了10000次随机IO。
我能想到这两种优化方式:
可以在业务上改进,将"第几页"改成"下一页”,先记录上一页的最后一条记录的id,然后下次就直接从该记录的位置开始扫描,这样就避免 MySQL 扫描大量不需要的行然后再抛弃掉的问题。
如果要遵循第几页的方案,可以通过覆盖索引+子查询方式改进。子查询语句主要查询分页数据对应的数据库唯一id值,因为主键在辅助索引上就有,所以子查询可以不用回表。然后主查询再根据子查询返回的 id,进行索引查询完整的数据行。
推荐学习
110. 如果 SQL 和索引都没问题,查询还是很慢怎么办?
分析
需要发散下思维,往架构优化方向思考。
回答
分批查询:针对一个大查询可以拆分多个小查询,每个小查询只返回一部分查询数据。
增加缓存:针对频繁读取的热点数据,我们可以放到 Redis 缓存,避免每次都要请求 MySQL。
分表:如果表的数据量很大,比如表数据千万级别了,这时候可以考虑分表了,通过减少每次查询数据总量来解决数据查询缓慢的问题。
主从复制:针对读多写少的场景,我们可以搭建 MySQL 主从模式来分摊读请求的流量。
分库:针对写多读少的场景,单库的性能无法抗住高并发流量, 就需要进行分库,把并发请求分散到多个实例中去。
推荐学习
数据库选型
111. SQL 和 NoSQL 数据库有什么区别?
分析
| 区别维度 | SQL数据库 | NoSQL 数据库 |
|---|---|---|
| 数据模型 | 关系型,有固定行和列的表格 | 文档型、键值对性、JSON 文档型、列不固定的列式存储、图类型 |
| 发展历史 | 开发于 20 世纪70 年代 | 产生于 2000年左右 |
| 典型代表 | Oracle、MySQL、Microsoft SQL Server.PostgreSQL | - 文档型:MongoDB and CouchDB - 键值对:Redis and DynamoDB - 列式:Cassandra and HBase - 图数据库:Neo4j |
| Schemas(模式) | 数据表格 | 灵活结构 |
| 扩展性 | 垂直扩展,需要升级机器硬件配置 | 水平扩展 |
| 事务支持 | ACID | 不支持 ACID |
| 应用访问模式 | 数据中间层、ORM | 直接映射为应用语言数据结构 |
- SQL
SQL(Structured Query Language) 是关系型数据库中的语言标准。对于SQL和关系型数据库来说,最重要的就是Strucutred和RelationShip,所以我自己认为SQL就像是一个严谨的社牛!对于关系型数据库,它要求你存储的数据都要符合格式标准(Table Schema),所有数据都必须按照它定义好的标准来,这是它严谨性方面的直接体现。另一方面,它又鼓励各个数据之间可以随意的建立连接,互相认识交朋友,建立起庞大的关系网络,这是他社牛属性的体现。
假设我们想要在关系型数据库中存储一个员工的基本信息。

在员工信息的存储中,我们构建了两个Table,分别用来存储员工信息和部门信息。
可以发现,**两章表中都包含了主键和数据类型,这是我们在构建Table时必须指定的内容。**一旦指定了数据类型,这就意味着你想要存储的数据必须符合所规定的类型,如果你想新加入一个员工的信息,但是这个员工的性别却是一个数字,比如‘3’,这种操作是绝对不允许的,因为SQL已经规定了性别必须是varchar类型。
在关系型数据库中,**我们还可以用外键把两个Table联系起来, 用来表示两个数据之间的联系,这也是关系型数据库中独有的特点。**关系型数据库中的表关系包括一对一,一对多,多对一,多对多,这些关系构建了不同数据之间的复杂关系网络。
- NoSQL
NoSQL(Not only Structured Query Language) 代表了非关系型数据库的机制。对于NoSQL来说,他像是一个自由的艺术家。自由体现在他对数据的存储格式没有约束,你可以在NoSQL中存储各式各样的数据,比如文档、图等等。
假设我们同样想在NoSQL中存储员工信息,并且选择文档数据库来进行存储,那么它会是什么形式呢?

可以看到,员工信息由两个集合组成,分别是个人集合和部门信息集合,这里的集合(Collection)就相当于SQL中的Table概念。但是我们可以发现,集合内存储的东西是类似JSON格式的文本,并且第一个员工李峰存储了性别信息,但是第二个员工却没有存储性别信息。这就是NoSQL的自由性,它不会对数据的格式和存储的内容做出严格的约束。
我们这里使用了文档数据库来存储JSON信息,不同于SQL,NoSQL不仅仅可以存储文档,还可以存储各种各样的数据,大体上NoSQL可以分为四类:
文档数据库,代表数据库是MongoDB和CouchDB
Key-Value数据库:代表数据库是Redis和DynamoDB
Graph图数据库:代表数据库是Neo4j
Column列数据库,代表数据库是Cassandra 和 HBase

SQL和NoSQL的对比:
查询对比:
SQL支持Table之间建立多种关系和固定的Table结构,因此**SQL可以支持各种复杂多样的嵌套查询。**比如在之前的员工信息表中,由于外键的存在,我们可以快速的查找到所有属于研发部门的员工。
NoSQL由于数据格式不统一并且没有统一个查询语言,因此很难实现复杂多样的嵌套查询。
扩展性对比:
SQL数据库在增加capacity的时候,一般会选择垂直扩展,也就是增加的单台服务器的性能。不过,现在的SQL数据库也早已通过分shard来支持了水平扩展,通过对Table进行分区并部署在不同的server上来增加SQL数据库的capacity。但是,由于SQL中的Table之间包含复杂的联系,因此对Table进行分区时通常需要考虑很多因素,比如需要解决跨服务器 JOIN,分布式事务等问题。
NoSQL数据库在增加capacity的时候,通常会选择水平扩展,也就是通过增加服务器的数量来提升数据库的capacity。由于NoSQL中的数据相当于都是独立的个体,数据之间的联系很少,因此在进行水平扩展的时候更方便,不用考虑复杂的数据关联问题,比如 Redis 自带主从复制模式、哨兵模式、切片集群模式。
事务对比:
SQL数据库具有ACID的特性,强调事务和数据的安全可靠性。
NoSQL则不考虑ACID特性,它更注重的是处理任务的性能和吞吐量,不会保证数据的可靠性。
适用场景对比:
SQL数据库更适用于OLTP类的基于事务的处理分析任务,这类任务往往需要对数据进行大量的Update、Insert和Delete等操作,并且经常需要对多个数据进行联合查询。SQL数据库的ACID特性提供了对事务执行的强大支持,因此SQL最为适合处理基于事务的在线分析任务。
NoSQL数据库则没有ACID特性的保证,同时也没有固定的数据格式,这种近乎自由的数据存储给NoSQL带来了更高的性能。因此NoSQL更时候对访问性能有要求同时又不要求数据可靠性的场景,比如搜索,缓存等,都非常时候使用NoSQL来实现。
回答
SQL 和 NoSQL 数据库区别主要有:
数据存储的区别:SQL 数据库是关系型数据库,主要代表的数据库是MySQL,数据是严格按照二维表格的形式存储的,表与表之间可以建立连接来查询数据。NoSQL是非关系型数据库,主要代表的数据库是Redis、MongoDB等,数据是灵活存储的,对数据的存储格式没有约束,可以在NoSQL中存储各式各样的数据,比如Json文档、图、键值对等等。
事务的区别:SQL数据库具备ACID四大特性的事务,而 NoSQL数据库是不具备满足ACID特性的事务,因为 NoSQL 数据库都是通过牺牲了 ACID 特性来获取更高性能的
扩展性的区别:SQL 数据库的数据之间存在关联性,一般会选择垂直扩展,也就是增加服务器的性能,虽然也可以通过分库分表的方式实现水平扩展,但是水平扩展之后,会带来很多新的问题,比如需要解决跨库跨表的查询、分布式事务、全局唯一ID等问题。NoSQL 数据库的数据相当于独立的个人,数据之间的联系很少,因此在进行水平扩展的时候更方便,不用考虑复杂的数据关联问题。
我觉得 NoSQL 并非是为了取代 SQL,相反它们是需要相互结合的,SQL数据库提供ACID事务,NoSQL数据库提供更好的扩展性和性能,所以NoSQL一般是作为传统关系型数据库的补充而存在,弥补关系型数据库在性能、扩展性和某些场景下的不足。
推荐学习
NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?
112. MySQL 和 mongodb 之间怎么选型?
分析
mysql 是关系型数据库
优势:
由二维表结构来逻辑表达,相对网状、层次等其他模型更加容易被理解。严格遵循数据格式与长度规范,数据以行为单位,一行数据表示一个实体信息,每一行数据的属性都是相同的。
操作方便,通用的 SQL 语言使得操作关系型数据库非常方便,支持 join 等复杂查询,Sql + 二维关系是关系型数据库最无可比拟的优点,这种易用性非常贴近开发者。
支持 ACID 特性,可以维护数据之间的一致性,这是使用关系数据库非常重要的一个理由,例如同银行转账,张三转给李四 100 块钱,张三扣 100 元,李四加 100 元,而且必须同时成功或者同时失败,否则就会造成用户的资损。
劣势:
为维护数据一致性付出的代价大,数据一致性是关系型数据库的核心,但是同样为了维护数据一致性的代价也是非常大的。我们都知道 SQL 标准为事务定义了不同的隔离级别,从低到高依次是读未提交、读已提交、可重复度、串行化,事务隔离级别越低,可能出现的并发异常越多,但是通常而言能提供的并发能力越强。那么为了保证事务一致性,数据库就需要提供并发控制与故障恢复两种技术,前者用于减少并发异常,后者可以在系统异常的时候保证事务与数据库状态不会被破坏。对于并发控制,其核心思想就是加锁,无论是乐观锁还是悲观锁,只要提供的隔离级别越高,那么读写性能必然越差。
表结构扩展不方便,由于数据库存储的是结构化数据,因此表结构 schema 是固定的,扩展不方便,如果需要修改表结构,需要执行 DDL(data definition language)语句修改,修改期间会导致锁表,部分服务不可用。
水平扩展后带来的种种问题难处理,随着业务规模扩大,一种方式是对数据库做分库,做了分库之后,数据迁移(1 个库的数据按照一定规则打到 2 个库中)、跨库 join、分布式事务处理都是需要考虑的问题,尤其是分布式事务处理,业界当前都没有特别好的解决方案。
mongodb 是文档型 NoSQL 数据库,以 JSON 或者 XML 格式存储数据,因此文档型 NoSql 是没有 Schema 的,由于没有 Schema 的特性,我们可以随意地存储与读取数据,因此文档型 NoSql 的出现是解决关系型数据库表结构扩展不方便的问题的。
优点:
- 没有预定义的字段,扩展字段容易
- 相较于关系型数据库,读写性能优越,命中二级索引的查询不会比关系型数据库慢,对于非索引字段的查询则是全面胜出
缺点:
不支持事务操作
多表之间的关联查询不支持(虽然有嵌入文档的方式),join 查询还是需要多次操作
回答
MySQL 是关系型数据库,支持 ACID 特性的事务,而 MongoDB 是NoSQL类型的数据库,不支持事务,如果业务需要通过事务保证数据一致性的话,是需要选择 MySQL的。
如果业务上没有强一致性的要求,那么可以根据下面这两种场景来考虑:
MongoDB 是灵活的文档模型。也就是说,如果我预计我的数据可以被一个稳定的模型来描述,那么我会倾向于使用 MySQL 等关系型数据库。而一旦我认为我的数据模型会经常变动,比如说我很难预料到用户会输入什么数据,这种情况下我就更加倾向于使用 MongoDB。
MongoDB 属于 NoSQL,更容易进行横向扩展。虽然关系型数据库也可以通过分库分表来达成横向扩展的目标,但是比 MongoDB 要困难很多,后期运维也要复杂很多,而这一切在 MongoDB 里面都是自动的,运维成本低。
推荐学习
高可用
113. MySQL 主从复制的过程是怎么样?
分析

主从复制过程梳理成 3 个阶段:
写入 binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
同步 binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
回放 binlog:回放 binlog,并更新存储引擎中的数据。
回答
主从复制主要有3个阶段:
主库修改数据后,会写入 binlog 日志,从库连接到主库之后,主库会创建一个 log dump 线程,用于发送 bin log 的内容。
从库会创建一个专门的 I/O 线程 来连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
接着从库还会创建一个用于回放 binlog 的 SQL 线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
推荐学习
114. MySQL 提供了几种复制模式?默认的复制模式是什么?
分析
说出 3 种同步模式:同步复制、半同步复制、异步复制,然后说一下各个模式的适合场景。
回答
主要有三种同步复制、半同步复制、异步复制,MySQL 默认的复制模型是异步复制。
同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式是性能最差的复制模式,但是能保证数据的安全性,如果对数据安全性比较高的业务,可以考虑采用同步复制的模式。
异步复制:MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果,这种模式性能是最高的,但是一旦主库宕机,数据就会发生丢失。
半同步复制:介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
推荐学习
MySQL 主从复制 —— 全同步复制、异步复制、半同步复制
115. MySQL 主从复制的数据延迟怎么解决?
分析
主从复制延迟可能导致读写分离时由于数据延迟导致新请求读到从库中的旧数据
考察解决主从复制延迟问题的方案设计
回答
使用缓存解决**:**可以在写入数据主库的同时,把数据写到 Redis 缓存里,这样其他线程再获取数据时会优先查询缓存,也可以保证数据的一致性。不过这种方式会带来缓存和数据库的一致性问题。
直接查询主库**:**对于数据延迟敏感的业务,可以强制读主库。但是我们要提前明确查询的数据量不大,不然会出现主库写请求锁行,影响读请求的执行,最终对主库造成比较大的压力。
推荐学习
多个从节点出现数据不一致的原因及解决方法
即使主从复制正常,多个从库之间也可能因为各种原因出现数据不一致。常见原因包括:
复制中断或错误跳过
- SQL线程因执行失败(如主键冲突、表结构不同)而停止,管理员手动跳过事务导致部分从库跳过的事件数量不一致。
主库binlog丢失或损坏
- 主库异常重启,binlog未完全写入;或从库请求的binlog已被主库清理。
主从配置差异
从库参数不同(如
sql_mode、character_set),导致相同binlog执行结果不同。使用了非事务性引擎(如MyISAM)且发生主库宕机,导致主从不一致。
人为操作
- 直接在从库上写入数据(未设置
read_only),破坏了数据一致性。
- 直接在从库上写入数据(未设置
网络抖动与半同步退化
- 半同步复制超时后降级为异步,在此期间主库宕机可能导致部分从库缺失事务。
解决多个从节点之间数据不一致的方法
当发现多个从库之间数据不一致(或从库与主库不一致)时,通常需要先定位差异,再修复,最后预防。
- 检测不一致
推荐使用 Percona Toolkit 中的 pt-table-checksum 工具。它通过在主库执行校验SQL,计算每张表的数据校验和,然后与各从库比对,找出差异的行或块。
pt-table-checksum --host=主库IP --user=xxx --password=xxx --databases=db1
该工具会在主库创建percona.checksums表,并逐表分块计算校验和,自动对比所有从库。执行时对业务影响较小,但建议在低峰期运行。
- 修复不一致
根据不一致的规模和场景,选择合适的修复方式:
- 使用
pt-table-sync在线修复 Percona Toolkit 的另一工具pt-table-sync可以根据主库或指定的基准库修复从库。
将从库1修复到与主库一致pt-table-sync --execute --sync-to-master h=从库1IP,u=xxx,p=xxx
它能生成修复语句(INSERT/UPDATE/DELETE)并执行,但执行期间可能产生锁,建议谨慎评估。
支持仅输出SQL不执行(
--print),人工审核后手动执行。重建从库(最安全但耗时) 若不一致范围较大或业务允许停机,最可靠的方法是重新克隆一个一致的从库:
使用
mysqldump、xtrabackup或clone插件从主库(或某个可信从库)备份数据。将备份还原到需要修复的从库。
重新建立复制(
CHANGE MASTER TO)。
- 这种方法能彻底消除所有差异,但需要较多时间和资源。
跳过/注入事务(仅限少数已知差异) 若差异由个别SQL线程报错引起,且已知该事务可以跳过(例如重复插入),可以动态跳过:
-- 查看复制状态SHOW SLAVE STATUS\G*-- 跳过当前一个错误事务(需先停止复制)*STOP SLAVE;SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;START SLAVE;
- 注意:跳过可能导致该事务在所有从库上的缺失不一致,不建议用于修复多个从库间的差异。
- 统一所有从库的数据
如果有多个从库之间不一致,但主库是正确的,通常做法是将主库作为基准,使用 pt-table-sync 分别修复每一个从库,确保所有从库与主库一致。
如果主库也存在问题(比如主库损坏),则需要从某个数据正确的从库提升为主库,然后再重建其他从库。
- 预防措施
为了避免未来再次出现多从库不一致,建议采取以下措施:
启用GTID:GTID能保证每个事务的全局唯一性,方便故障切换和复制位置管理,避免因
MASTER_LOG_FILE/MASTER_LOG_POS错误导致的差异。使用基于行的复制(binlog_format=ROW):避免STATEMENT模式下的不确定性。
开启半同步复制(至少一个从库):减少主库宕机时数据丢失的概率。
设置从库为只读(read_only=1):禁止在从库上直接写入。
监控复制延迟和状态:使用监控系统(如Prometheus+MySQL Exporter)及时发现复制中断或错误。
定期校验一致性:通过
pt-table-checksum定期巡检,防患于未然。统一环境配置:确保所有从库的
sql_mode、character_set_server、innodb_flush_log_at_trx_commit等参数与主库一致。
关键版本特性
GTID(全局事务标识符):自MySQL 5.6起支持,每个事务有唯一标识,简化了故障切换和复制位置管理。
ROW格式:推荐使用基于行的binlog格式,可避免STATEMENT格式下某些函数(如
UUID()、NOW())导致的主从不一致。
总结
主从复制原理:主库binlog → Binlog Dump → 从库I/O线程 → relay log → SQL线程重放。
数据不一致原因:网络、配置、人为、语句不确定性、复制中断等。
解决方法:先用
pt-table-checksum检测,再用pt-table-sync或重建从库修复,最后通过GTID、ROW格式、半同步、只读等机制预防。
在实际生产环境中,一旦发现多个从库数据不一致,应优先评估业务影响,尽量在业务低峰期进行修复,并确保有完整的备份作为回退方案
116. MySQL 主从架构中,读写分离怎么实现?
分析
一种简单的做法是:提前把所有数据源配置在工程中,每个数据源对应一个主库或者从库,然后改造代码,在代码逻辑中进行判断,将 SQL 语句发送给某一个指定的数据源来处理。这个方案简单易实现,但 SQL 路由规则侵入代码逻辑,在复杂的工程中不利于代码的维护。
另一个做法是:独立部署的代理中间件,如 MyCat,这一类中间件部署在独立的服务器上,一般使用标准的 MySQL 通信协议,可以代理多个数据库。该方案的优点是隔离底层数据库与上层应用的访问复杂度,比较适合有独立运维团队的公司选型;缺陷是所有的 SQL 语句都要跨两次网络传输,有一定的性能损耗,再就是运维中间件是一个专业且复杂的工作,需要一定的技术沉淀。
回答
可以独立部署的代理中间件 MyCat 来实现读写分离。
推荐学习
117. MySQL 主库挂了怎么办?
分析
MySQL 没有像 Redis 集群有哨兵模式,可以自动将从库升级为主库。
MySQL 的“发现主服务器宕机”“处理故障转移逻辑”要由数据库高可用套件完成,常用的是 MHA,它是一款开源的 MySQL 高可用程序,它由两大组件所组成,MHA Manger 和 MHA Node。
MHA Manager 通常部署在一台服务器上,用来判断多个 MySQL 高可用组是否可用。当发现有主服务器发生宕机,就发起 failover 操作。MHA Manger 可以看作是 failover 的总控服务器。
而 MHA Node 部署在每台 MySQL 服务器上,MHA Manager 通过执行 Node 节点的脚本完成failover 切换操作。
MySQL 的高可用套件用于负责数据库的 Failover 操作,也就是当数据库发生宕机时,MySQL 可以剔除原有主机,选出新的主机,然后对外提供服务,保证业务的连续性。
回答
MySQL 主从复制没有实现发现主服务器宕机和处理故障迁移的功能,要实现自动主从故障迁移的话,我简单了解过,可以使用开源的 MySQL 高可用套件 MHA,MHA 可以在主数据库发生宕机时,可以剔除原有主机,选出新的主机,然后对外提供服务,保证业务的连续性。
推荐学习
118. 什么是分库分表?什么时候需要分表?什么时候需要分库?
分析
分库分表使用的场景不一样:
分表是因为数据量比较大,导致事务执行缓慢;
分库是因为单库的性能无法满足要求。
回答
分库分表的意思把原本存储于单个数据库上的数据拆分到多个数据库,把原来存储在单张数据表的数据拆分到多张数据表中,实现数据切分。
分库分表使用的场景不一样:
当单张数据表的数据量太大的时候,经验值是 500W以上的数据量,就会影响了事务的执行效率,这时候就要考虑分表了,通过减少每次查询数据总量来解决数据查询缓慢的问题。
当单台 MySQL 扛不住高并发流量的时候,就要考虑分库了,把并发请求分散到多台 MySQL 实例中。
推荐学习
119. 分库分表后,会产生什么问题?怎么解决?
分析
分布式事务问题,解决方式:
2PC两阶段、3PC三阶段提交协议(强一致性,性能差,用的少)
TCC 分段提交、基于本地消息表来实现分布式事务(最终一致性,性能较好,用的多)
资料补充学习:海量并发场景下,如何回答分布式事务一致性问题?、 分布式事务有哪些解决方案?
全局 ID 唯一性问题,解决方式:
雪花算法生成ID、或者美团leaf算法生成唯一主键ID
跨库跨表关联查询问题,解决方式:
冗余额外字段避免跨库关联,或者交给数据库分库分表中间件来实现
将数据全量存储到ES中去,通过ES进行查询(如果简历上写用过ES,可能会问 mysql 的数据是怎么同步到 es 的:MySQL数据同步ES的四种方法!你能想到几种?)
跨库跨表的排序问题,解决方式:
业务代码或者数据库分库分表中间件分别查询每个子表中的数据,然后汇总进行排序。
将数据全量存储到ES中去,通过ES进行查询(如果简历上写用过ES,可能会问 mysql 的数据是怎么同步到 es 的:MySQL数据同步ES的四种方法!你能想到几种?)
跨库跨表 COUNT 查询的问题,解决方式:
将计数的数据单独存储在一张表里
将聚合查询的数据同步到 ES 中,交给ES进行查询(如果简历上写用过ES,可能会问 mysql 的数据是怎么同步到 es 的:MySQL数据同步ES的四种方法!你能想到几种?)
回答
分布式事务问题:对业务进行分库之后,同一个操作会分散到多个数据库中,涉及跨库执行 SQL 语句,也就出现了分布式事务问题。解决方式,如果对一致性要求比较高的业务,比如金融类的业务,可以使用分布式事务中间件,实现 TCC 事务模型;互联网的业务通常对一致性要求低,会使用基于本地消息表来实现分布式事务,达到最终一致性的效果,这个分布式事务的方案性能会好一些。
全局 ID 唯一性问题:在单库单表时,业务 ID 可以依赖数据库的自增主键实现,进行分库分表之后,如果还是用数据库的自增主键,可能会导致主键重复。我们可以通过雪花算法或者美团leaf算法生成唯一主键ID。
跨库跨表关联查询问题:分库分表后,跨库和跨表的查询操作实现起来会比较复杂,我们可以通过冗余额外字段避免跨库关联,或者交给数据库分库分表中间件来实现,也可以将数据全量存储到ES中去,通过ES进行查询。
跨库跨表的排序问题:分库分表以后,数据分散存储到不同的数据库和表中,如果需要对数据列表进行排序时,就变得异常复杂,我们可以通过业务代码或者数据库分库分表中间件分别查询每个子表中的数据,然后汇总进行排序,也可以将数据全量存储到ES中去,通过ES进行查询。
跨库跨表 COUNT 查询的问题:分库分表以后,数据分散存储到不同的数据库和表中,如果需要对表进行 COUNT 查询就会很复杂,我们可以将计数的数据单独存储在一张表里,或者将聚合查询的数据同步到 ES 中,交给ES进行查询。
推荐学习