1、MySQL的日志:
1.1、MySQL的执行流程:

这里就是一个最典型的执行流程,例如:SQL语句在执行update ... where id = 1的这行SQL语句时,磁盘数据页会先将数据以页的形式加载到buffer pool里面,然后在修改前记录undolog快照。这时候在从buffer pool里面准备执行二阶段提交最后成功后返回成功同时异步刷盘到磁盘数据页里面。具体的功能后面慢慢详细解释。
1.2、undolog详讲
1.2.1、什么是undolog?有什么用?
MySQL有事务,其的特性有原子性。要么全部执行成功、要么全部执行失败,后面的SQL执行失败了也要进行回滚。这时候就需要去保存数据的不同版本了,因为回滚需要去记录不同的数据版本。每修改一次就把原始的数据版本保存下来。会当前版本的指针会指向上一个版本,这个就是undolog版本链。

第一个作用:在事务执行的过程中修改的数据MySQL会把旧数据记录到undolog日志里面,如果事务正常提交,那就结束,如果说事务因为异常或者执行了手动回滚这种操作那就通过undolog找到旧版本的数据去进行回滚。
第二个作用就是配合read view和表的隐藏字段去实现MVCC(下面会详细讲解)。MVCC去执行普通的select语句就会去对比事务id和read view来查看数据版本对当前事务是否可见。不可见就沿着undolog的版本链继续查找当前事务可见的一个数据版本。
1.2.2、buffer pool讲解
当我们写业务代码的时候感觉查数据太慢了怎么办呢?我们就会去加缓存、一级缓存、二级缓存等等。在MySQL里面呢也是这样的设计了一个缓冲池,叫buffer pool,MySQL存数据是以页为单位的,一个页是16KB每查询一条数据会把硬盘这一页的数据加载出来并将这个数据页加载到buffer pool里面去。那么查数据我们将这条数据放入缓存就行了哇,不至于将整个数据页读取出来放到这个buffer pool里面中吧,这个就是操作系统中的程序有空间局部性原理。也就是说一个空间位置被访问了这就意味着未来很可能附近的内存位置都会被访问,因为代码里会大量使用数组这种连续结构,那数组第一个位置被访问了那第二个位置、第三个位置大概率也还会被访问,同样的一些管理系统里面分页展示了很多数据,那你看完一页的数据还会接着点下一页、下一页这就是连续翻页的过程。MySQL一次把一个页里面的数据放入到buffer pool中,对于取连续数据的来说命中率就很高了,这就是利用了程序的一个空间局部性原理,后续取数据从buffer pool中取,没有再取磁盘里面读,写数据也是先写入buffer pool但是不hi立刻刷盘,而是把这个页标记为脏页,然后由后台线程在某一个时间呢把这个脏页刷盘。
1.2.3、redolog讲解:
MySQL每次把某个磁盘页做了什么样的修改,记录到redolog中,然后呢,事务提交之后刷盘redolog,如果说buffer pool中的数据未刷盘就宕机了,那么就可以读取redolog来恢复数据,这就是redolog能够保证事务的持久性,让MySQL能够做到崩溃恢复。
(1)为什么修改buffer pool之后,他不直接刷盘呢?而是去写redolog,去刷盘redolog呢?
因为buffer pool刷盘是随机IO,刷盘的时候需要找到某个磁盘页,然后去修改再去找到另外一个磁盘页再去修改。那本来磁盘操作就很慢,这随机IO就更慢。redolog只记录在哪个磁盘位置做了怎么样的修改,那刷盘的时候只需要往redolog日志文件里面追加就行了,这就是顺序IO,不需要去找一个又一个的磁盘页,它磁头往一个往一个方向去移动就可以了。这就是为什么redolog里面要记录数据页的物理修改,而不是直接去记录数据,所以只要redolog一刷盘,即便MySQL崩溃了数据也能恢复。事务的持久性也就得到了保证。
(2)如果redolog没刷盘,MySQL就已经崩溃了怎么办?
redolog其实也不是自己就写入磁盘的,它也有自己的缓存(redolog buffer)。redolog会写入redolog buffer里面,然后统一把redolog buffer中的数据刷盘到磁盘中,那么MySQL中有个参数叫innodb_flash_log_and_trx_commit。这个参数就是控制redolog的刷盘时间,如果这个参数的值为0那事务提交的时候就会把redolog的数据放到redolog buffer但并不会刷盘,所以这就比较危险(因为只是放入缓冲池中,如果MySQL宕机redolog就会丢失数据),如果值为1,那事务提交的时候就会把redolog数据刷入磁盘,刷盘完成之后才告诉客户端事务执行成功了,这就能够保证事务只要完成即便MySQL崩溃了,数据也不会丢。所以一般情况下把这个参数设置为1是比较稳妥的。如果值为2,那事务提交的时候就会把redolog的数据写入操作系统的文件缓存里面也就是page cache。操作系统本身对文件也是有缓存的,这个缓存就是page cache,数据写入page cache之后操作系统会在某一个时间,真正的把这个数据写入磁盘中所以只要操作系统不崩溃,那就能保证持久性。但是如果说电脑突然断电了,那数据还是会丢的。说到这里你就会发现计算机全是缓存,处处都是缓存。
(3)redolog能用来做备份恢复吗?主从复制能用redolog吗?
不行,redolog是循环写,就日志空间的大小是固定的,全部写满就只能从头开始写,边写边擦除,只能记录事务提交之后没有被刷盘哪个数据,那已经刷盘的会从redolog中慢慢擦除掉。所以redolog它是事务级的数据记录,它不是数据库级别的数据记录。它只能做那些因为断电或者数据库故障导致buffer pool数据未刷盘的这种数据恢复,它不能做数据库全量的数据恢复。那咋办呢?要做备份恢复或者要做主从同步应该用binlog(追加写)。
1.2.4、binlog讲解
binlog是记录的全量日志,所以binlog更适合用来做备份恢复或者主从同步。redolog是物理日志,记录的是某个数据页上做的什么样的修改,binlog是逻辑日志它记录的内容其实就类似于SQL语句本身,所以binlog就非常使用于做这种备份恢复或者主从同步。binlog也有对应的缓存叫binlog cache,事务的执行过程中会先把这个binlog日志写到这个binlog cache,事务提交的时候就会把binlog cache刷到磁盘中。做备份什么的就得用日志来进行恢复。
(1)如果redolog和binlog中有一个刷盘失败了改怎么办?(日志就不一致了)
为了去解决两份日志的数据不一致的问题,MySQL就用了prepare和commit,就是两阶段提交。
1.2.5、两阶段提交
大致的执行流程如下:

如果说写入redolog,那就是prepare异常了,redolog、binlog都没有就直接回滚事务,事务根本就没有提交成功。
如果是写入binlog异常呢?MySQL就根据redolog进行数据恢复的时候,这是redolog处于prepare阶段并且它没有对应的binlog日志,回滚事务。
如果说redolog设置了commit时异常了,那MySQL就会发现redolog是prepare阶段,但是能找到对应的binlog日志,那binlog和redolog就是一致的所以MySQL也会认为数据是完整的所以直接提交事务。两阶段提交最终还是看binlog,只要binlog刷盘了那就能提交事务。
(1)buffer pool是什么时候进行异步刷盘的?
在事务提交成功并返回"成功"给客户端之后,由后台线程异步进行脏页刷盘。buffer pool异步刷盘没有准确的时间, 异步刷盘取决于后台进程,redo log是为了防止buffer pool中的数据丢失准备持久化方案,当redo log日志满了的情况下,也会主动触发脏页刷新到磁盘。(1)BP内存不足,要淘汰页面;(2)脏页占比超阈值(默认75%);(3)redo日志快要写满,触发模糊检查点;(4)数据库空闲,后台慢悠悠刷。
1.2.6、其他日志
(1)错误日志(Error Log):记录MySQL服务器启动、运行或停止时出现的问题。
(2)慢查询日志(Slow Query Log):记录执行时间超过long_query_time值的所有SQL语句。这个时间值是可配置的,默认情况下,慢查询日志功能是关闭的。
(3)一般查询日志(General Query Log):记录MySQL服务器的启动关闭信息,客户端的连接信息,以及更新、查询的SQL语句等。
2、SQL优化:
2.1、什么是慢SQL?
MySQL中有一个叫long_query_time的参数,原则上执行时间超过该参数值的SQL就是慢 SQL,会被记录到慢查询日志中。
可通过show variables like 'long_query_time'; 查看当前的long_query_time的参数值。

2.2、SQL的执行过程了解吗?
了解。
SQL的执行过程大致可以分为六个阶段:连接管理、语法解析、语义分析、查询优化、执行器调度、存储引擎读写等。Server层负责理解和规划SQL怎么执行,存储引擎层负责数据的真正读写。

来详细拆解一下:
1、客户端发送SQL语句给MySQL服务器。
2、如果查询缓存打开则会优先查询缓存,缓存中有对应的结果就直接返回。不过,MySQL 8.0已经移除了查询缓存。这部分的功能正在被Redis等缓存中间件取代。
3、分析器对SQL语句进行语法分析,判断是否有语法错误。
4、搞清楚SQL语句要干嘛后,MySQL会通过优化器生成执行计划。
5、执行器调用存储引擎的接口,执行SQL语句。
SQL执行过程中,优化器通过成本计算预估出执行效率最高的方式,基本的预估维度为:
①IO成本:从磁盘读取数据到内存的开销。
②CPU成本:CPU处理内存中数据的开销。
基于这两个维度,可以得出影响SQL执行效率的因素有:
①IO成本,数据量越大,IO成本越高。所以要尽量查询必要的字段;尽量分页查询;尽量通过索引加快查询。
②CPU成本,尽量避免复杂的查询条件,如有必要,考虑对子查询结果进行过滤。
2.3、如何优化慢SQL呢?
首先,需要找到那些比较慢的SQL,可以通过启用慢查询日志,记录那些超过指定执行时间的SQL查询。
也可以使用show processlist; 命令查看当前正在执行的SQL语句,找出执行时间较长的SQL。

或者在业务基建中加入对慢SQL的监控,常见的方案有字节码插桩、连接池扩展、ORM框架扩展等。

然后,使用EXPLAIN查看慢SQL的执行计划,看看有没有用索引,大部分情况下,慢SQL的原因都是因为没有用到索引。
sql
EXPLAIN SELECT * FROM your_table WHERE conditions;
最后,根据分析结果,通过添加索引、优化查询条件、减少返回字段等方式进行优化。
2.4、慢sql日志怎么开启?
编辑MySQL的配置文件my.cnf,设置slow_query_log参数为1
sql
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2 # 记录执行时间超过2秒的查询
然后重启MySQL就好了。
也可以通过set global命令动态设置。
sql
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 2;
2.5、你知道哪些方法来优化SQL?
SQL优化的方法非常多,但本质上就一句话:尽可能少地扫描、尽快地返回结果。
最常见的做法就是加索引、改写SQL让它用上索引,比如说使用覆盖索引、让联合索引遵守最左前缀原则等。

2.5.1、如何利用覆盖索引?
覆盖索引的核心是"查询所需的字段都在同一个索引里",这样MySQL就不需要回表,直接从索引中返回结果。

实际使用中,我会优先考虑把WHERE和SELECT涉及的字段一起建联合索引,并通过EXPLAIN观察结果是否有Using index,确认命中索引。
举个例子,现在要从test表中查询city为上海的name字段。
sql
select name from test where city='上海'
如果仅在city字段上添加索引,那么这条查询语句会先通过索引找到city为上海的行,然后再回表查询name字段。
为了避免回表查询,可以在city和name字段上建立联合索引,这样查询结果就可以直接从索引中获取。
sql
alter table test add index index1(city,name);
2.5.2、如何正确使用联合索引?
使用联合索引最重要的一条是遵守最左前缀原则,也就是查询条件需要从索引的左侧字段开始。
比如说我们创建了一个三列的联合索引。
sql
CREATE INDEX idx_name_age_sex ON user(name, age, sex);
我们来看一下什么样的查询条件可以用到这个索引:
| 查询条件 | 能否用上 idx_name_age_sex? | 说明 |
|---|---|---|
| WHERE name = 'itwanger' | ✅ 可以 | 匹配第一列,命中索引 |
| WHERE name = 'itwanger' AND age=20 | ✅ 可以 | 匹配前两列,命中索引 |
| WHERE age = 20 | ❌ 不行 | 第一列没用上,索引失效 |
| WHERE name='itwanger' AND sex='女' | ✅ 部分可用(只用前一列) | age 被跳过,后面的列无法使用 |
| WHERE name LIKE 'it%' | ✅ 可以(前缀匹配) | name 是前缀匹配,不影响使用 |
| WHERE name LIKE '%wanger%' | ❌ 不行 | 通配符在前,不能用索引 |
2.5.3、如何进行分页优化?
分页优化的核心是避免深度偏移带来的全表扫描,可以通过两种方式来优化:延迟关联和添加书签。
延迟关联适用于需要从多个表中获取数据且主表行数较多的情况。它首先从索引表中检索出需要的行ID,然后再根据这些ID去关联其他的表获取详细信息。
sql
SELECT e.id, e.name, d.details
FROM employees e
JOIN department d ON e.department_id = d.id
ORDER BY e.id
LIMIT 1000, 20;
延迟关联后,第一步只查主键,速度快,第二步只处理20条数据,效率高。
sql
SELECT e.id, e.name, d.details
FROM (
SELECT id
FROM employees
ORDER BY id
LIMIT 1000, 20
) AS sub
JOIN employees e ON sub.id = e.id
JOIN department d ON e.department_id = d.id;
添加书签的方式是通过记住上一次查询返回的最后一行主键值,然后在下一次查询的时候从这个值开始,从而跳过偏移量计算,仅扫描目标数据,适合翻页、资讯流等场景。
假设需要对用户表进行分页。
sql
SELECT id, name
FROM users
ORDER BY id
LIMIT 1000, 20;
通过添加书签来优化后,查询不再使用OFFSET,而是从上一页最后一个用户的ID开始查询。这种方法可以有效避免不必要的数据扫描,提高了分页查询的效率。
sql
SELECT id, name
FROM users
WHERE id > last_max_id -- 假设last_max_id是上一页最后一行的ID
ORDER BY id
LIMIT 20;
2.5.4、为什么分页会变慢?
分页查询的效率问题主要是由于OFFSET的存在,OFFSET会导致MySQL必须扫描和跳过offset + limit条数据,这个过程是非常耗时的。
比如说,我们要查询第100000条数据,那么MySQL就必须扫描100000条数据,然后再返回 10 条数据。
sql
SELECT * FROM user ORDER BY id LIMIT 100000, 10;
数据越多、偏移越大,就越慢!
2.5.5、JOIN代替子查询有什么好处?
第一,JOIN的ON条件能更直接地触发索引,而子查询可能因嵌套导致索引失效。
第二,JOIN的一次连接操作替代了子查询的多次重复执行,尤其在大数据量的情况下性能差异明显。
比如说我们有两个表orders和customers。
sql
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
amount DECIMAL(10,2),
INDEX idx_customer_id (customer_id) -- customer_id字段有索引
);
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100)
);
子查询的写法:
sql
SELECT o.order_id, o.amount,
(SELECT c.name
FROM customers c
WHERE c.customer_id = o.customer_id) AS customer_name
FROM orders o;
JOIN的写法:
sql
SELECT o.order_id, o.amount, c.name AS customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
| 对比项 | 子查询 | JOIN |
|---|---|---|
| 索引使用 | 内层子查询 WHERE c.customer_id = o.customer_id 每次执行时,可能无法直接利用 orders 表的 customer_id 索引。 |
JOIN 的 ON 条件 o.customer_id = c.customer_id 可以直接利用 orders 的 idx_customer_id 索引,加速连接过程。 |
| 执行计划 | 子查询会被重复执行(每次外层 orders 行都会触发一次子查询),导致全表扫描。 | 优化器可能选择通过索引快速关联两张表,减少数据扫描量。例如,先通过 orders 的索引找到 customer_id,再与 customers 主键快速匹配。 |
| 性能表现 | 当 orders 表数据量大时,子查询可能因重复执行导致性能急剧下降。 | JOIN 的一次连接操作通常更高效,尤其在大数据量时。 |
对于子查询,执行流程是这样的:
①外层orders表的每一行都会触发一次子查询。
②如果orders表有1000条记录,则子查询会执行1000次。
③每次子查询都需要单独查询customers表(即使customer_id相同)。
而JOIN的执行流程是这样的:
①数据库优化器会将两张表的连接操作合并为一次执行。
②通过索引(如orders.customer_id 和 customers.customer_id)快速关联数据。
③仅执行一次关联操作,而非多次子查询。
来看一下子查询的执行计划:
sql
EXPLAIN SELECT o.order_id,
(SELECT c.name FROM customers c WHERE c.customer_id = o.customer_id)
FROM orders o;

子查询(DEPENDENT SUBQUERY)类型表明其依赖外层查询的每一行,导致重复执行。
再对比看一下JOIN的执行计划:
sql
EXPLAIN SELECT o.order_id,
(SELECT c.name FROM customers c WHERE c.customer_id = o.customer_id)
FROM orders o;

JOIN通过eq_ref类型直接利用主键(customers.customer_id)快速关联,减少扫描次数。
2.5.6、JOIN操作为什么要小表驱动大表?
第一,如果大表的JOIN字段有索引,那么小表的每一行都可以通过索引快速匹配大表。

时间复杂度为小表行数N乘以大表索引查找复杂度log(大表行数 M),总复杂度为N*log(M)。
显然小表做驱动表比大表做驱动表的时间复杂度M*log(N)更低。
第二,如果大表没有索引,需要将小表的数据加载到内存,再全表扫描大表进行匹配。

时间复杂度为小表分段数K乘以大表行数M,其中K=小表行数N/内存大小join_buffer_size。

显然小表做驱动表的时候K的值更小,大表做驱动表的时候需要多次分段。
sql
-- 小表驱动(高效)
SELECT * FROM small_table s
JOIN large_table l ON s.id = l.id; -- l.id有索引
-- 大表驱动(低效)
SELECT * FROM large_table l
JOIN small_table s ON l.id = s.id; -- s.id无索引
1、当使用left join时,左表是驱动表,右表是被驱动表。
2、当使用right join时,刚好相反。
3、当使用join时,MySQL会选择数据量比较小的表作为驱动表,大表作为被驱动表。
为了验证这一点,我特意新建了两个表departments和employees。

插入测试数据:
sql
-- 插入测试数据
INSERT INTO departments VALUES
(1, '研发部'),
(2, '市场部'),
(3, '人事部');
-- 插入更多数据到员工表
INSERT INTO employees VALUES
(1, '张三', 1),
(2, '李四', 1),
(3, '王二', 2),
(4, '赵六', 2),
(5, '钱七', 3),
(6, '孙八', NULL),
(7, '周九', 1),
(8, '吴十', 2);
然后用explain查看执行计划:

当使用left join的时候,第一行是employees表,说明左表是驱动表;当使用right join的时候,第一行是departments表,说明右表是驱动表;当使用join的时候,第一行是departments表,说明 MySQL默认选择了小表作为驱动表。
这里的小表指实际参与JOIN的数据量,而不是表的总行数。大表经过where条件过滤后也可能成为逻辑小表。
sql
-- 实际参与JOIN的数据量决定小表
SELECT * FROM large_table l
JOIN small_table s ON l.id = s.id
WHERE l.created_at > '2025-01-01'; -- l经过过滤后可能成为小表
也可以强制通过STRAIGHT_JOIN提示MySQL使用指定的驱动表。
sql
explain select table_1.col1, table_2.col2, table_3.col2
from table_1
straight_join table_2 on table_1.col1=table_2.col1
straight_join table_3 on table_1.col1 = table_3.col1;
explain select straight_join table_1.col1, table_2.col2, table_3.col2
from table_1
join table_2 on table_1.col1=table_2.col1
join table_3 on table_1.col1 = table_3.col1;
2.5.7、为什么要避免使用JOIN关联太多的表?
第一,多表JOIN的执行路径会随着表的数量呈现指数级增长,优化器需要估算所有路径的成本,有可能会导致出现大表驱动小表的情况。
sql
SELECT * FROM A
JOIN B ON A.id = B.a_id
JOIN C ON B.id = C.b_id
JOIN D ON C.id = D.c_id
JOIN E ON D.id = E.d_id; -- 5 个表,优化器需评估 5! = 120 种顺序
第二,多表JOIN需要缓存中间结果集,可能超出join_buffer_size,这种情况下内存临时表就会转为磁盘临时表,性能也会急剧下降。
2.5.8、如何进行排序优化?
第一,对ORDER BY涉及的字段创建索引,避免filesort。
sql
-- 优化前(可能触发 filesort)
SELECT * FROM users ORDER BY age DESC;
-- 优化后(添加索引)
ALTER TABLE users ADD INDEX idx_age (age);
如果是多个字段,联合索引需要保证ORDER BY的列是索引的最左前缀。
sql
-- 联合索引需与 ORDER BY 顺序一致(age 在前,name 在后)
ALTER TABLE users ADD INDEX idx_age_name (age, name);
-- 有效利用索引的查询
SELECT * FROM users ORDER BY age, name;
-- 无效案例(索引失效,因 name 在索引中排在 age 之后)
SELECT * FROM users ORDER BY name, age;
第二,可以适当调整排序参数,如增大sort_buffer_size、max_length_for_sort_data等,让排序在内存中完成。

①sort_buffer_size:用于控制排序缓冲区的大小,默认为256KB。也就是说,如果排序的数据量小于 256KB,MySQL会在内存中直接排序;否则就要在磁盘上进行filesort。
②max_length_for_sort_data:单行数据的最大长度,会影响排序算法选择。如果单行数据超过该值,MySQL会使用双路排序,否则使用单路排序。
③max_sort_length:限制字符串排序时比较的前缀长度。当MySQL不得不对text、blob字段进行排序时,会截取前max_sort_length个字符进行比较。
第三,可以通过where和limit限制待排序的数据量,减少排序的开销。
sql
-- 优化前
SELECT * FROM users ORDER BY age LIMIT 100;
-- 优化后(减少数据传输和排序开销)
SELECT id, name, age FROM users ORDER BY age LIMIT 100;
-- 深度分页优化(避免 OFFSET 扫描全表)
SELECT * FROM users ORDER BY age LIMIT 10000, 20; -- 低效
SELECT * FROM users WHERE age > last_age ORDER BY age LIMIT 20; -- 高效(记录上一页最后一条的 age 值)
2.5.9、什么是filesort?
当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量比较小,会在内存中进行;如果数据量比较大就需要写临时文件到磁盘再排序,我们将这个过程称为文件排序。

好,让我们来验证一下filesort的情况,建表、插入数据。

执行explain查看执行计划。

能够看得出来,当order by id也就是主键的时候,没有触发filesort;当order by age的时候,由于没有索引,就触发了filesort。
2.5.10、全字段排序和rowid排序了解多少?
当排序字段是索引字段且满足最左前缀原则时,MySQL可以直接利用索引的有序性完成排序。

当无法使用索引排序时,MySQL需要在内存或磁盘中进行排序操作,分为全字段排序和rowid排序两种算法。

全字段排序会一次性取出满足条件行的所有字段,然后在sort buffer中进行排序,排序后直接返回结果,无需回表。
以SELECT * FROM user WHERE name = "王二" ORDER BY age为例:
①从name索引中找到第一个满足name='张三'的主键 id;
②根据主键id取出整行所有的字段,存入sort buffer;
③重复上述过程直到处理完所有满足条件的行
④对sort buffer中的数据按age排序,返回结果。
优点是仅需要一次磁盘IO,缺点是内存占用大,如果数量超过sort buffer的话,需要分片读取并借助临时文件合并排序,IO次数反而会增加。
也无法处理包含text和blob类型的字段。

rowid排序分为两个阶段:
第一阶段:根据查询条件取出排序字段和主键ID,存入sort buffer进行排序;
第二阶段:根据排序后的主键ID回表取出其他需要的字段。
同样以SELECT * FROM user WHERE name = "王二" ORDER BY age为例:
①从name索引中找到第一个满足name='张三' 的主键id;
②根据主键id取出排序字段age,连同主键id一起存入sort buffer;
③重复上述过程直到处理完所有满足条件的行
④对sort buffer中的数据按age排序;
⑤遍历排序后的主键id,回表取出其他所需字段,返回结果。
优点是内存占用较少,适合字段多或者数据量大的场景,缺点是需要两次磁盘IO。
MySQL会根据系统变量max_length_for_sort_data和查询字段的总大小来决定使用全字段排序还是rowid排序。
如果查询字段总长度<= max_length_for_sort_data,MySQL 会使用全字段排序;否则会使用 rowid排序。
2.5.11、为什么要尽量避免使用 select *?
SELECT *会强制MySQL读取表中所有字段的数据,包括应用程序可能并不需要的,比如text、blob类型的大字段。
加载冗余数据会占用更多的缓存空间,从而挤占其他重要数据的缓存资源,降低整体系统的吞吐量。
也会增加网络传输的开销,尤其是在大字段的情况下。
最重要的是,SELECT *可能会导致覆盖索引失效,本来可以走索引的查询最后变成了全表扫描。
sql
-- 使用覆盖索引(假设索引为 idx_country)
SELECT id, country FROM users WHERE country = 'china'; -- 可能仅扫描索引
-- 使用 SELECT *
SELECT * FROM users WHERE country = 'china'; -- 需回表读取所有列
2.5.12、你还知道哪些SQL优化方法?
①避免使用!=或者<>操作符
!=或者<>操作符会导致MySQL无法使用索引,从而导致全表扫描。
可以把column<>'aaa',改成column>'aaa' or column<'aaa'。
②使用前缀索引
比如,邮箱的后缀一般都是固定的@xxx.com,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引:
sql
alter table test add index index2(email(6));
需要注意的是,MySQL无法利用前缀索引做order by和group by操作。
③避免在列上使用函数
在where子句中直接对列使用函数会导致索引失效,因为MySQL需要对每行的列应用函数后再进行比较。
sql
select name from test where date_format(create_time,'%Y-%m-%d')='2021-01-01';
可以改成:
sql
select name from test where create_time>='2021-01-01 00:00:00' and create_time<'2021-01-02 00:00:00';
通过日期的范围查询,而不是在列上使用函数,可以利用create_time上的索引。
2.6、explain平常有用过吗?
经常用,explain是MySQL提供的一个用于查看SQL执行计划的工具,可以帮助我们分析查询语句的性能问题。
一共有10来个输出参数。

比如说type=ALL,key=NULL表示SQL正在全表扫描,可以考虑为where字段添加索引进行优化;Extra=Using filesort表示SQL正在文件排序,可以考虑为order by字段添加索引。
使用方式也非常简单,直接在select前加上explain关键字就可以了。
sql
explain select * from students where name='王二';
更高级的用法可以配合format=json参数,将explain的输出结果以JSON格式返回。
sql
explain format=json select * from students where name='王二';
2.6.1、explain输出结果中常见的字段含义理解吗?
在EXPLAIN输出结果中我最关注的字段是type、key、rows 和 Extra。
我会通过它们判断SQL有没有走索引、是否全表扫描、预估扫描行数是否太大,以及是否触发了filesort或临时表。一旦发现问题,比如type=ALL或者Extra=Using filesort,我会考虑建索引、改写SQL或控制查询结果集来做优化。
3、数据库的架构:
3.1、说说MySQL的基础架构?
MySQL采用分层架构,主要包括连接层、服务层、和存储引擎层。

①连接层主要负责客户端连接的管理,包括验证用户身份、权限校验、连接管理等。可以通过数据库连接池来提升连接的处理效率。
②服务层是MySQL的核心,主要负责查询解析、优化、执行等操作。在这一层,SQL语句会经过解析、优化器优化,然后转发到存储引擎执行,并返回结果。这一层包含查询解析器、优化器、执行计划生成器、日志模块等。
③存储引擎层负责数据的实际存储和提取。MySQL支持多种存储引擎,如InnoDB、MyISAM、Memory等。
3.1.1、binlog写入在哪一层?
binlog在服务层,负责记录SQL语句的变化。它记录了所有对数据库进行更改的操作,用于数据恢复、主从复制等。
3.2、一条查询语句是如何执行的?
当我们执行一条SELECT语句时,MySQL并不会直接去磁盘读取数据,而是经过6个步骤来解析、优化、执行,然后再返回结果。

第一步,客户端发送SQL查询语句到MySQL服务器。
第二步,MySQL服务器的连接器开始处理这个请求,跟客户端建立连接、获取权限、管理连接。
第三步,解析器对SQL语句进行解析,检查语句是否符合SQL语法规则,确保数据库、表和列都是存在的,并处理SQL语句中的名称解析和权限验证。
第四步,优化器负责确定SQL语句的执行计划,这包括选择使用哪些索引,以及决定表之间的连接顺序等。
第五步,执行器会调用存储引擎的API来进行数据的读写。
第六步,存储引擎负责查询数据,并将执行结果返回给客户端。客户端接收到查询结果,完成这次查询请求。
3.3、一条更新语句是如何执行的?
总的来说,一条UPDATE语句的执行过程包括读取数据页、加锁解锁、事务提交、日志记录等多个步骤。

拿update test set a=1 where id=2举例来说:
在事务开始前,MySQL需要记录undo log,用于事务回滚。
| 操作 | id | 旧值 | 新值 |
|---|---|---|---|
| update | 2 | N | 1 |
除了记录undo log,存储引擎还会将更新操作写入redo log,状态标记为prepare,并确保 redo log持久化到磁盘。这一步可以保证即使系统崩溃,数据也能通过redo log恢复到一致状态。
写完redo log后,MySQL会获取行锁,将a的值修改为1,标记为脏页,此时数据仍然在内存的buffer pool中,不会立即写入磁盘。后台线程会在适当的时候将脏页刷盘,以提高性能。
最后提交事务,redo log中的记录被标记为committed,行锁释放。
如果MySQL开启了 binlog,还会将更新操作记录到binlog中,主要用于主从复制。
以及数据恢复,可以结合redo log进行点对点的恢复。binlog的写入通常发生在事务提交时,与redo log共同构成"两阶段提交",确保两者的一致性。
注意,redo log的写入有两个阶段的提交,一是binlog写入之前prepare状态的写入,二是 binlog写入之后commit状态的写入。
3.4、说说MySQL的段区页行
MySQL是以表的形式存储数据的,而表空间的结构则由段、区、页、行组成。

①段:表空间由多个段组成,常见的段有数据段、索引段、回滚段等。
创建索引时会创建两个段,数据段和索引段,数据段用来存储叶子节点中的数据;索引段用来存储非叶子节点的数据。
回滚段包含了事务执行过程中用于数据回滚的旧数据。
②区:段由一个或多个区组成,区是一组连续的页,通常包含64个连续的页,也就是1M的数据。
使用区而非单独的页进行数据分配可以优化磁盘操作,减少磁盘寻道时间,特别是在大量数据进行读写时。
③页:页是InnoDB存储数据的基本单元,标准大小为16 KB,索引树上的一个节点就是一个页。
也就意味着数据库每次读写都是以16 KB为单位的,一次最少从磁盘中读取16KB的数据到内存,一次最少写入16KB的数据到磁盘。
④行:InnoDB采用行存储方式,意味着数据按照行进行组织和管理,行数据可能有多个格式,比如说COMPACT、REDUNDANT、DYNAMIC 等。
MySQL 8.0 默认的行格式是 DYNAMIC,由COMPACT 演变而来,意味着这些数据如果超过了页内联存储的限制,则会被存储在溢出页中。
可以通过show table status like '%article%'查看行格式。

4、数据库存储引擎:
MySQL底层存储数据主要是通过数据页(固定16KB)来进行存储的。
假如说数据库1000W数据,update where id in (1,1777,535345),随机写、很慢、非常慢。
随机写(update很多数据,但是数据肯定分散在磁盘的不同位置)很慢。顺序写 (一直往后写,不用考虑位置了) 很快,写内存最快。
4.1、MySQL有哪些常见存储引擎?
MySQL支持多种存储引擎,常见的有MyISAM、InnoDB、MEMORY等。

我来做一个表格对比:
| 功能 | InnoDB | MyISAM | MEMORY |
|---|---|---|---|
| 支持事务 | Yes | No | No |
| 支持全文索引 | Yes | Yes | No |
| 支持 B+树索引 | Yes | Yes | Yes |
| 支持哈希索引 | Yes | No | Yes |
| 支持外键 | Yes | No | No |
除此之外,我还了解到:
①MySQL5.5之前,默认存储引擎是 MyISAM,5.5之后是InnoDB。
②InnoDB支持的哈希索引是自适应的,不能人为干预。
③InnoDB从 MySQL5.6开始,支持全文索引。
④InnoDB的最小表空间略小于10M,最大表空间取决于页面大小。

如何切换MySQL的数据引擎?
可以通过alter table语句来切换MySQL的数据引擎。
sql
ALTER TABLE your_table_name ENGINE=InnoDB;
4.2、存储引擎应该怎么选择?
大多数情况下,使用默认的InnoDB就可以了,InnoDB可以提供事务、行级锁、外键、B+树索引等能力。
MyISAM适合读多写少的场景。
MEMORY适合临时表,数据量不大的情况。因为数据都存放在内存,所以速度非常快。
4.3、InnoDB和MyISAM主要有什么区别?
InnoDB和MyISAM的最大区别在于事务支持和锁机制。InnoDB支持事务、行级锁,适合大多数业务系统;而MyISAM不支持事务,用的是表锁,查询快但写入性能差,适合读多写少的场景。

另外,从存储结构上来说,MyISAM用三种格式的文件来存储,.frm文件存储表的定义;.MYD存储数据;.MYI存储索引;而InnoDB用两种格式的文件来存储,.frm文件存储表的定义;.ibd存储数据和索引。
从索引类型上来说,MyISAM为非聚簇索引,索引和数据分开存储,索引保存的是数据文件的指针。

InnoDB为聚簇索引,索引和数据不分开。

更细微的层面上来讲,MyISAM不支持外键,可以没有主键,表的具体行数存储在表的属性中,查询时可以直接返回;InnoDB支持外键,必须有主键,具体行数需要扫描整个表才能返回,有索引的情况下会扫描索引。
4.4、InnoDB的内存结构了解吗?
InnoDB的内存区域主要有两块,buffer pool和log buffer。 buffer pool用于缓存数据页和索引页,提升读写性能;log buffer用于缓存redo log,提升写入性能。

4.5、数据页的结构了解吗?
InnoDB的数据页由7部分组成,其中文件头、页头和文件尾的大小是固定的,分别为38、56和8个字节,用来标记该页的一些信息。行记录、空闲空间和页目录的大小是动态的,为实际的行记录存储空间。

来个表格总结下:
| 名称 | 中文名 | 大小(单位:B) | 描述 |
|---|---|---|---|
| File Header | 文件头部 | 38 | 页的一些通用信息 |
| Page Header | 页面头部 | 56 | 数据页专有的一些信息 |
| Infimum + Supermum | 最小记录和最大记录 | 26 | 两个虚拟的行记录 |
| User Records | 用户真实记录 | 不确定 | 实际存储的行记录内容 |
| Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| File Trailer | 文件尾部 | 8 | 校验页是否完整 |
真实的记录会按照指定的行格式存储到User Records中。

每个数据页的File Header都有一个上一页和下一页的编号,所有的数据页会形成一个双向链表。

在InnoDB中,默认的页大小是16KB。可以通过show variables like 'innodb_page_size'; 查看。

4.6、InnoDB的Buffer Pool了解吗?
Buffer Pool是InnoDB存储引擎中的一个内存缓冲区,它会将经常使用的数据页、索引页加载进内存,读的时候先查询Buffer Pool,如果命中就不用访问磁盘了。

如果没有命中,就从磁盘读取,并加载到Buffer Pool,此时可能会触发页淘汰,将不常用的页移出Buffer Pool。

写操作时不会直接写入磁盘,而是先修改内存中的页,此时页被标记为脏页,后台线程会定期将脏页刷新到磁盘。
Buffer Pool可以显著减少磁盘的读写次数,从而提升MySQL的读写性能。
4.7、Buffer Pool的默认大小是多少?
我本机上InnoDB的Buffer Pool默认大小是128MB。
sql
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
另外,在具有1GB-4GB RAM的系统上,默认值为系统RAM的25%;在具有超过4GB RAM的系统上,默认值为系统RAM的50%,但不超过4GB。

4.8、InnoDB对LRU算法的优化了解吗?
了解,InnoDB对LRU算法进行了改良,最近访问的数据并不直接放到LRU链表的头部,而是放在一个叫midpoiont的位置。默认情况下,midpoint位于LRU列表的5/8处。

比如Buffer Pool有100页,新页插入的位置大概是在第80页;当页数据被频繁访问后,再将其移动到young区,这样做的好处是热点页能长时间保留在内存中,不容易被挤出去。
可以通过innodb_old_blocks_pct参数来调整Buffer Pool中old和young区的比例;通过innodb_old_blocks_time参数来调整页在young区的停留时间。

默认情况下,LRU链表中old区占37%;同一页再次访问提升的最小时间间隔是1000毫秒。
也就是说,如果某页在1秒内被多次访问,只会计算一次,不会立刻升级为热点页,防止短时间批量访问导致缓存污染。
5、MVCC(多版本并发控制):
5.1、多版本并发控制
多版本是指:一行数据的版本链、并发一般指具体的业务场景、控制:控制读取的结果。
版本链是个什么概念:一条数据记录不同的版本,undolog、MySQL对每行数据的隐藏字段。trx_id和指向上个版本的指针。


这里的max_id(最大事务id )怎么理解?因为事务的id是会慢慢变大的,所以这里的最大事务id就是指的全局下一个事务。

只要Read View(读视图)确定了那么读的范围也就确定了,那么就可以可重复读了。当然这里其实还有个问题,就是可重复读和读已提交他们都有ReadView为什么可重复读能保证呢?因为可重复读它只是在第一次查询的时候生成ReadView,而读已提交是每一次查询基本上都会去生成一个ReadView。这样依赖ReadView不再固定,就能读到最新的数据。例如:重新更新一下就是把max_id更新到106,这样我trx_id = 105就从一开始的不能读到到能读到了。
MVCC解决的是旧数据下的幻读情况(快照读),而间隙锁主要解决的是最新数据下的幻读情况(当前读)。
