Skip to content

为什么在Mysql中使用%LIKE或%LIKE%会进行全表扫描

在 MySQL 中,使用前置通配符(例如 LIKE '%abc%')时,由于 B-Tree 索引只能对字符串开头进行快速定位,数据库无法利用索引直接跳转到匹配行,因而只能对整张表的每一行逐一检查,导致全表扫描(type=ALL)下面将从原理、表现及优化方案三个方面进行详解。

原因分析

B-Tree 索引的工作原理

MySQL 的 InnoDB 和 MyISAM 存储引擎使用 B-Tree 索引结构,它能够对固定顺序的键值进行范围查找(range scan)或二分查找(binary search),从而在海量数据中快速定位到符合条件的行 然而,一旦查询模式以通配符 % 开头,查询条件变成了“在任意位置搜索子串”,B-Tree 索引就失去了“可预测的范围起点”,无法执行范围查找,只能退化为全表扫描

EXPLAIN 中的 ALL 类型

在执行 EXPLAIN SELECT ... WHERE col LIKE '%abc%' 时,type 列会显示为 ALL,意味着 MySQL 必须扫描表中的每一行来判断是否匹配条件,这就是常说的全表扫描

表现特点

  • I/O 和 CPU 占用激增 全表扫描会将大量数据读入 Buffer Pool,导致磁盘 I/O 激增,同时 CPU 也需要处理更多的行匹配判断,于是系统负载急剧上升
  • 行访问数大 原本可能只需要访问少量行,但因无法利用索引,MySQL 会读取并检查表中的所有行,造成 rows 列数字飙升

优化与替代方案

1. 避免前置通配符

尽量将通配符放在字符串末尾,例如:

sql
WHERE col LIKE 'abc%';

此时 MySQL 可利用索引进行范围查找,执行计划中 type 将变为 rangeref,并使用索引

2. 使用全文索引(Full-Text Index)

对于复杂的、需要中间匹配的自然语言查询,可考虑 InnoDB 自 MySQL 5.6 起支持的全文索引,它通过倒排索引(inverted index)快速定位包含特定词项的行,效率远超 %LIKE% 全表扫描

3. 建立辅助列并生成索引

若只需搜索字符串的一部分(如域名、后缀等),可通过生成列(Generated Column)提取特定子串并对其建索引。例如:

sql
ALTER TABLE users 
  ADD COLUMN domain VARCHAR(255) AS (SUBSTRING_INDEX(email,'@',-1)),
  ADD INDEX idx_domain (domain);

SELECT * 
FROM users 
WHERE domain = 'example.com';

此时查询可走该索引,无需全表扫描

4. 借助外部搜索引擎

对于需要更强大模糊匹配和排序功能的场景,可将全文检索交给 Elasticsearch、Sphinx 等专业搜索引擎来处理,再将结果回写或关联至 MySQL。

总结

  • 根本原因:前置 % 破坏了 B-Tree 索引的有序性,使得 MySQL 无法通过索引范围查找,只能全表扫描
  • 优化思路:尽量避免前置通配符、使用全文索引或生成列索引,必要时引入专用搜索引擎,从而避免高开销的全表扫描。

通过上述方法,你可以有效减少或避免 MySQL 在 %LIKE% 查询时的全表扫描,显著提升查询性能。

1. MySQL 中的数据排序是怎么实现的?

  • 利用索引排序:如果 ORDER BY 的字段有合适的索引,MySQL 会直接按索引顺序读取数据,这种方式性能最好,不需要额外的排序过程。
  • FileSort 排序:当无法使用索引时,MySQL 会采用 FileSort 算法来排序数据。FileSort 可能在内存中完成(数据没有超过 sort_buffer),也可能写入磁盘数据(超过 sort_buffer),效率比索引排序低。

实际开发中,为了提升排序效率,我们通常会给常用排序字段加索引,并通过 EXPLAIN 来检查查询是否使用了索引排序,避免触发 FileSort。

2. MySQL 的 Change Buffer 是什么?它有什么作用?

Change Buffer 是 InnoDB 存储引擎中的一个机制,用来缓存对二级索引页的变更操作(如插入、更新、删除),当这些索引页还不在内存(Buffer Pool)时使用。

它的作用是:

  1. 减少随机磁盘 I/O:把对索引页的修改先缓存在 Change Buffer 中,等将来读取这个索引页时一起合并,从而避免频繁地把磁盘页读入内存。
  2. 提升写入性能:尤其在插入或更新大量数据时,对二级索引的写入压力大,Change Buffer 能显著降低 I/O 开销。

需要注意的是,Change Buffer 只作用于二级索引对主键(聚簇索引)不适用。而且可以通过 innodb_change_buffering 参数来控制它的行为,比如只缓存插入,或全部缓存。

3. 一条 SQL 语句在 MySQL 中的执行过程

一条 SQL 语句在 MySQL 中的执行过程,主要分为以下几个步骤:

  1. 连接层(连接器)
    • 客户端发送 SQL 请求,先由 MySQL 的连接层处理,进行用户认证权限校验,判断是否有权限执行这个 SQL。
  1. 查询层(SQL 层)
    • 语法解析(解析器):SQL 语句会经过解析器,生成语法树,检查 SQL 是否书写正确。
    • 查询优化(优化器):优化器根据语法树,生成执行计划,决定使用哪个索引、采用什么连接顺序(如 Nested Loop、Hash Join 等)。
    • 执行器(执行器):根据执行计划,调用存储引擎接口获取数据。
  1. 存储引擎层
    • MySQL 会把读取数据的请求交给对应的存储引擎(如 InnoDB)。
    • InnoDB 先在Buffer Pool(内存)中查找数据页,如果没有命中,会从磁盘读取。
    • 过程中可能会用到Change Buffer(优化二级索引写入)和自适应哈希索引
  1. 事务日志
    • 如果是写操作(如 INSERTUPDATE),InnoDB 会先将操作记录到Redo Log(物理日志)和Undo Log(逻辑日志),确保事务的原子性持久性
    • Redo Log 用于崩溃恢复,Undo Log 用于事务回滚和实现MVCC(多版本并发控制)。
  1. 返回结果
    • 存储引擎把数据返回给 SQL 执行层,执行器对数据进行排序、聚合、过滤等操作。
    • 最终结果通过连接层返回给客户端。

✅(可选加分版:补充高级细节)

  • 如果是 SELECT,还会触发 查询缓存(MySQL 8.0 已废弃)。
  • 执行计划可以通过 EXPLAIN 查看,帮助我们优化查询性能。
  • MySQL 采用双写缓冲区(Doublewrite Buffer)来保证数据页写入磁盘时的一致性。

4. MySQL 的存储引擎有哪些?它们之间有什么区别?

MySQL 常见的存储引擎主要有:

  1. InnoDB
    • 默认引擎,支持事务行级锁MVCC
    • 支持外键,数据和索引存储在一个表空间中。
    • 适用于大多数OLTP场景(高并发读写)。
  1. MyISAM
    • 早期默认引擎,不支持事务,只有表级锁
    • 读取速度快,占用空间小,但写入并发能力差。
    • 适合读多写少的应用场景,如数据仓库、报表系统。
  1. Memory
    • 数据存储在内存中,速度极快。
    • 断电数据丢失,适合临时表缓存场景。
  1. Archive
    • 专门用于归档,只支持插入查询,不支持更新和删除。
    • 适用于存储历史数据、审计日志等。
  1. NDB (NDBCluster):
    • 支持高可用性和数据分布,适合大规模分布式应用
    • 提供行级锁和自动分区。

实际开发中,InnoDB 是主流选择,因为它支持事务和高并发。


✅(加分版:可以补一句)

"MySQL 8.0 之后,官方已经默认只推荐使用 InnoDB,其他引擎如 MyISAM 已不再适合作为主力引擎。"


🟢 记忆口诀(帮你速记)

InnoDB事务强,MyISAM读得爽,Memory速度狂,Archive存得长。

5. MySQL 的索引类型有哪些?

简要回答(重要)

索引类型按照不同的方向进行划分

划分方向类型
数据结构B+树索引、哈希索引、倒排索引(Full text)、R-树索引(多维空间树)、位图索引(Bitmap)
物理存储聚簇索引、非聚簇索引
字段特性主键索引、唯一索引、普通索引(二级索引、辅助索引)、前缀索引
字段个数单列索引、联合索引

补充回答

  • B+树索引:通过树形结构存储数据,适用于范围查询和精确查询,支持有效数据的快速查找、排序和聚合操作,MySQL的默认索引类型,常用于InnoDB和MyISAM。
  • 哈希索引:基于哈希表的结构,适用与等值查询,但不支持范围查询,查询速度很快,同时不存储数据的顺序,常用于Memory引擎
  • 倒排索引:它将文档集合中的每个唯一单词(词条)映射到包含该单词的所有文档列表,倒排索引通过将单词作为索引的键,文档列表作为值,实现了从单词到文档的快速查找,而全文索引通常依赖倒排索引这种数据结构来实现
  • R-树索引:为了多维空间数据(地理坐标)设计,适用于空间查询(计算最近距离,区域查询等)。常用于存储和查询地理信息系统(GIS)中的空间数据
  • 聚簇索引:索引的叶子节点存储完整数据记录
  • 非聚簇记录:索引的叶子节点存储了主键值和对应的索引字段
  • 普通索引:一般指非主键索引且非唯一索引(二级索引、辅助索引 )
  • 主键索引:唯一且不能为NULL,每个表只能有一个,InnoDB中主键索引是聚簇索引结构实现的
  • 联合索引:多个列组成的索引,适用于多列的查询条件,也可以通过联合索引实现覆盖索引和索引下推技术
  • 唯一索引:唯一,允许为null,但一个列中可以有多个null,可以有效防止重复数据的插入
  • 全文索引:准确来说是一种索引技术,通常依赖倒排索引这种数据结构实现,针对文本数据的一种索引机制,能让用户对文本内容进行全面检索
  • 空间索引:具体来说是一个宽泛的概念,旨在为空间数据提供一种高效的组织和检索方式,R-树索引就是空间索引的具体实现方式。
  • 位图索引:一种在数据库管理中适用的特殊索引结构,特别适用于处理低基数(列中不同值的数量相对较少)的数据列(例如男女性别)。

扩展回答:

  • Q:InnoDB为什么使用B+树实现索引?
  • A:1. B+树是一棵平衡树,每个叶子节点到根节点的路径长度相同查询效率高 2. 所有关键字都在叶子节点上,因此范围查询时只需要遍历一遍叶子节点即可。3.叶子节点都按照关键字大小顺序存放,因此可以快速的根据关键字大小进行排序。4. 非叶子节点不存储实际数据,因此可以存储更多的索引数据 5. 非叶子节点是用指针链接叶子节点,因此可以快速的支持范围查询倒序查询。6. 叶子节点之间通过双向链表链接,方便进行范围查询。所以可以总结得出,使用B+树有以下有几点,支持范围查询支持排序可以存储更多的索引数据、因为叶子节点大小固定,节点分裂和合并时,IO操作少,同时因为大小固定,还有利于磁盘预读,因为非叶子节点只存储指向子节点的指针,而不存储数据,所以可以缓存更多的索引数据,有利于缓存
  • Q:B+树索引和Hash索引有什么区别?
  • A:1. 因为B+树索引将索引列的值按照大小排序存储,所以更适合于范围查询,而哈希索引是基于Hash表的结构,所以哈希索引更适合等值查询,但不适合范围查询和排序操作。2 . 如果B+树索引插入数据和删除数据时需要调整索引结构,可能涉及到页分裂和页合并等操作(无序插入),维护成本较高,而哈希索引在插入和删除数据只需要计算哈希值并插入或者删除相应的记录。 3. B+树索引在磁盘上是有序存储的,而哈希索引是无序存储的
  • Q:唯一索引和主键索引的区别?
  • A:两者都具有唯一性,但是主键索引不能为null,唯一索引可以,主键索引每表只能有一个,唯一索引可以创建多个,在innoDB中,主键索引就是聚簇索引,但唯一索引通常是非聚簇索引(除了特殊情况,就是在没有创建主键索引的情况下,MySQL会默认选择一个唯一的非空索引作为聚簇索引),同时主键索引一定不需要回表,但是唯一索引查询通常是需要回表的,主键可以被其他表引用为外键,而唯一索引不可以。
  • Q:MySQL如何保证唯一索引的唯一性?
  • A:在支持事务的存储引擎中(例如InnoDB)中,事务机制和锁定协议帮助维护索引的唯一性,当一个事务正在修改索引列时,其他事务对相同键值的修改会被适当的阻塞,直到第一个事务提交或回滚,确保了数据的一致性和唯一性,并且在实际的写入数据到磁盘之前,MySQL也会执行约束检查,确保不会违反唯一性约束。相应的因为唯一索引保证了指定列的值唯一,会让唯一性索引查询比非唯一性查询根块,因为能够快速的匹配到唯一的记录,但是也是因为要保证索引列的唯一性,因此在插入的时候需要检查是否存在相同的索引值,会对插入性能产生一定的影响。

6. MySQL InnoDB 引擎中的聚簇索引和非聚簇索引有什么区别?

在 InnoDB 中:

  1. 聚簇索引(Clustered Index)
    • InnoDB 的主键索引就是聚簇索引。
    • 数据和索引存储在一起,叶子节点保存了整行数据
    • 一个表只能有一个聚簇索引(因为数据行只能有一个物理顺序)。
    • 查询主键时,性能最高,不需要回表
  1. 非聚簇索引(Secondary Index)
    • InnoDB 中的普通索引(如索引在 name、age 上)属于非聚簇索引。
    • 叶子节点存储的是主键值,而不是整行数据。
    • 查询时,先通过二级索引找到主键,再去回表到聚簇索引中获取完整数据,这个过程叫“回表查询”。
    • 一个表可以有多个非聚簇索引。

所以,区别在于存储结构查询路径:主键查询快,二级索引查询多了一次“回表”操作。


✅(加分版:可以补一句)

"在索引设计时,这种回表成本是我们选择是否需要覆盖索引(索引包含查询所需全部列)的关键考虑点。"


🟢 记忆口诀

主键聚簇,数据同住;二级索引,主键带路。

7. MySQL 中的回表是什么?

"回表" 是指在使用二级索引(非聚簇索引)作为条件进行查询时,由于二级索引中只存储了索引字段的值和对应的主键值,无法得到其它数据。如果要查询数据行中的其它数据,需要根据主键去聚簇索引查找实际的数据行,这个过程被称为回表

8. MySQL 索引的最左前缀匹配原则是什么?

MySQL 索引的最左前缀匹配原则指的是在使用联合索引时,查询条件必须从索引的最左侧开始匹配。如果一个联合索引包含多个列,查询条件必须包含第一个列的条件,然后是第二个列,以此类推。

底层原理:因为联合索引在 B+ 树中的排列方式遵循“从左到右”的顺序,例如联合索引 (first_name, last_name, age) 会按照 (first_name, last_name, age) 的顺序在 B+ 树中进行排序。

MySQL 在查找时会优先使用 first_name 作为匹配依据,然后依次使用 last_nameage。因此,组合索引能够从左到右依次高效匹配,跳过最左侧字段会导致无法利用该索引。

✅ **「Index Skip Scan」**怎么优化?

MySQL 8.0.12 开始,MySQL 引入了 Index Skip Scan 技术:

它的思路是:

  • 即使你没有给最左列(a)条件,它也可以跳跃式利用索引来扫描!
  • 它会在索引里跳过不同的 a 值,直接扫描 b 列。

可以理解为:

它模拟了 **a** 做枚举扫描,然后在每个 a 下对 b=100 进行索引查找。

所以执行计划里你会看到:

plain
Using index for skip scan

说明:

  • 索引利用上了,虽然你没给最左列 a 加条件
  • 这种扫描比全表扫描更快,尤其当 a 的取值比较少的时候(比如 a 只有 5 个不同值)

✅ 总结一句话(面试答法)

Using index for skip scan 是 MySQL 8.0 的一种优化技术,它允许查询没有命中最左前缀时,仍然可以跳跃式扫描联合索引(mysql自动拼接最左前缀),从而避免全表扫描,提高查询效率。它特别适用于联合索引中第一列取值少、第二列查询频繁的场景。

Skip Scan 的效果取决于最左列(a)取值的基数,如果 a 的取值非常多,Skip Scan 的收益就会降低。 适合最左边索引基数少的场景

9. MySQL 的覆盖索引是什么?

回答重点

MySQL 的覆盖索引(Covering Index)是指二级索引中包含了查询所需的所有字段,从而使查询可以仅通过访问二级索引而不需要访问实际的表数据(主键索引)。

覆盖索引的优点

  • 减少I/O操作:因为查询可以直接从索引中获取所有需要的数据,避免了访问实际表的数据页,从而减少了I/O操作。
  • 提高查询速度:索引比表数据更紧凑,因此从索引中读取数据比从表中读取要快。
  • 减少内存占用:只需要读取索引页而不是表数据页,可以减少内存占用。

10. MySQL 的索引下推是什么?

面试标准回答:什么是索引下推(ICP)?

索引下推(Index Condition Pushdown, ICP)是 MySQL 从 5.6 开始引入的一种查询优化技术。

它的作用是:在存储引擎层提前利用索引过滤掉不匹配的数据行,使数据量减少,可以减少回表次数,从而提升查询性能。

简单来说:尽可能在索引层完成筛选,只把符合条件的行返回给服务器层。

索引下推特别适合联合索引,并且条件字段要包含在索引列里,这样才能在索引层直接判断。


通俗理解

以前(ICP没开启的时候):

  • MySQL 先扫描索引找到主键(或行指针),
  • 然后每一行都去回表(到主键索引或数据页里)来判断 WHERE 条件是否成立。

这样效率低,因为很多行其实是回表后才发现不符合条件。


现在(ICP开启的时候):

  • MySQL 会先在索引叶子节点就直接判断 WHERE 条件(只要这个字段在索引里)。
  • 符合条件的行才会回表,不符合的直接在索引层就丢掉

👉 所以大幅减少了回表的次数,查询速度更快。


举个具体例子

假如你有这样一个表:

plsql
CREATE TABLE user (
  id INT PRIMARY KEY,
  age INT,
  name VARCHAR(100),
  INDEX idx_age_name (age, name)
);

查询:

plsql
SELECT * FROM user WHERE age = 30 AND name LIKE 'A%';
  • 这个查询,agename 都在索引里。
  • 开启 索引下推(ICP)后,MySQL 会在索引层就筛选掉 name 不以 'A' 开头 的记录,
  • 只有符合 age=30 AND name LIKE 'A%' 的记录才回表取完整行数据。

面试加分总结

ICP 主要适用于 二级索引查询,能有效减少回表操作。

**EXPLAIN** 执行计划中,你会看到:

plsql
Using index condition

这就说明启用了索引下推优化。


🟢 记忆口诀

条件下推索引层,减少回表效率先。


11.

12. 拓展:MySQL 优化这个问题要怎么回答?

这里我直接拿出我压箱底的总结了,如果大家有需要可以自取,然后总结成自己的。

引言

MySQL 优化我觉得主要从三个层面去说,分别是软件层面、硬件层面以及架构层面。

软件层面

首先最直接的、优化效果的就是软件层面,所谓软件层面,就是指 MySQL 自身的优化。

MySQL 升级

最简单的方式就是 MySQL 的升级,从 5.7 到 8.0 ,按照 MySQL 官方的描述,8.0 的性能是 5.7 的 2 倍,这个我当时看了官方文档,在 MySQL 8.0 官方针对 InndDB 存储引擎进行了优化,主要是索引方面,MySQL 8 已经支持对索引进行拆分了,简单来说就是一个大索引可以拆分成多个小索引,这个和 JDK 1.7 版本 CurrentHashMap 1.7 里面分段锁的思想是一致的,其主要目的就是通过将一个大的方式进行分段,插入的时候则分段插入,最大程度的减少锁的冲突,因为在我们插入或者修改数据的时候,其实会对索引进行一个更新操作,拆分完索引之后可以从一定程度上减少锁的粒度。这个主要是官方的说法,但是我也没有进行详细的尝试,当时我在优化的时候,主要就是针对 MySQL 的版本进行了一个升级,所以我觉得对于这个说法还是要以实际压测的数据为主。

建立合适索引

第二个关键的点就是建立合适的索引,因为我们知道,建立索引太少的话,可能效果并不是很明显,建立索引太多了,在维护的时候有需要一定的成本,而且不一定有用,所以从一定程度上来说,索引的好坏决定了查询的快慢。MySQL 的索引底层用的是 B+ 树,平衡二叉树的升级版,并且按照 MySQL 默认页大小 16k,我们假设单条数据 1k 的情况,那么三层的 B+ 树就可以存储 2000W 的数据,然后我们一般情况下 B+ 树的层数维持在 3-4 层左右,也就是说如果我们使用了索引,最多 3-4 次 IO 就可以找到对应的数据。不过索引多了,缺点也明显,因为我们索引多了,我们再插入的时候如果更新了索引字段也同时会去更新索引插入的性能也就不高。

因此,我们在建立索引的时候,可以针对一些经常查询、排序、分组的字段去建立对应的索引,然后在建立索引的时候,可以针对一些区分度比较高的字段去建立索引,比如一张个人信息表,就可以针对身份证号码、手机号这种标识度比较高的字段去建立索引,然后我们当时由于日志这一块,日志记录的 ID 基本是唯一的,所以当时为了加快查询,加上后面分库分表,就直接将日志记录的 ID 作为主键索引,然后就是我们在查询的时候,有时候会针对日志的级别是进行对应的分组,我们也有建立对应的索引,排序这块我们日志优化的时候主要是针对时间段的,然后基本插入都是递增的形式,所以当时就没有针对这块去做对应的优化。另外一个点,就是我们可以针对一些比较长的字段去建立前缀索引,因为索引字段长度过长的话,索引占据的空间就越大,IO 的效率也就越低,比如我之前做过的一个需求,就是针对 UUID 去建立对应的前缀索引,然后当时日志系统优化的时候,其实我们有想过针对日志中的服务来源去做前缀索引,不过后面想着这个没有多大的必要,就没有去实现,而针对 UUID 去建立前缀索引的原因其实很简单,因为 UUID 其实类似于 Git 的版本号,前面几个字符就可以识别一条记录,然后具体取多少位的前缀,这里我记得当时是有一个索引选择性的点,只要保证选择的位数可以使得索引选择性为 1,或者接近为 1 差不多就可以了。

SQL 语句优化

说完索引建立之后,接下来就经典的数据库 SQL 语句优化了,SQL 语句之所以执行慢,目前我遇到的情况,主要有三个方面:

  • SQL 语句没有走索引,走全表扫描
  • 回表次数太多,导致随机 IO 过多
  • 单行数据量较大,导致系统磁盘 IO 太久

主要就是一个点,就是避免写一些全表扫描的 SQL,首先一个方面就是尽量使用字段去查询,不要使用 select * 去进行查询,另外一个方面就是尽可能地避免索引失效的情况。

索引失效情况

(1)首先最常见的就是模糊匹配,即 like 以 % 开头导致索引失效,针对这个情况,如果一定要进行前后的模糊查询匹配的话,可以存 2 个字段,即一个字段正向存储,一个字段方向存储,比如 abcd 和 cdba,或者用倒排索引还有全文检索的方式,然后这里有一个点,就是虽然 MySQL 现在可以全文检索了,但是有个缺点,就是会把最小两个字段作为倒排索引,这个还是比较致命的,所以全文检索的话还是推荐使用 ES 会好一点。

(2)第二个点就是我之前做过测试的,就是在我们 in 一些特别大的数据量的时候,in 可能会导致索引失效,当时我测试的应该是大概 20%~30% 左右的时候,就会导致索引失效了,然后如果没有失效的话,一般会走一个 range 的索引,性能还是可以的。

(3)第三个点就是在条件匹配,即条件查询也就是 where 的时候使用一些函数,比如使用不等于符号,匹配的时候范围查询,查询条件包含 or 并且字段列不包含索引,这个会直接导致索引失效。

(4)第四个点就是我之前经历过的一个关联查询的场景,即关联条件进行了隐式的类型转换,比如 A 表是 int 类型,B 表的关联字段是 varchar 类型,我以前做过的一个真实场景就是因为关联字段数据不一致导致的,当时有一张合同表和一张审批表,合同不一定有审批,所以合同表左关联,审批表去分页查询,这个时候就发现两边差不多只有 5w 条数据的时候,查询 10 条数据却要 6-7s,当时第一时间排查到就是这两张表对应的关联字段不一致,就合同表的 id 是 int 类型,但是审批表存储的那个确实 varchar,然后当时有一个问题,就是审批表还在被其他模块使用,所以没有办法去修改审批表的字段类型,这里我们针对审批表的 SQL 去进行了一个优化,因为我这个场景是左关联的,先把那 10 条数据去查询出来,然后把这个子查询作为临时表去关联审批表,这样就算是全表扫描,优化之后的查询时间就差不多 300ms 就可以查询出来,不过这里最好的优化方式还是去修改字段类型,然后这里当时接触的还有一个点,就是如果表的字符集,比如一张表是 utf8 ,另一张表是 utf8mb4 的时候也可能导致索引失效。

(5)最后一个点就是在我们使用一些 is null 的语句的时候,也会导致索引失效。

SQL 监控

只说 SQL 语句注意的点其实不太够,因为有时候你以为他是走了索引,但是却还是可能没走索引,比如 in 的这个情况,你很难去判断数据量,所以还是得使用一些 SQL 监控的工具来定位哪些 SQL 执行比较慢,有些公司会有 SQL 监控工具,那个除外,我这里说一下普遍用的,就 MySQL 自带的慢查询日志,首先我们可以通过在数据库中配置 show_query_log = 1 来打开慢查询日志,或者配置 log_queries_not_using_index = 1 来监控哪些 SQL 语句没有走索引,然后配置一下慢 SQL 的阈值 long_query_time,默认的时间是 10s,然后一般的话会配置 2~4s,超过这段时间就标记为慢 SQL。

然后我们拿到慢 SQL 之后,一般可以通过 explain 计划排查慢 SQL。查看执行计划,执行计划主要看 type,性能从好到差分别是 system>const>eq_reg_ref>index_merge>range>index>ALL,一般我们对于一条 SQL 语句的要求起码要达到 range 才可以。我们进行优化的时候,也是通过各种方式去优化 type 的级别。我记得当时我在拓尔思实习的时候,我们有一个出库的场景,就数据库里面存了一条条的存储明细,然后出库之后要把对应的某种商品出库的库存状态更新为出库,当我们做批量出库的时候,一下子出库了很多数据,大概 1W 条的时候,写的是一个 in 操作,就是查询这批出库的 ID,然后 update 状态 in select 这些 ID,走的这个索引类型是 range,大概 8s,后来我们集合我们数据自身的情况,结合这个自增主键,查询出 1w 条里面最大的那个值,然后直接更新比这个值小的数据就可以了,然后我 explain 了一下,走的是 index merge,就索引合并,优化之后大概耗时是 3 秒多,提升了很多。

MySQL 回表

回表次数太多,即我们查询的时候索引并没有走所有的字段,这个时候就会多了一些不必要的查询,即随机 IO,从而导致效率不高,这个主要和 MySQL 的索引有关,MySQL 的索引分为聚集索引以及非聚集索引,非聚集索引就是普通索引,如果命中了非聚集索引,就可能走一个二次回表的操作,你查询的数据越多,回表次数也就越多,所以针对这个情况,可以对一些比较固定查询的字段监控你联合索引,这样查询的时候就可以走索引覆盖,避免二次回表,这个也是一种优化方式。

数据库优化

数据库设计

这个合理的数据库设计包含索引、字段、表结构几个方面,字段设计的话最主要就是的就是主键最好选择单调递增的数字,主要有两个方面,一个方面是单调底层可以保证新插入的数据在最后面,避免索引重建,另外一个面就是数字主键查询的时候可以直接比较大小,比字符串的逐字比较性能会好很多。然后一些字段如果是空的可以设置空字符串或者 0,这样查询的时候也可以走索引,另外就是表结构的设计,表结构的设计要尽可能满足三大方式,但是如果完全按照第二范式和第三范式的话,我们在数据量大的时候,可能会增加很多的 join 操作,join 操作的底层就是一个 for 循环,多次 join 就代表多次嵌套,会导致性能比较差。所有有的时候我们会采取反范式设计,即适当地进行数据冗余,比如一张学生表、一张班级表,一张学生班级关系表,如果要查询哪些班有哪些学生,就必须要关联三张表。但是如果把学生名字、班级名称这些字段冗余到学生班级关系表里面,就可以不用多次 join,从而提高性能。

数据库的三大范式:

第一范式:原子性,每个字段的值不能再分。

第二范式:唯一性,表内每行数据必须描述同一业务属性的数据。

第三范式:独立性,表中每个非主键字段之间不能存在依赖性。

合理分表

慢 SQL 的一个重要原因就是单行查询的数据量过大,从而导致磁盘 IO 较大,针对这个的话,我们一般会去采取分表的策略,分表的话一般有垂直分表和水平分表两种方式,垂直分表其实就是为了减少单条数据的大小,从而让聚簇索引能够容纳更多的数据,减少单表数据的高度,这个的话和我想到了之前一个 BI 分析的项目,当时主要就是给提示词给大模型,然后大模型按照模板生成对应的 Echart 代码,然后一开始的时候就是 Echart 代码直接存储到 MySQL 的一个数据列中,后面想到的就是如果单行数据列过大可能导致整体查询效率过低,就用垂直分表去解决,后面的话但是我是继续拓展,使用 MongoDB 进行存储,另外一个方式就是水平分表,水平分表还有两种方式,一种是按照某一个字段进行哈希分表,分表数量是 2 的倍数,这种分表的分表原则就是针对表进行压测评估,看看多少容量级会有明显的延迟,一般是按照未来 3 年的数据量来预估要分多少张表。还有一种方式就是按照时间去进行拆分,形成归档表以及冷热表,这个的话我当时东华教育 CRM 以及框架项目分表、还有我实习公司当时活动后台的日志数据查询,都是按照这个逻辑,即按照时间线去进行分表,形成归档表以及冷热表。比如按照月份一个月一张表进行分,或者按照年去进行分表。实际工作中主要是这两种方式结合去做,比如我当时东华教育项目中用到的,首先我们订单表根据用户去进行分表,然后我们同时还观察到用户可能只关心最近的数据,所以我们把 3 个月以内的数据定义为热数据,3-6 个月的数据定义为温数据,把 6 个月以上的数据定义为冷数据,然后使用像 DataX、Kettle ETL 工具定期将数据同步到另外的一些表里面,比如饿了么就只能查询一年以内的数据。

配置参数

第五个就是配置合理的参数,这里的值有点多,我说一下几个比较重要的值,比如 buffer_pool,独立部署的话,缓冲池大小一般推荐是服务器物理内存的 50%-80%,还有一些其他的参数比如 join_buffer_size 是用于 MySQL 中 join 时分配的缓冲区大小,sort_buffer_size 是用于排序时候的缓存,如果太小的话 MySQL 可能会在硬盘上进行排序,从而导致性能不高。read_buffer_size 是控制单个 session 读取数据的缓冲池大小,增大也可以提高性能,不过这些参数的设置具体还是得根据实际压测的效果来给出。

软件层面大概就这么多了,然后接下来就是硬件层面的内容。

硬件层面

硬件层面的话一般是到了万不得已的时候采取的方式,因为 MySQL 其实没有我们想得那么脆弱,软件层面的优化成本是最低的,比如有限更换硬盘的类型,更换硬盘的成本其实是最低的,比如机械硬盘可以换成 SSD 方式存储,因为 SSD 的随机读写能力比机械硬盘要好一点,以前我利用实验室的电脑去压测过,如果是 insert 语句,插入一个大数据量的字符串,本地的 SSD 硬盘可以达到 1000 TPS,TPS 即每秒钟执行的数据量,我当时是直接插入,所以相当于每条 SQL 语句就是一次事务提交,然后机械硬盘会少一点,只有 600 多-700 的样子。

然后就是扩大硬件规格,因为 MySQL 是一个 IO 密集型的中间件,最推荐的就是加内存,因为内存越大,上面所说的哪些缓冲池参数可以设置更大,这样 MySQL 可以将更多的数据放到内存中去,提高的效率也就越多。

架构层面

然后最后就是架构层面,首先对于这个方面,我感觉可以部署 MySQL 一主一从集群,因为做了这个集群以后,理论上就可天然地将读的性能提高一倍差不多,不过了解到还有一主二从那些,不过那些主要还是看项目需求。或者可以采用分区表的方式,不过分区表理论上还是会收到单机性能来的影响,所以可以引入分布式数据库,分布式数据库当时我之前项目中冷热数据分离这块有用到过,当时主要是日志数据,我们将冷数据归档到 HBase 中,在结合中间表以及索引等机制提高冷数据查询效率,然后分布式数据库也顶不住之后,就可以考虑使用分库分表,不过一般是到了没有办法才使用的,因为分库分表需要考虑的点很多,比如全局 ID 选择、分库分表字段等,这些如果经验不是很够的话很难去进行选择,而且分库分表还会带来跨表查询、跨表事务等问题,然后针对分库分表方案,这里可以考虑引入 MyCat 或者 Sharding 等框架。

还有一个点忘记说了,就是你可以考虑加缓存,这可以理论上来说已经不算是数据库的优化了,但是可以减轻数据库的压力,即把一些查询频繁的,然后更新不怎么频繁的数据放到缓存中去,从而减少对于数据库的访问,然后缓存的话我们可以考虑使用多级缓存,比如 Java 里面的 Caffeine、guava、ehcache,Go 里面我之前用到的一个缓存驻留的库 singleflight,然后二级缓存比较常用的就是 redis、Memcached,以上就是我感觉比较可以的点了,以上就是我想到的关于数据库优化的大概方案,然后我之前还有过一个相关的优化案例,需要我说一下吗?

SQL 优化案例

SQL 优化案例补充:4s -> 500ms

当时遇到一个 SQL 语句,那个是一个外包的后台系统好像,然后当时我们实验室主要就是负责这个系统的一些运维,然后那个 SQL 一开始在生产环境查询的时候,全表差不多 22w 条数据,查询时间是用了 3.8s-4.5s 的样子,然后后面就让我们优化,本地测试的时候时间差不多就是 1.8s 左右,然后用的 MySQL 版本是 8.0,然后一开始我们对接的时候说这个时间还是可以的,后面那边说不太可以,因为这条 SQL 需要执行差不多两次,一次是查询结果,然后一次是 count 语句,算下来时间就差不多 7 秒左右,最后优化的结果是返回 3000 多条记录,然后执行时间差不多 500ms 左右,我记得当时最快的一次是达到了 300 ms.

  1. 执行 explain 执行计划,查看是否命中索引了,当时发现已经命中索引了,虽然有一些走的是合并索引,然后我这里简单说一下合并索引这个点,这个是 5.1 之后引入的技术,对同一个表可以使用多个索引分别进行条件扫描,然后将结果进行合并处理,然后最后进行回表查询。

这样做的好处就是可以同时利用两个索引去过滤掉一些 ID 值,从而节省一些回表操作,不过索引合并还是会产生一定的性能损耗,一般来说用联合索引会更加好一些。

  1. 然后看见索引都命中之后,接下来主要看 filtered 以及 rows 的条数,因为知道走索引了,接下来主要判断其走回表的次数,然后当时先去看一张 join 的 cfg 表,那个 join 的 cfg 表走了 5w 多条数据,由于 join 的主键是非聚簇索引,代表 join 的时候走了 5w 次回表,然后针对这个,我们当时是针对这个 cfg 表先建一个联合索引,然后尝试执行一下,马上效率就到 1.9s 了。
  2. 然后第二个优化的点还是一个 join 的子表,那个表的问题主要就是出在一个索引合并上,那个索引合并的表只走了前面的索引,即第一个索引,后面的索引并没有走,然后当时我们先查询了其中一个字段,发现只有 5000 多条数据走了其中一个索引,然后我们当时是根据条件查询的字段加上命中的字段建立一个联合索引,后面发现有点神奇,他走的还是一开始那个索引,并没有走我们后面建立的索引,不过他的时间下降了,这里当时我第一反应就是发生索引失效了,后面排查的时候发现主要是走的数据变少了,差不多只有 3000 多条,也就说回表的次数减少到了 3000 多,时间差不多就到 1.6~1.8 左右。
  3. 然后回到上面的索引失效问题,我发现我当时的 SQL 语句也没有导致索引失效的场景,比如在 where 子句上面去进行函数计算操作、模糊匹配、隐式转换也都没有,后面突然想到一件事,就是可能 MySQL 自己的查询优化器的缘故,就是我建立的联合索引效率不一定比我之前那个索引的效率高,所以只能继续优化
  4. 继续优化的时候我想着,既然我建立的索引回表次数少了,则说明可能是索引的排序出现问题,因为我只建立了对应的联合索引,原先的字段并没有改变,那我就以原先索引的字段作为第一个字段,然后将建立的联合索引字段排在这个原先索引前面就 ok 了,这样就不用走二次回表了,应该可以快一点,后面发现还是没有效果,他还是走了原先的索引,然后我说一下我建立的两个索引的区别,主要就是第一个字段,一个是以 type 作为第一个字段,一个是以 id 作为第一个字段,走 id 那个会扫描 3000 多条数据,走 type 差不多扫描就是 1000 多条数据
  5. 后面我的想法就是让优化器去强制走我们建立type 的索引,即在我们后面建立索引的基础上,去加一个 force idx,即强制索引,后面发现差不多就可以达到 500 ms 了

深分页优化

深度分页问题是指在数据库查询中,当你尝试访问通过分页查询返回的结果集的后面部分(即深层页码)时遇到的性能问题。

假设你有一个包含数百万条记录的表,你想通过分页的方式来展示这些数据。当用户请求第10000页数据时,假设pageSize为10,那么最终就是LIMIT 99990,10 ,数据库必须先扫描过前99990条记录,才能返回第10000页的数据,这会导致显著的性能下降。

99991是起始ID = (页数 - 1) * 每页项目数 + 1

对于第1页,起始ID将是1,结束ID将是10。对于第2页,起始ID将是11,结束ID将是20,以此类推。

对于第10000页:

起始ID = (10000 - 1) * 10 + 1 = 99991

使用子查询和 JOIN 优化

假如我们这样一条SQL:

plain
▼text

复制代码SELECT c1, c2, cn... FROM table WHERE name = "test" LIMIT 1000000,10

我们可以基于子查询进行优化,如以下SQL:

plain
▼text

复制代码SELECT c1, c2, cn...
FROM table
INNER JOIN (
    SELECT id
    FROM table
    WHERE name = "test"
    ORDER BY id
    LIMIT 1000000, 10
) AS subquery ON table.id = subquery.id

首先,使用子查询获取限定条件下的一部分主键 id,这部分id对应于我们分页的目标区域,使用这些 id 在主查询中获取完整的行数据。

以上SQL,在name有索引的情况下,子查询中查询id是不需要回表的。而当我们查询出我们想要的10个ID之后,基于ID查询不仅快,而且要查的数据量也很少。

使用子查询和 ID 过滤优化

plain
▼text

复制代码SELECT c1, c2, cn...
FROM table
WHERE name = "linqi"
  AND id >= (SELECT id FROM table WHERE name = "Hollis" ORDER BY id LIMIT 1000000, 1)
ORDER BY id
LIMIT 10

这个方法代替了join的方式,使用了一个子查询来获取从哪里开始分页的参考点,基于ID做范围查询。但是这个方案有个弊端,那就是要求ID一定要是自增的。

和上面的方案同理,他也可以减少回表的次数。

记录上一个 ID

还有一种方式,就是上一个方式的变种,就是提前预估要查询的分页的条件,记住上一叶的最大 ID,下一次查询的时候,可以根据 id > max_id_in_last_page 进行查询。

使用搜索引擎

另外,如果是基于文本内容的搜索,可以使用 Elasticsearch 这样的全文搜索引擎来优化深度分页性能。但是需要注意的是,ES也会有深度分页的问题,只不过他的影响比MySQL要小一些。

分库分表

这里不做过多的赘述,分库分表主要在你基本所有的优化方式使用过之后,实在没有办法可以考虑,不过也可以直接考虑上分布式数据库,像 HBase、TiDB、Cassandra 等等,可以通过这种方式进行优化,这里就不做过多赘述了。

总结

然后到这里基本上这个问题也回答差不多了,我面试用过几次,差不多时间可以到 5~10分钟左右,所以应该还是可以的。

13. MySQL 中使用索引一定有效吗?如何排查索引效果?

https://www.mianshiya.com/bank/1791003439968264194/question/1780933295463231490#heading-0

拓展知识:InnoDB 的表数据实际上存储在聚簇索引(主键索引)的叶子节点

🟢 怎么更严谨地表达?

你可以这样说(面试用):

InnoDB 的表数据实际上存储在聚簇索引(主键索引)的叶子节点中。所以如果表中没有建立其他二级索引,所有的数据查询最终都需要通过聚簇索引来访问数据。因为数据行本身就是聚簇索引的一部分。


再帮你做个简化理解

  • 聚簇索引 = 数据行本身
  • 二级索引 = 索引+主键引用,查数据还得回到聚簇索引

所以:

  • 没有二级索引 👉 查数据只能走聚簇索引
  • 有二级索引 👉 可以先走二级索引,但最后还得“回表”到聚簇索引拿完整数据

🟡 你原句的小瑕疵

  • “必须且只能” 👉 逻辑上没错,但其实应该说:

    • 只有走聚簇索引才能拿到完整数据行
    • 如果你建了二级索引,查询条件满足索引覆盖,也可以只用索引不用回表

所以严谨说:

没有其他索引的情况下,InnoDB 的数据查询最终只能通过聚簇索引来访问。


一句口诀帮你记

InnoDB:聚簇索引即数据,二级索引指路牌。

14. MySQL 中的索引数量是否越多越好?为什么?

标准面试回答

索引可以加快查询速度,但是索引不是越多越好,因为索引也会带来以下成本

  1. 写入性能下降
    • 每次执行 INSERTUPDATEDELETE,不仅要改数据行,还要同步修改相关索引
    • 索引越多,写入和更新的成本就越高
  1. 占用更多存储空间
    • 索引本质上也是一种数据结构(B+ 树),每个索引都会占用磁盘和内存空间。
    • 索引多了,磁盘空间和**Buffer Pool(内存)**压力也会增加。
  1. 查询优化器选择成本
    • 当索引过多时,MySQL 的查询优化器需要在众多索引中计算和选择最优路径,这会增加优化成本,甚至可能选择次优索引导致查询变慢。

加分点(可以补充一句)

索引设计要遵循**“高选择性、低冗余、常查询”的原则,对经常用于 **WHERE****JOIN****ORDER BY****GROUP BY** 的列加索引,但避免对低选择性频繁变动**的列滥用索引。

15. 请详细描述 MySQL 的 B+ 树中查询数据的全过程

1️⃣ 从根节点开始

  • 查询从B+ 树根节点启动。
  • 根节点通常常驻在内存Buffer Pool中,所以读取速度快。
  • 根据要查询的键值,与节点中存储的索引键值进行比较,使用二分查找确定数据落在哪个区间,从而选出对应的子分支

2️⃣ 层层下探,直到叶子节点

  • 依次进入下一级的非叶子节点,每层节点也会通过二分查找来决定下一步走哪条分支。
  • 这种查找会递归下探,直到定位到叶子节点

3️⃣ 定位叶子节点中的数据行

  • InnoDB 的叶子节点存储着:

    • 聚簇索引:完整数据行
    • 二级索引:索引键 + 主键值
  • 需要注意:

    • 每个叶子节点其实就是一个数据页(默认大小16KB),一页中包含多条数据行

4️⃣ 叶子节点内:先二分查组,再链表遍历

  • 叶子节点内部,数据行是**以组(记录组)**的形式划分的。
  • 首先通过页目录结构,对这些组进行二分查找,快速定位到目标组。
  • 然后在目标组内,通过链表遍历,找到具体的数据行

5️⃣ 返回数据 or 回表

  • 如果是查询聚簇索引(比如按主键查询):

    • 直接在叶子节点取出完整数据行返回。
  • 如果是查询二级索引

    • 先拿到主键值(二级索引叶子节点只存主键),然后回表到聚簇索引,再执行一次查找(按主键)取出完整数据。

16. 为什么 MySQL 选择使用 B+ 树作为索引结构?

标准面试回答:为什么MySQL使用B+树

MySQL(尤其是InnoDB)选择使用B+树作为索引结构,主要因为它对磁盘IO和范围查询特别友好,适合数据库这种大规模数据存储场景:

1️⃣ 磁盘IO效率高

  • B+树的非叶子节点不存储具体数据,只存储键和指针,因此单个节点能容纳更多键值(提高扇出)。
  • 这样B+树的高度更低,查找数据时磁盘IO次数更少,一般3~4次IO就能定位数据。

2️⃣ 范围查询性能优越

  • B+树的叶子节点之间通过链表连接,天然支持范围查询BETWEEN><等操作。
  • 比如一次查a > 100 AND a < 200,只需要沿着叶子节点链表顺序扫描,非常高效。

3️⃣ 数据和索引结构分离

  • 非叶子节点不存数据,数据只在叶子节点,保证了所有查询路径长度一致,查找性能更稳定。

🟢 通俗理解

B+树就像书的目录

  • 目录页(非叶子节点)只告诉你在哪页找,不放正文(数据) 👉 能装下更多信息,查得快。
  • 目录指向的叶子节点顺序连接,查范围(比如“查第5章到第10章”)直接一遍扫过去,非常高效。
  • 所有正文都在叶子节点,查询路径一致,性能稳定。

为什么不用其他结构?(面试加分)

数据结构为什么不选
B树叶子节点和非叶子节点都存数据,导致范围查询复杂、节点利用率低
哈希索引只适合等值查询,不支持范围查(><
红黑树适合内存结构,高度较高(O(logN)),磁盘IO次数多,不适合数据库
跳表在高磁盘IO场景下,不如B+树扇出高、IO次数少

总结一句话

B+树能最大化减少磁盘IO,且对范围查询友好,非常契合数据库索引的需求,因此MySQL选择它。

17. MySQL 是如何实现事务的?

标准面试回答:MySQL事务的实现机制

MySQL(InnoDB引擎)通过多种核心机制协同实现了事务:

1️⃣ 事务的ACID特性

MySQL事务要满足ACID,即:

  • A(原子性):事务要么全成功,要么全失败
  • C(一致性):事务执行前后,数据保持一致性规则
  • I(隔离性):并发事务之间互不干扰
  • D(持久性):事务提交后,数据永久保存

2️⃣ 关键实现机制

特性实现机制
原子性Undo日志(回滚日志)
一致性事务+日志+锁共同保证
隔离性锁机制 + MVCC
持久性Redo日志(重做日志)

3️⃣ 详细流程

✅ a. Undo日志(回滚日志)

  • 在事务执行前,InnoDB会先记录旧值到Undo日志。
  • 如果事务失败,可以通过Undo日志回滚,实现原子性。

✅ b. Redo日志(重做日志)

  • 事务修改时,先记录到Redo日志(WAL机制:先写日志,再写磁盘)。
  • 即使数据库崩溃,Redo日志可以恢复未持久化的数据,实现持久性。

✅ c. MVCC(多版本并发控制)

  • InnoDB用快照读+Undo日志实现事务隔离,支持高并发。
  • 读操作无需加锁,提升性能。

✅ d. 锁机制

  • 行级锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)实现严格的隔离控制,避免幻读。

🟢 通俗理解

你可以这样记:

事务就像银行转账:

  • Undo日志:先记下原账本(以防出错能撤回)
  • Redo日志:先写流水账(以防断电后还能恢复)
  • 锁和MVCC:让多个客户转账互不干扰
  • 提交时,把日志真正写入账本,保证钱不会丢

一句话总结

MySQL通过Undo日志实现回滚、Redo日志保证持久、MVCC+锁确保隔离,从而完整实现了事务的ACID特性。


🔥 加分细节(面试可以补充)

  • MySQL事务使用**WAL(Write Ahead Logging)**机制,先写Redo日志,后写磁盘。
  • InnoDB事务隔离级别支持:读未提交、读已提交、可重复读、串行化,默认可重复读
  • MVCC原理依赖于隐藏字段(事务ID、回滚指针)Undo日志组合实现。

口诀助记

Undo保原子,Redo保持久,锁与MVCC,隔离不冲突。

下述图片总结很到位

img

18. MySQL中的MVCC是什么?

https://www.mianshiya.com/bank/1791003439968264194/question/1780933295484203009#heading-1

🔄 读已提交(Read Committed) vs. 可重复读(Repeatable Read)—— ReadView 特性对比

特性读已提交(RC)可重复读(RR)
ReadView 生成时机每次查询时都会生成新的 ReadView事务开始时生成,一整个事务中复用同一个 ReadView
是否每次查询视图更新✅ 是(动态视图)❌ 否(静态视图)
是否可能出现不可重复读✅ 可能(同一事务中两次查询结果不同)❌ 不会(同一事务中两次查询结果相同)
事务中的多次 **SELECT** 行为每次可能读到新的已提交版本总是读到事务开始时可见的版本
MVCC 过滤逻辑根据最新 ReadView判断是否可见根据事务开始时的 ReadView判断是否可见
快照一致性只保证当前查询的一致性保证整个事务的一致性

📚 口诀帮记:

  • 读已提交 ➔「每次查,每次新视图」➡️ 动态
  • 可重复读 ➔「事务开始定快照」➡️ 静态

🎯 直观理解:

  • RC:像每次刷新网页,都能看到最新已提交的内容
  • RR:像事务一开始拍了张照片(快照),整个事务期间都只能看到这张照片里的内容

当前读和快照读

当前读 = 读取最新已提交版本 + 从现在起加锁防止并发冲突 ➡️ 已经提交的数据变化对当前读可见 ➡️ 还没提交的数据对当前读不可见(被MVCC屏蔽) ➡️ 其他事务从现在起要改,会被锁住(阻塞)


🎯 对比帮助理解:

读类型读取的版本是否加锁对并发事务的影响
快照读事务开始时的ReadView❌ 不加锁不阻塞其他事务
当前读最新已提交版本✅ 加锁阻塞其他事务从现在起对记录的修改

🚀 用一句形象化帮助你彻底记住:

🔒 当前读:我看的是最新提交数据,同时从现在起锁住,不许别人再动! 📸 快照读:我只看我开拍时的照片,别人动不动跟我无关!

🔒时间轴理解当前读

时间点事务1 (当前事务)事务2 (并发事务)
T1开启事务
T2SELECT ...(普通快照读,不加锁)
T3开启事务
T4**UPDATE** 数据,拿到行锁
T5提交事务(释放行锁)
T6**SELECT ... FOR UPDATE**(当前读,加锁)
T7事务1 当前读**,**看到事务2的提交结果

事务2修改在事务1之后,但事务1的当前读能看到事务2的结果

MVCC 和锁的关联

🔑 核心问题:

为什么在使用MVCC的同时,还需要加锁呢?


1. MVCC的工作方式

  • MVCC的核心思想是:通过多版本控制,每个事务只看它自己开始时的快照,而不是其他事务的未提交数据。
  • 数据的版本是通过为每个修改记录事务ID来管理的。每个数据行(记录)有多个版本,不同的事务看到不同的版本

例如:

  • 事务1开始时看到版本1的记录。
  • 事务2开始时看到版本2的记录(因为事务2提交了修改)。
  • 事务3看到的记录是事务3开始时的快照,即它看到的记录版本根据事务3的时间点进行更新。

通过这种方式,MVCC避免了多个事务在同一数据上发生冲突读取(读已提交隔离级别下的效果),也避免了“脏读”,这意味着它在保证一致性的同时也能够允许更高的并发。


2. 加锁的目的和作用

虽然MVCC可以保证事务看到的版本是一致的,但MVCC并不完全解决所有并发问题,尤其是在修改数据(写操作)的场景下。因此,仍然需要加锁来防止数据冲突或不一致。

关键原因有以下几个:

1. 避免“幻读”

  • 幻读是指:在同一个事务中,两次查询同样的条件时,返回的结果集不一致,这通常发生在INSERT或者DELETE操作导致的数据插入或删除

  • MVCC解决的是数据版本的问题,但它并不能解决事务间的冲突,比如:

    • 事务A查询符合某个条件的记录集合。
    • 事务B在此基础上插入了新的记录,满足事务A查询条件。
    • 当事务A再次执行查询时,发现新增记录,导致结果集发生变化。

这种情况下,即使事务A对这些记录进行了快照读取,幻读问题依然存在。

加锁(如SELECT ... FOR UPDATE)可以避免这种问题,因为在事务A查询期间,锁住了数据,从而阻止了其他事务对数据的插入或修改,确保查询结果的一致性。

2. 写操作的并发冲突

  • MVCC可以让多个事务并发地读取不同版本的数据,但它并不能避免多个事务同时尝试修改同一行数据的情况。
  • 加锁通过加锁数据行来防止多个事务对同一数据行的并发修改,从而避免丢失更新的问题。

3. 确保事务的ACID属性(特别是原子性和隔离性)

  • 加锁是数据库中一种常见的机制,用于确保事务的隔离性。即确保一个事务的操作在它提交之前不会被其他事务看到,也不会影响其他事务。锁机制能够确保事务的原子性隔离性,让事务内部的操作作为一个不可分割的单位执行。

4. 防止“脏读”和“不可重复读”

  • 脏读:事务读取到未提交的修改,可能会导致数据不一致。
  • 不可重复读:事务在两次查询中读取到不同版本的数据,导致结果不一致。

加锁(如行锁)确保了在事务处理过程中,数据的一致性和事务隔离性,即使数据的版本在MVCC上有变化,仍然可以保证事务在读取时不会受到其他事务的干扰。


3. MVCC与加锁的结合使用

  • MVCC负责通过版本控制来避免读取冲突,特别是对读取操作来说,它有效地避免了“脏读”和“不可重复读”。
  • 加锁则主要用于防止写冲突,特别是当多个事务同时对同一数据进行修改时,通过加锁避免了丢失更新幻读等问题。

总结:

  • MVCC能解决读取一致性的问题(避免脏读、不可重复读等)。
  • 加锁能解决写操作的冲突,并且能够防止幻读以及事务并发修改冲突,保证事务之间的隔离性。

最后总结:

  • MVCC解决读一致性,保证了不同事务可以并行读取不同版本的记录。
  • 加锁是为了保证写一致性,防止写操作之间的冲突,确保事务的隔离性。

读已提交和可重复读这两个隔离级别的本质

读已提交可重复读这两个隔离级别的核心区别,就是 read view(快照)创建的时机不同


🔥 我帮你把这个机制拆得更彻底一点:

隔离级别read view 创建时机结果表现
读已提交 (READ COMMITTED)每次查询时都会重新创建新的 read view所以:一次事务内多次查询结果可能不一样(不可重复读)
可重复读 (REPEATABLE READ)事务开始时就创建了 read view,整个事务期间固定不变所以:事务内多次查询结果始终一致(可重复读)

✅ 也可以这么理解:

  • MVCC机制本质上通过read view来决定:

    • 这个事务应该看到哪些版本
    • 哪些版本对它来说是不可见
  • 所以:

    • 读已提交动态 read view ➔ 隔离性稍弱(允许不可重复读)
    • 可重复读固定 read view ➔ 隔离性更强(避免不可重复读)

🔑 换句话说:

你说的这句话:

读已提交和可重复读就是通过readview的创建时机来进行读取不同版本的数据

19. MySQL 中长事务可能会导致哪些问题?

1)长时间的锁竞争,阻塞资源

  • 长事务持有锁的时间较长,容易导致其他事务在尝试获取相同锁时发生阻塞,从而增加系统的等待时间和降低并发性能。
  • 业务线程也会因为长时间的数据库请求等待而阻塞,部分业务的阻塞可能还会影响到别的服务,导致产生雪崩,最终使得服务全面崩盘,导致非常严重的线上事故。

2)死锁风险

  • 长事务更容易产生死锁,因为多个事务可能在互相等待对方释放锁,导致系统无法继续执行。

3)主从延迟

  • 主库需要长时间执行,然后传输给从库,从库又要重放好久,期间可能有很长一段时间数据是不同步的。

4)回滚导致时间浪费

  • 如果长事务执行很长一段时间,中间突发状况导致抛错,使得事务回滚了,之前做的执行都浪费了。

解决方法:

1)分批处理

2)并行执行

3)合理使用索引

4)删除转插入

20. 如果 MySQL 中没有 MVCC,会有什么影响?

  1. 如果没有 MVCC,系统必须频繁地对读写操作进行加锁来保证数据的正确性,因为增加了锁的获取和释放的开销,会导致整体系统响应速度变慢,这种实现叫 LBCC (Lock-Based Concurrent Control)
  2. 多个事务之间会频繁的竞争锁资源,事务可能因为等待锁而被组赛,从而延长用户的的响应时间。

21. 事务的隔离级别有哪些?

1)读未提交(READ UNCOMMITTED)

  • 这是最低的隔离级别,在该级别下,一个事务可以看到另一个事务尚未提交的数据修改。这可能会导致脏读问题,即读取到其他事务未提交的数据。

2)读已提交(READ COMMITTED)

  • 在这个级别下,一个事务只能看到已经提交的其他事务所做的修改。这可以避免脏读问题,但是可能会引发不可重复读问题,即在同一个事务中,相同的查询可能返回不同的结果。

3)可重复读(REPEATABLE READ)

  • 在这个级别下,确保在一个事务中的多个查询返回的结果是一致的。这可以避免不可重复读问题,但是可能会引发幻读问题,即在同一个事务中,多次查询可能返回不同数量的行(MySQL 默认的隔离级别)。

4)串行化(SERIALIZABLE)

  • 并发 SQL 事务在 SERIALIZABLE 隔离级别下的执行被保证是可串行化的。可串行化执行被定义为:并发执行的 SQL 事务的操作,其效果与这些 SQL 事务按某种顺序串行执行的效果相同。串行执行是指每个 SQL 事务在下一个 SQL 事务开始之前完成其全部操作 来源 SQL92定义
  • 这是最高的隔离级别,在这个级别下,保证事务间的操作结果相当于一个按顺序执行的单线程操作。这可以避免所有的并发问题,但是会大大降低并发性能。

img

22. MySQL中锁有哪些类型?

快照读

  • 仅在 ⌈读已提交⌋ 和 ⌈可重复读⌋ 隔离级别下才会对 SELECT 语句使用快照读

    • ⌈读已提交⌋ 级别下,快照读在 ⌈每次执行读SQL语句时⌋ 读取的都是最新的快照数据
    • ⌈可重复读⌋ 级别下,快照读总是在 ⌈开启事务时⌋ 读取最新的快照数据

当前读

  • 需要给读取的行记录添加 X 型锁 或者 S 型锁;确保数据一致性。
  • 常用于事务操作,包括如下语句
plsql
# 对读的记录加一个 X 锁
SELECT ... FOR UPDATE

# 对读的记录加一个 S 锁
SELECT ... LOCK IN SHARE MO`DE

# 对读的记录加一个 S 锁
SELECT .... FOR SHARE

# 添加 X 锁
INSERT ....
UPDATE ....
DELETE ....

23. MySQL 的乐观锁和悲观锁是什么?

标准面试回答

🔷 1️⃣ 悲观锁(Pessimistic Lock)

  • 假定并发冲突很可能发生,所以在数据访问前会先加锁来阻止其他事务对数据的修改。

  • 在数据库中,通常通过行锁来实现,如:

    • SELECT ... FOR UPDATE
    • SELECT ... LOCK IN SHARE MODE

✅ 特点

  • 并发安全,不会出现脏读、幻读
  • 性能较低,加锁阻塞其他事务,影响并发。

🔷 2️⃣ 乐观锁(Optimistic Lock)

  • 假定并发冲突很少发生,所以数据操作时不加锁,提交时再检查数据是否被别人改过。
  • 通常通过版本号(version)时间戳字段来实现。

✅ 实现方式

  1. 读取数据时获取version
  2. 更新数据时,WHERE id=... AND version=旧值,同时version+1
  3. 如果影响行数=1,说明更新成功;否则说明数据被改过,更新失败(需重试)

✅ 特点

  • 并发性能好,不加锁,不阻塞其他事务。
  • 适合读多写少的场景。
  • 存在更新失败、重试的成本。

24. MySQL 中如果发生死锁应该如何解决?

25. 如何使用 MySQL 的 EXPLAIN 语句进行查询分析?

26. MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?

在 MySQL 中,count(*)count(1)count(字段名) 都是用来统计行数的聚合函数,但它们有些许的区别:

功能上:

1)count(*) 会统计表中所有行的数量,包括 null 值(不会忽略任何一行数据)。由于只是计算行数,不需要对具体的列进行处理,因此性能通常较高。

2)count(1)count(*) 几乎没差别,也会统计表中所有行的数量,包括 null 值。

3)count(字段名) 会统计指定字段不为 null 的行数。这种写法会对指定的字段进行计数,只会统计字段值不为 null 的行。

27. MySQL中如何进行SQL调优?

简要回答

  1. 避免select *,只查询必要的字段
  2. 合理设计索引,通过联合索引进行覆盖索引及索引下推技术的优化,减少回表的次数,提升效率
  3. 避免SQL中进行函数计算等操作,导致无法命中索引
  4. 避免使用like,导致全表扫描(如果符合最左前缀匹配原则可以走索引)
  5. 注意使用联合索引需满足最左匹配原则
  6. 不要对无索引字段进行排序操作
  7. 连表查询需注意不同字段的字符集保持一致,
  8. 注意隐式类型转换操作,会导致索引失效
  9. 使用OR ,两边需保持等值匹配且都为索引列,才会走索引

补充回答

以上都是对SQL进行优化,避免索引失效等进行SQL调优,还可以从其他方面进行考虑,比如先分析SQL慢的原因

  • 索引失效
  • 多表join
  • 查询字段太多
  • 表中数据量太大
  • 索引区分度不高
  • 数据库连接数不够
  • 数据库的表结构不合理
  • 数据库IO或者CPU比较高
  • 数据库参数不合理
  • 长事务导致的
  • 锁竞争导致的长时间的等待

一次完整的SQL调优,一般要考虑以上几个因素,一般会涉及其中一个或者多个问题。

扩展回答

Q: 为什么多表join会导致SQL慢?

A : MySQL是使用了嵌套循环Nested-Loop Join)的方式来实现关联查询的,简单点说就是要通过两层循环,用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出。 而具体到算法实现上主要有simple nested loopblock nested loopindex nested loop这三种。而且这三种的效率都没有特别高。 MySQL是使用了嵌套循环Nested-Loop Join)的方式来实现关联查询的,如果有2张表join的话,复杂度最高是O(n^2),3张表则是O(n^3)...随着表越多,表中的数据量越多,JOIN的效率会呈指数级下降

Q: 不使用join,如何做关联查询?

A: 主要有以下做法: 1、在内存中自己做关联,即先从数据库中把数据查出来之后,我们在代码中再进行二次查询,然后再进行关联。 2、数据冗余,那就是把一些重要的数据在表中做冗余,这样就可以避免关联查询了。 3、宽表,就是基于一定的join关系,把数据库中多张表的数据打平做一张大宽表,可以同步到ES或者干脆直接在数据库中直接查都可以

Q:你上面说数据量太大也会导致SQL慢,那怎么解决?

A:具体的解决方案有以下几种:

1、数据归档,把历史数据移出去,比如只保留最近半年的数据,半年前的数据做归档。 2、分库分表、分区。把数据拆分开,分散到多个地方去,这里不详细介绍了,我们的文档中有分库分表和分区的详细介绍,不展开了。 3、使用第三方的数据库,比如把数据同步到支持大数量查询的分布式数据库中,如oceanbase、tidb,或者搜索引擎中,如ES等。

Q:数据库参数不合理需要怎么调整?

A:首先通过SHOW VARIABLES LIKE 'innodb%';查看当前的innoDB参数配置,这些参数包括缓冲池大小、刷新间隔、日志大小等。

  • 调整缓冲池大小(设置为系统可用内存的70%-80%,假设有8g内存可用)
SET GLOBAL innodb_buffer_pool_size=6G;
  • 调整存储引擎使用的I/O线程数量(通常这两个参数设置为CPU核心数的一半)
plsql
SET GLOBAL innodb_read_io_threads=4;
SET GLOBAL innodb_write_io_threads=4;
  • 调整事务日志文件的大小(默认为5M,但远远不够,可以设置为业务高峰2小时写入的日志量,也可以直接设置为1G或者系统内存的1/4)
SET GLOBAL innodb_log_file_size=1G;

28. 如何在 MySQL 中避免单点故障?

一般会使用主从架构来避免单点故障,主数据库处理写操作,从数据库处理读操作,主数据库故障时可以切换到从数据库。

同时会对数据进行定期备份并存储在不同的物理位置,以便在发生故障时能够快速恢复数据。

并且需要建立监控系统,实时监控数据库的健康状态,并在发生故障时及时告警。

29. 如何在 MySQL 中实现读写分离?

1️⃣ 架构原理

  • 写操作:发送到主库(Master)
  • 读操作:发送到从库(Slave)
  • 主库和从库通过复制保持数据同步

2️⃣ 实现方式

🔷 (1) 应用层实现

  • 在应用代码中,手动分配读请求到从库,写请求到主库

  • 框架支持:

    • MyBatis:可以配置读写分离数据源
    • Spring:用AbstractRoutingDataSource实现动态路由

🔷 (2) 中间件/代理层实现

  • 使用代理中间件自动分发读写请求

    • MySQL Router(官方)
    • ProxySQL(业界广用,功能强)
    • AtlasMaxScale 也是可选

🔷 (3) 数据库内置

  • InnoDB Cluster + MySQL Router可以自动做读写分离和高可用

30.

主从复制的具体流程

  1. 线程创建:从服务器开启主从复制后,会创建 I/O 线程和 SQL 线程。
  2. 连接建立:从服务器的 I/O 线程与主服务器建立连接,主服务器的 binlog dump 线程与之交互。
  3. 同步位置告知:从服务器的 I/O 线程告知主服务器的 dump 线程从何处开始接收 binlog。
  4. 主服务器操作:主服务器更新时将更改记录保存到 binlog,不同格式记录内容有别。
  5. binlog 传输:主服务器 dump 线程检测到 binlog 变化,从指定位置读取,由从服务器 I/O 线程拉取,采用拉取模式利于从库管理同步进度和处理延迟
  6. 中继日志存储:从服务器 I/O 线程将接收到的内容保存到 relay log 中。
  7. 数据写入:从服务器的 SQL 线程读取 relay log 内容,解析成具体操作后写入自身数据表 。

img

扩展知识

innoDB的一次更新事务流程

  1. 数据读取:从 Buffer Pool 中查找待更新记录,若不存在则从磁盘读取到 Buffer Pool。
  2. 记录 UndoLog:修改前在内存中记录原数据,之后由后台线程定时写入磁盘,用于事务回滚以保证原子性和一致性。
  3. 更新数据:在 Buffer Pool 中更新数据,并将数据页标记为 “脏页”。
  4. 记录 RedoLog Buffer:将修改操作写入 redo log buffer 。
  5. 提交事务:完成修改后提交事务,将 Redo Log 写入磁盘保证持久性。
  6. 写入磁盘:提交后,后台线程异步将 Buffer Pool 中的脏页写入磁盘实现持久化。
  7. 记录 Binlog:提交过程中,将事务相关信息(如开始时间、数据库名等)记录到 Binlog,用于主从复制 。

img

主从同步的方式

  • 异步复制:是默认方式,主库执行完事务马上给客户端返回,不关注从库是否完成事务执行。缺点是主库故障时,若未来得及同步数据,从库升级为主库会丢失事务变更内容。
  • 全同步复制:主库执行完事务后,等待所有从库完成数据复制才给客户端反馈。安全性高,但性能差,从库数量多会延长整个过程。
  • 半同步复制:介于前两者之间,主库执行完事务不立即给客户端反馈,而是等其中一个从库接收到事件后再反馈。在事务提交两阶段完成后,等从库接收 binlog 才返回成功 。

** 如果把半同步复制过程加入到整体事务流程中,流程如下**

img

如何解决主从同步的延迟?

数据库主从延迟指主从复制过程中从服务器与主服务器数据的时间差,常见原因如下:

  • 网络延迟:主从节点间网络不佳是常见致因。
  • 从节点性能问题:硬件资源(CPU、内存、磁盘)不足会使处理复制事件能力受限,进而增加延迟。
  • 复制线程不够:从节点线程少或不足会导致数据回放慢,引发主从数据延迟。

解决主从延迟可采取以下措施:

  • 优化网络:保证主从节点网络稳定,同城或同单元部署以降低延迟。
  • 提高从服务器性能:增加硬件资源提升处理能力。
  • 并行复制:利用 MySQL 并行复制功能提升效率、减少延迟。

并行复制的原理

在 MySQL 主从复制中,因从库单个 SQL 线程处理大量日志易导致主从延迟,MySQL 推出多种并行复制方案:

  • MySQL 5.6 库级别并行复制:基于 Schema(库)进行并行复制,每个库可拥有自己的复制线程来并行处理不同库的写入,提升性能。但多数业务为单库,该方案实用性欠佳,未获开发者和 DBA 认可。
  • MySQL 5.7 基于组提交的并行复制(MTS):组提交将多个事务的提交操作合并为批处理,减少磁盘 IO 和锁定开销。当多个事务进入 Prepare 阶段且锁无冲突(即修改不同行记录)时,可在从库用多个 SQL 线程并行执行组提交中的 SQL,提高主从复制效率,降低延迟。不过该方案依赖主库并行度,主库并发不高时可能无法进行组提交,也就无法使用并行复制优化。同时如果主库的SQL执行并没有那么频繁,那么时间间隔可能就会超过组提交的那两个参数阈值,就不会进行组提交。那么复制的时候就不能用并行复制了。
  • MySQL 8.0 基于 WRITESET 的并行复制:为解决 MySQL 5.7 方案的局限性而引入。即便主库串行提交事务,只要事务间不冲突,在从库就能并行回放。WRITESET 是使用 C++ STL 中 set 容器的集合,元素为行数据主键和唯一键的 hash 值(与指定算法有关)。通过检测事务更新记录的 hash 值是否冲突,判断能否并行回放,确保同一 write_set 中的变更不冲突,进而可通过多个线程并行回放 SQL 。

31. 如何处理 MySQL 的主从同步延迟?

32. MySQL 的 Doublewrite Buffer 是什么?它有什么作用?

1️⃣ 定义

Doublewrite Buffer 是 InnoDB 存储引擎中用于防止页写入损坏的一种机制。 它位于系统表空间中,大小为2MB,用于先写入一次、再写入两次(Double Write),保证数据页的持久性和一致性。

2️⃣ 作用

🔷 (1) 防止部分页写入失败

  • 如果数据库崩溃、断电,可能导致数据页只写入了一半(称为部分写),数据页损坏。

  • Doublewrite机制确保:

    • 数据页先写入Doublewrite Buffer并落盘(保存到Doublewrite Buffer Files)(顺序IO)
    • 从Buffer拷贝到数据文件(随机IO)
    • 崩溃后可以通过Doublewrite Buffer中完整页进行恢复。

\1) 写入 Doublewrite Buffer

当 InnoDB 需要将脏页(dirty page,即已被修改但尚未写入磁盘的页)写入磁盘时,首先将这些数据页写入到 Doublewrite Buffer 中。

2)写入实际数据文件:

将数据页写入 Doublewrite Buffer 和落盘后,InnoDB 将这些数据页从 Doublewrite Buffer 写入到实际的数据文件中(如 .ibd 文件)。

3)如果发生部分写的情况:

数据页写入一半断电了,在崩溃恢复的时候,InnoDB 会检查 Doublewrite Buffer 中的数据页。如果在系统崩溃前数据页已经成功写入 Doublewrite Buffer,那么这些数据页是完整和一致的。

🔷 (2) 提升数据页的一致性保障

  • 它主要保护数据页(16KB大小)免受磁盘故障、系统崩溃带来的损坏,而不保护日志

33. MySQL 中的 Log Buffer 是什么?它有什么作用?

1️⃣ 定义

Log Buffer 是 InnoDB 存储引擎中的内存缓冲区,专门用来暂存**重做日志(Redo Log)**的。 它的默认大小由参数 innodb_log_buffer_size 控制。

2️⃣ 作用

🔷 (1) 提升性能

  • 事务执行时,先写入 Log Buffer,而不是直接写磁盘
  • 这样可以批量刷新到磁盘,减少磁盘IO操作,提升整体写入性能。

🔷 (2) 保障事务持久性

  • 事务提交时,Log Buffer 中的数据刷新到磁盘的 Redo Log 文件ib_logfile0, ib_logfile1)。
  • 如果数据库崩溃,InnoDB 可以通过Redo Log恢复事务,实现**"持久性"(D in ACID)**。

34. 为什么在 MySQL 中不推荐使用多表 JOIN?

1️⃣ 性能下降

  • 多表 JOIN,尤其是大表 + 大表,容易导致临时表排序内存溢出,查询变慢。
  • JOIN 过程中容易全表扫描回表,CPU和IO压力大。

2️⃣ 索引利用率下降

  • 多表 JOIN 场景下,只有驱动表能完全利用索引,其他表往往索引利用受限,查询效率降低。

3️⃣ 复杂度提升

  • SQL逻辑复杂,维护难度高。
  • 业务侧难以理解,调优成本高。

MySQL底层多表JOIN用嵌套循环,小表套大表,数据量大时性能迅速下降。

🔥 详细理解

1️⃣ 什么是驱动表?

  • Nested Loop Join中,最先被扫描的表,叫驱动表
  • MySQL优化器会选择数据量小条件命中索引多的表作为驱动表。

✅ 驱动表可以这样查:

plsql
SELECT * 
FROM small_table s 
JOIN big_table b ON s.id = b.sid
WHERE s.status = 1;
  • 这里的 small_table 是驱动表。
  • 如果 s.status 有索引,它走索引扫描;没有索引,它就全表扫描

❗️误区:

驱动表 不等于 一定全表扫描。它能不能用索引,取决于你 WHERE 子句有没有命中索引。


2️⃣ 被驱动表呢?

  • 它是 for 循环里每次根据驱动表的一行,去查匹配的数据。

  • 这个查找过程一般是走索引

    • 如果 ON 条件字段有索引,被驱动表是利用索引高效查找。
    • 没有索引的话,就惨了,要每次都全表扫描(性能灾难)。

35. MySQL 中 TEXT 类型最大可以存储多长的文本?

1)TINYTEXT:最大长度为 255 字节。2^8-1

2)TEXT:最大长度为 65,535 字节(约 64 KB)。 2^16-1

3)MEDIUMTEXT:最大长度为 16,777,215 字节(约 16 MB)。2^24-1

4)LONGTEXT:最大长度为 4,294,967,295 字节(约 4 GB)。 2^32-1

36. MySQL中varchar(100)和varchar(10)的区别是什么?

两者的区别就是能存储的字符串长度上限不同,字符数上限是由定义中的括号内的数字决定的。

  • VARCHAR(100) 最多可以存储 100 个字符。
  • VARCHAR(10) 最多可以存储 10 个字符。

两者存储相同字符串时占用的空间是一样的。除了存储字符本身外,还需要额外的 1 或 2 个字节来记录字符串的长度。对于长度小于 255 的字符串,使用 1 个字节;对于长度 255 及以上的字符串,使用 2 个字节。

虽然存储的空间一样,但是在查询时,即带上 SORTORDER 这些字段时,VARCHAR(100) 字段会使得查询所占用的内存空间更多,因为在排序相关内存分配时,它是按照固定大小的内存块进行保存,VARCHAR(100) 的内存块会大于 VARCHAR(10)。

37.

✅ 先帮你回忆一遍RR隔离级别加锁规则

🔥 InnoDB的RR隔离级别下:

  • 当前读(如UPDATE):

    • 加行锁(Record Lock)
    • 加间隙锁(Gap Lock)
    • 实际效果:✅ 加的是Next-Key Lock(即“记录+前面的间隙”)

Next-Key Lock的定义:

plain
(前一个值, 当前值]  —— 锁住区间 + 当前行

✅ 现在来拆解你的这个SQL

plsql
UPDATE t SET b = 5 WHERE b = 3;

❓ 实际的InnoDB执行逻辑:

  • 扫描整个b索引
  • 每一个扫描到的索引区间Next-Key Lock
  • 遇到b=3的记录时,执行UPDATE操作;
  • 对扫到的每个区间都加锁,以防止“幻读”。

✅ 🔥 为什么会锁那么多区间?

看你说的这个锁:

plsql
x-lock(1,2);  // 加排它锁,锁区间 (1,2]
x-lock(2,3);  // 锁区间 (2,3]
x-lock(3,2);  // 锁区间 (3,2] (这里可能打印顺序颠倒,其实是锁(2,3]或(3,4])
x-lock(4,3);  // 锁区间 (3,4]
x-lock(5,2);  // 锁区间 (4,5]

🔥 它锁这么多区间的根本原因

  • 索引扫描是范围扫描 ➡️ 它沿着B+树索引一段段区间向前扫描;
  • Next-Key Lock规则 ➡️ 每个区间都必须锁,防止其他事务在扫描过程中插入新记录(即防止幻读)。

✅ 🚀 为什么锁“不是只锁当前访问行”?

🔥 这是因为InnoDB执行UPDATE时:

  • 不只是“锁中命中的b=3的行”;

  • 还必须锁扫描过程路过的所有区间,因为:

    • InnoDB认为事务隔离要防止其他事务插入新行 ➡️ 必须对扫描过的区间加锁;
    • 这叫做**“范围锁定”,是RR隔离级别的防幻读机制**。

所以RR级别下加锁范围 = 扫描范围

  • ❌ 不是“条件匹配范围”
  • ✅ 是索引扫描路径范围 ➡️ 这个范围会更大!

✅ 🔥 进一步帮你脑补一张“锁加法”的心智模型:

🔒 在RR隔离级别下:

  • UPDATE t SET b=5 WHERE b=3 实际相当于:
    1. b 的索引范围扫描 ➡️ 扫描b <= 3的所有索引区间;
    2. 扫描路径Next-Key Lock ➡️ 锁 (前值, 当前值]
    3. 防止其他事务在这些区间插入新行 ➡️ 防止幻读。

✅ 🔥 一句话终极总结

RR隔离级别 下,InnoDB对UPDATE ... WHERE b=3加锁范围 = 整个索引扫描范围,而不仅仅是“b=3”的行; 因为 Next-Key Lock 机制要防止其他事务在扫描路径中插入新幻行 ➔ 所以加锁范围比你想象的更大


✅ 🟢 口诀帮你秒记

RR不只锁命中,还锁扫描路Next-Key防插入,扫过皆加锁