MySQL 性能调优面试复习:Explain、索引、慢查询、缓存和架构优化

文章目录

    • 前言
    • [一、先用 EXPLAIN 定位问题](#一、先用 EXPLAIN 定位问题)
      • [1. possible_keys 和 key 的区别](#1. possible_keys 和 key 的区别)
      • [2. key_len 看联合索引用得深不深](#2. key_len 看联合索引用得深不深)
      • [3. rows 看扫描量大不大](#3. rows 看扫描量大不大)
    • [二、重点看 type:扫描方式决定效率](#二、重点看 type:扫描方式决定效率)
      • [1. ALL:全表扫描](#1. ALL:全表扫描)
      • [2. index:全索引扫描](#2. index:全索引扫描)
      • [3. range:索引范围扫描](#3. range:索引范围扫描)
      • [4. ref:非唯一索引等值查询](#4. ref:非唯一索引等值查询)
      • [5. eq_ref:唯一索引关联查询](#5. eq_ref:唯一索引关联查询)
      • [6. const:主键或唯一索引查一行](#6. const:主键或唯一索引查一行)
    • [三、Extra 里三个高频信号](#三、Extra 里三个高频信号)
      • [1. Using filesort:额外排序](#1. Using filesort:额外排序)
      • [2. Using temporary:使用临时表](#2. Using temporary:使用临时表)
      • [3. Using index:覆盖索引](#3. Using index:覆盖索引)
    • 四、索引优化:不要只会说"加索引"
      • [1. 哪些字段适合建索引](#1. 哪些字段适合建索引)
      • [2. 哪些字段不适合建索引](#2. 哪些字段不适合建索引)
      • [3. 联合索引要遵循最左匹配原则](#3. 联合索引要遵循最左匹配原则)
      • [4. 联合索引字段顺序怎么排](#4. 联合索引字段顺序怎么排)
    • 五、常见索引失效场景
      • [1. 左模糊查询](#1. 左模糊查询)
      • [2. 对索引列使用函数](#2. 对索引列使用函数)
      • [3. 对索引列做表达式计算](#3. 对索引列做表达式计算)
      • [4. 隐式类型转换](#4. 隐式类型转换)
      • [5. OR 使用不当](#5. OR 使用不当)
      • [6. 范围查询后的字段](#6. 范围查询后的字段)
    • [六、如果 MySQL 选错索引怎么办](#六、如果 MySQL 选错索引怎么办)
      • [1. USE INDEX](#1. USE INDEX)
      • [2. FORCE INDEX](#2. FORCE INDEX)
      • [3. IGNORE INDEX](#3. IGNORE INDEX)
    • [七、查询优化:少查、少回表、少 JOIN](#七、查询优化:少查、少回表、少 JOIN)
      • [1. 避免 SELECT *](#1. 避免 SELECT *)
      • [2. 尽量使用覆盖索引](#2. 尽量使用覆盖索引)
      • [3. JOIN 要小结果集驱动大结果集](#3. JOIN 要小结果集驱动大结果集)
      • [4. 被驱动表关联字段要有索引](#4. 被驱动表关联字段要有索引)
      • [5. 高频 JOIN 可以考虑冗余字段](#5. 高频 JOIN 可以考虑冗余字段)
    • 八、分页优化:重点解决深分页
      • [1. 基于位置分页](#1. 基于位置分页)
      • [2. 先查 id 再回表](#2. 先查 id 再回表)
    • 九、表结构优化:大表和宽表要拆
      • [1. 垂直拆分宽表](#1. 垂直拆分宽表)
      • [2. 冷热数据分离](#2. 冷热数据分离)
      • [3. 分库分表](#3. 分库分表)
    • 十、缓存优化:读多写少优先考虑
      • [1. 缓存穿透](#1. 缓存穿透)
      • [2. 缓存击穿](#2. 缓存击穿)
      • [3. 缓存雪崩](#3. 缓存雪崩)
    • 十一、架构优化:读写分离和分库分表
      • [1. 主从复制](#1. 主从复制)
      • [2. 读写分离](#2. 读写分离)
      • [3. 分库分表解决什么](#3. 分库分表解决什么)
    • 十二、慢查询和锁等待怎么定位
      • [1. 开启慢查询日志](#1. 开启慢查询日志)
      • [2. 慢查询日志看什么](#2. 慢查询日志看什么)
      • [3. 用工具分析慢日志](#3. 用工具分析慢日志)
      • [4. 当前正在执行什么 SQL](#4. 当前正在执行什么 SQL)
      • [5. 什么是 MDL 锁](#5. 什么是 MDL 锁)
      • [6. 查 InnoDB 事务和锁等待](#6. 查 InnoDB 事务和锁等待)
      • [7. 常见锁等待原因](#7. 常见锁等待原因)
      • [8. 死锁怎么排查](#8. 死锁怎么排查)
      • [9. 面试怎么回答慢查询定位](#9. 面试怎么回答慢查询定位)
    • 十三、面试回答模板
    • 十四、复习速记版

前言

这篇文章系统梳理 MySQL 查询慢的排查和优化方法,读完你可以用一套清晰思路回答性能调优类面试题。

面试里问 MySQL 性能优化,最怕答成散点。

比如只说"加索引"。

这不够。

真正靠谱的回答,应该从 定位问题 开始,再到 索引优化、SQL 优化、分页优化、表结构优化、缓存和架构优化

核心目标就一句话:

让 MySQL 少扫描、少回表、少排序、少建临时表、少做无效 IO。


一、先用 EXPLAIN 定位问题

查询慢,不要上来就加索引。

第一步应该是看执行计划。

常用命令:

sql 复制代码
EXPLAIN SELECT * FROM user WHERE age = 18;

EXPLAIN 可以告诉你 MySQL 准备怎么执行这条 SQL。

重点看这些字段:

字段 含义 重点
possible_keys 可能使用的索引 优化器发现的候选索引
key 实际使用的索引 NULL 说明没用索引
key_len 实际使用的索引长度 常用于判断联合索引用到几列
rows 预估扫描行数 越大越危险
type 访问类型 判断查询效率的关键
Extra 额外信息 看排序、临时表、覆盖索引

1. possible_keys 和 key 的区别

possible_keys 表示 MySQL 认为可能用得上的索引。

key 表示最终真正用到的索引。

比如:

text 复制代码
possible_keys: idx_age
key: NULL
type: ALL

这说明:

  • idx_age 理论上可用;
  • 但优化器最终没用;
  • 查询还是全表扫描。

原因可能是数据量小、索引区分度低、回表成本高,或者统计信息不准确。

2. key_len 看联合索引用得深不深

假设有联合索引:

sql 复制代码
CREATE INDEX idx_name_age_city ON user(name, age, city);

如果 SQL 是:

sql 复制代码
SELECT * FROM user WHERE name = 'Tom' AND age = 18;

key_len 可以帮你判断索引用到了 name,还是用到了 name + age

它不是越长越好。

它的价值是判断 联合索引是否被充分使用

3. rows 看扫描量大不大

rows 是优化器预估要扫描的行数。

比如:

text 复制代码
type: ALL
rows: 1000000

这基本就是危险信号。

它说明 MySQL 可能要扫描 100 万行。

rows 是估算值。

如果统计信息不准,可以执行:

sql 复制代码
ANALYZE TABLE user;

让 MySQL 重新统计。


二、重点看 type:扫描方式决定效率

type 表示 MySQL 找数据的方式。

常见类型从差到好大致是:

text 复制代码
ALL < index < range < ref < eq_ref < const

1. ALL:全表扫描

ALL 是最差的情况。

它表示 MySQL 要扫描整张表。

text 复制代码
type: ALL
key: NULL

这种情况通常要重点优化。

2. index:全索引扫描

index 是全索引扫描。

它比全表扫描稍好一点,因为扫的是索引树。

但本质还是从头扫到尾。

数据量大时也很慢。

3. range:索引范围扫描

常见于:

sql 复制代码
WHERE age > 18
WHERE price BETWEEN 10 AND 80
WHERE id IN (1, 2, 3)

range 开始,索引作用就比较明显了。

4. ref:非唯一索引等值查询

比如:

sql 复制代码
SELECT * FROM user WHERE age = 18;

如果 age 是普通索引,可能出现:

text 复制代码
type: ref

因为 age = 18 可能匹配多行。

5. eq_ref:唯一索引关联查询

常见于多表 JOIN。

比如订单表关联用户表:

sql 复制代码
SELECT *
FROM orders o
JOIN user u ON o.user_id = u.id;

如果 u.id 是主键或唯一索引,可能出现 eq_ref

6. const:主键或唯一索引查一行

比如:

sql 复制代码
SELECT * FROM user WHERE id = 1;

id 是主键,结果最多一条。

这种查询非常快。


三、Extra 里三个高频信号

Extra 是执行计划里的补充信息。

面试里最常问这三个:

text 复制代码
Using filesort
Using temporary
Using index

1. Using filesort:额外排序

Using filesort 表示 MySQL 不能直接利用索引顺序完成排序,只能自己额外排序。

注意,filesort 不一定真的写磁盘文件。

它可能在内存里排。

数据太大时才会落盘。

比如:

sql 复制代码
SELECT * FROM user ORDER BY age;

如果 age 没有索引,MySQL 可能会:

text 复制代码
扫描数据
取出结果
额外排序
返回结果

这就可能出现 Using filesort

优化方式是让排序字段走索引:

sql 复制代码
CREATE INDEX idx_age ON user(age);

2. Using temporary:使用临时表

Using temporary 表示 MySQL 创建了内部临时表保存中间结果。

常见于:

  • GROUP BY
  • ORDER BY
  • DISTINCT
  • UNION

比如:

sql 复制代码
SELECT age, COUNT(*)
FROM user
GROUP BY age;

如果不能利用索引完成分组,MySQL 可能会把中间结果放进临时表。

临时表为什么慢?

因为 MySQL 要额外写入中间结果,再读取、排序或分组。数据量大时还可能落盘。

内部临时表可以有索引。

但通常由 MySQL 自动决定。

你优化的方向不是手动控制临时表,而是让原 SQL 能直接利用索引完成分组或排序。

3. Using index:覆盖索引

Using index 是好信号。

它表示查询需要的数据只从索引里就能拿到,不需要回表。

比如有索引:

sql 复制代码
CREATE INDEX idx_name_age ON user(name, age);

执行:

sql 复制代码
SELECT age FROM user WHERE name = 'Tom';

name 用于过滤。

age 用于返回。

两个字段都在索引里。

这就是覆盖索引。


四、索引优化:不要只会说"加索引"

索引的本质是减少扫描范围。

但索引不是越多越好。

它会占空间,也会降低写入速度。

1. 哪些字段适合建索引

常见适合建索引的字段:

  • 经常出现在 WHERE 里的字段;
  • 经常用于 ORDER BY 的字段;
  • 经常用于 GROUP BY 的字段;
  • 多表 JOIN 的关联字段;
  • 区分度高的字段。

比如:

sql 复制代码
SELECT * FROM orders WHERE user_id = 1001;

可以考虑:

sql 复制代码
CREATE INDEX idx_user_id ON orders(user_id);

2. 哪些字段不适合建索引

这些字段不一定适合:

  • 区分度很低的字段,比如性别、状态;
  • 很少用于查询条件的字段;
  • 经常更新的字段;
  • 小表上的普通字段;
  • 大文本字段的完整索引。

比如 gender 只有男、女。

建索引后筛选度很低。

优化器可能仍然选择全表扫描。

3. 联合索引要遵循最左匹配原则

假设有联合索引:

sql 复制代码
CREATE INDEX idx_a_b_c ON t(a, b, c);

可以命中索引的情况:

sql 复制代码
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

不太能充分命中的情况:

sql 复制代码
WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

因为缺少最左边的 a

4. 联合索引字段顺序怎么排

常见原则:

  • 等值查询字段放前面;
  • 区分度高的字段优先;
  • 高频查询字段优先;
  • 排序、分组字段尽量接在后面。

比如高频 SQL 是:

sql 复制代码
SELECT *
FROM orders
WHERE user_id = 1001 AND status = 1
ORDER BY create_time DESC;

可以考虑:

sql 复制代码
CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);

这样既能过滤,也能帮助排序。


五、常见索引失效场景

很多慢 SQL 不是没索引。

而是索引用不上。

1. 左模糊查询

sql 复制代码
WHERE name LIKE '%Tom'
WHERE name LIKE '%Tom%'

这种通常无法走普通 B+Tree 索引。

因为索引是从左到右有序的。

如果你不知道开头是什么,MySQL 很难按索引定位。

可以优化成右模糊:

sql 复制代码
WHERE name LIKE 'Tom%'

2. 对索引列使用函数

sql 复制代码
WHERE DATE(create_time) = '2026-06-08'

这样会让索引列参与函数计算。

可以改成范围查询:

sql 复制代码
WHERE create_time >= '2026-06-08 00:00:00'
  AND create_time <  '2026-06-09 00:00:00'

3. 对索引列做表达式计算

sql 复制代码
WHERE age + 1 = 19

可以改成:

sql 复制代码
WHERE age = 18

4. 隐式类型转换

如果 phone 是字符串类型,却这样写:

sql 复制代码
WHERE phone = 13800138000

可能发生隐式类型转换。

应该写成:

sql 复制代码
WHERE phone = '13800138000'

5. OR 使用不当

sql 复制代码
WHERE name = 'Tom' OR age = 18

如果 name 有索引,但 age 没索引,可能导致索引效果变差。

可以考虑给两个字段都建合适索引,或者拆成 UNION

6. 范围查询后的字段

联合索引中,范围查询后面的字段通常不能继续用于精确定位。

比如索引:

sql 复制代码
CREATE INDEX idx_a_b_c ON t(a, b, c);

SQL:

sql 复制代码
WHERE a = 1 AND b > 10 AND c = 3

这里 a 可以用。

b 可以做范围扫描。

c 通常不能继续用于缩小索引扫描范围。


六、如果 MySQL 选错索引怎么办

有时 EXPLAIN 会发现优化器选了不合适的索引。

不要第一反应就强制索引。

先看三个问题:

  1. 统计信息是否不准;
  2. 索引区分度是否太低;
  3. SQL 是否让索引失效。

可以先更新统计信息:

sql 复制代码
ANALYZE TABLE products;

如果确认优化器确实选错了,可以使用索引提示。

1. USE INDEX

建议优化器考虑某个索引:

sql 复制代码
SELECT *
FROM products USE INDEX (idx_buyprice)
WHERE buyPrice BETWEEN 10 AND 80;

2. FORCE INDEX

强制 MySQL 尽量使用某个索引:

sql 复制代码
EXPLAIN SELECT productName, buyPrice
FROM products FORCE INDEX (idx_buyprice)
WHERE buyPrice BETWEEN 10 AND 80
ORDER BY buyPrice;

如果生效,可能看到:

text 复制代码
key: idx_buyprice
type: range

3. IGNORE INDEX

让优化器忽略某个索引:

sql 复制代码
SELECT *
FROM products IGNORE INDEX (idx_name)
WHERE buyPrice BETWEEN 10 AND 80;

但这些都属于干预手段。

更推荐先从索引设计和 SQL 写法上解决。


七、查询优化:少查、少回表、少 JOIN

1. 避免 SELECT *

SELECT * 的问题很直接。

它会查出整行数据。

带来四类成本:

  • 磁盘 IO 更多;
  • 网络传输更多;
  • 应用反序列化更多;
  • 更容易破坏覆盖索引。

比如你只需要:

sql 复制代码
SELECT id, name FROM user WHERE id = 1;

就不要写:

sql 复制代码
SELECT * FROM user WHERE id = 1;

2. 尽量使用覆盖索引

普通二级索引查询流程:

text 复制代码
查二级索引
拿到主键 id
回表查完整行
返回结果

覆盖索引查询流程:

text 复制代码
查二级索引
直接拿到需要字段
返回结果

少了回表,自然更快。

3. JOIN 要小结果集驱动大结果集

JOIN 可以理解成嵌套循环。

如果小表有 100 行,大表有 100 万行。

理想方式是:

text 复制代码
先查小结果集 100 行
每行去大表按索引查

如果反过来:

text 复制代码
先扫大表 100 万行
每行去小表匹配

成本就高很多。

更准确地说,不是物理小表驱动大表。

而是 过滤后的小结果集驱动大结果集

4. 被驱动表关联字段要有索引

比如:

sql 复制代码
SELECT *
FROM orders o
JOIN user u ON o.user_id = u.id;

如果 u.id 是主键,匹配很快。

如果被驱动表关联字段没索引,可能每次匹配都扫描一遍表。

这会非常慢。

5. 高频 JOIN 可以考虑冗余字段

比如订单列表每次都要展示用户名。

原设计:

text 复制代码
orders(id, user_id, amount)
user(id, name)

每次都 JOIN:

sql 复制代码
SELECT o.id, o.amount, u.name
FROM orders o
JOIN user u ON o.user_id = u.id;

可以在订单表冗余:

text 复制代码
orders(id, user_id, user_name, amount)

查询就变成:

sql 复制代码
SELECT id, amount, user_name FROM orders;

代价是数据冗余和一致性维护。

适合读多写少、字段变化少的场景。

订单商品名、下单价格这类历史快照字段,也很适合冗余。


八、分页优化:重点解决深分页

普通分页:

sql 复制代码
SELECT * FROM tb_sku LIMIT 200000, 10;

它不是直接跳到第 200000 行。

MySQL 通常要先扫描前 200010 行,再丢掉前 200000 行。

这就是深分页慢的原因。

1. 基于位置分页

如果主键自增,可以改成:

sql 复制代码
SELECT *
FROM tb_sku
WHERE id > 200000
ORDER BY id
LIMIT 10;

这样 MySQL 可以从索引位置继续往后查。

效率高很多。

但它适合连续翻页,不适合随机跳页。

2. 先查 id 再回表

如果必须深分页,可以先用覆盖索引查 id:

sql 复制代码
SELECT id
FROM tb_sku
ORDER BY create_time
LIMIT 200000, 10;

再根据 id 回表查详情。

也可以写成 JOIN:

sql 复制代码
SELECT s.*
FROM tb_sku s
JOIN (
  SELECT id
  FROM tb_sku
  ORDER BY create_time
  LIMIT 200000, 10
) t ON s.id = t.id;

这样至少避免一开始就搬运完整行数据。


九、表结构优化:大表和宽表要拆

如果单表数据达到千万级,查询压力可能明显增加。

但不要一上来就分库分表。

先看索引和 SQL 是否还能优化。

1. 垂直拆分宽表

如果一张表字段很多,可以按访问频率拆。

比如:

text 复制代码
user(id, name, phone, avatar, description, remark, ext_json)

可以拆成:

text 复制代码
user(id, name, phone)
user_profile(user_id, avatar, description, remark, ext_json)

高频字段放主表。

低频大字段放扩展表。

这样可以减少单行大小,提高缓存命中率。

2. 冷热数据分离

订单表常见做法是按时间冷热分离。

比如:

  • 最近 3 个月订单放热表;
  • 历史订单放冷表;
  • 普通查询只查热表。

这样可以降低高频查询的数据规模。

3. 分库分表

当单库或单表成为瓶颈时,再考虑分库分表。

常见方式:

方式 解决问题
垂直分库 按业务拆库,降低单库压力
垂直分表 拆宽表,减少单行体积
水平分库 数据分散到多个库
水平分表 大表拆成多个小表

分库分表会引入复杂度。

比如:

  • 分布式事务;
  • 跨库 JOIN;
  • 全局唯一 ID;
  • 分页排序;
  • 扩容迁移。

所以它是后手,不是起手式。


十、缓存优化:读多写少优先考虑

热点数据可以放 Redis。

缓存的目标是减少数据库压力。

常见读流程是旁路缓存:

text 复制代码
应用先查缓存
缓存命中,直接返回
缓存未命中,查询数据库
数据库返回后,写入缓存

写流程常见做法:

text 复制代码
先更新数据库
再删除缓存

为什么不是先删缓存再更新数据库?

因为并发下可能出现旧数据回写缓存。

更推荐:

先保证数据库成功,再删除缓存,让下一次读取重新加载新数据。

缓存也不是银弹。

要考虑几个问题:

1. 缓存穿透

查询的数据根本不存在。

请求每次都打到数据库。

解决方式:

  • 缓存空值;
  • 使用布隆过滤器。

2. 缓存击穿

某个热点 key 过期。

大量请求同时打到数据库。

解决方式:

  • 热点 key 不设置短过期;
  • 加互斥锁;
  • 提前异步刷新。

3. 缓存雪崩

大量 key 同时过期。

数据库瞬间被打爆。

解决方式:

  • 过期时间加随机值;
  • 多级缓存;
  • 限流降级。

十一、架构优化:读写分离和分库分表

当 SQL 和索引已经优化过,但压力仍然大,就要看架构。

1. 主从复制

MySQL 主从复制大致流程:

text 复制代码
主库写入数据
主库记录 binlog
从库拉取 binlog
从库回放日志
从库得到相同数据

2. 读写分离

读写分离的思路:

text 复制代码
写请求 -> 主库
读请求 -> 从库

这样可以提高读并发。

但要注意主从延迟。

比如用户刚下单,马上查订单。

如果读从库,可能查不到。

这类强一致场景可以强制读主库。

3. 分库分表解决什么

分库主要解决单库资源瓶颈。

分表主要解决单表数据量过大。

简单理解:

text 复制代码
单库扛不住 -> 分库
单表太大   -> 分表
字段太多   -> 垂直拆表
读压力大   -> 读写分离 + 缓存

十二、慢查询和锁等待怎么定位

慢查询不一定都是索引问题。

有时 SQL 本身不慢,是被锁卡住了。

面试里可以把它拆成两类:

text 复制代码
执行慢:SQL 扫描多、排序多、回表多
等待慢:SQL 在等锁、等 IO、等连接

1. 开启慢查询日志

慢查询日志用来记录执行时间超过阈值的 SQL。

常见参数:

sql 复制代码
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

临时开启可以这样:

sql 复制代码
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;

long_query_time = 1 表示超过 1 秒的 SQL 会被记录。

生产环境一般要结合业务情况设置。

不是越小越好。

太小会产生大量日志。

2. 慢查询日志看什么

慢查询日志里常看:

  • Query_time:SQL 总耗时;
  • Lock_time:等待锁的时间;
  • Rows_sent:返回行数;
  • Rows_examined:扫描行数;
  • SQL 原文。

如果 Rows_examined 很大,说明扫描多。

重点查索引和 SQL 写法。

如果 Lock_time 很大,说明可能被锁阻塞。

重点查事务和锁等待。

3. 用工具分析慢日志

慢查询日志很多时,不适合手动看。

可以用:

bash 复制代码
mysqldumpslow -s t -t 10 slow.log

含义是按总耗时排序,取前 10 条。

也可以用 pt-query-digest

它能聚合相似 SQL,看总耗时、平均耗时、执行次数和扫描行数。

面试里说出这个工具,会显得更接近真实排查。

4. 当前正在执行什么 SQL

如果线上突然变慢,可以看当前连接:

sql 复制代码
SHOW FULL PROCESSLIST;

重点看:

  • Command 是否是 Query
  • Time 是否很长;
  • State 是否在等锁;
  • Info 里具体 SQL 是什么。

常见状态包括:

text 复制代码
Sending data
Waiting for table metadata lock
Locked
Creating sort index
Copying to tmp table

其中 Waiting for table metadata lock 很典型。

它表示在等元数据锁,也就是 MDL 锁。

5. 什么是 MDL 锁

MDL 是 metadata lock,元数据锁。

它保护表结构不被并发破坏。

比如一个事务正在查询表。

另一个线程执行:

sql 复制代码
ALTER TABLE user ADD COLUMN age INT;

这个 DDL 可能会等待 MDL 锁。

更麻烦的是,后续新的查询也可能排队。

于是一个 DDL 就拖慢一堆 SQL。

所以线上执行 DDL 要特别小心。

6. 查 InnoDB 事务和锁等待

可以看 InnoDB 当前状态:

sql 复制代码
SHOW ENGINE INNODB STATUS\G

重点看:

  • LATEST DETECTED DEADLOCK:最近死锁信息;
  • TRANSACTIONS:当前事务;
  • 是否有 lock wait;
  • 哪个事务持有锁;
  • 哪个事务在等待锁。

MySQL 8.0 也可以查系统表:

sql 复制代码
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

这些表可以看到谁持有锁、谁在等锁。

7. 常见锁等待原因

慢查询里的锁等待,常见原因有:

  • 大事务长时间不提交;
  • 更新没有命中索引,导致锁范围变大;
  • 间隙锁导致插入被阻塞;
  • DDL 等 MDL 锁;
  • 事务里夹杂慢接口调用;
  • 批量更新一次改太多数据。

比如:

sql 复制代码
UPDATE user SET status = 1 WHERE phone = '13800138000';

如果 phone 没索引,MySQL 可能扫描大量记录。

在事务隔离级别和执行条件影响下,锁范围可能明显扩大。

所以更新、删除语句的条件字段也要有索引。

8. 死锁怎么排查

死锁不是"数据库坏了"。

它是两个事务互相等对方释放锁。

例如:

text 复制代码
事务 A:锁住 id = 1,再等 id = 2
事务 B:锁住 id = 2,再等 id = 1

就会形成死锁。

InnoDB 会自动检测死锁,并回滚其中一个事务。

排查方式:

sql 复制代码
SHOW ENGINE INNODB STATUS\G

LATEST DETECTED DEADLOCK

优化方式:

  • 固定访问顺序;
  • 缩短事务时间;
  • 减少一次事务影响的数据量;
  • 更新条件走索引;
  • 避免用户交互或远程调用放在事务里。

9. 面试怎么回答慢查询定位

可以这样说:

text 复制代码
我会先看慢查询日志,
通过 Query_time、Lock_time、Rows_examined 判断是执行慢还是等锁慢。

如果 Rows_examined 很大,
就用 EXPLAIN 看执行计划,
重点看 type、key、rows、Extra,
再优化索引和 SQL。

如果 Lock_time 很大,
就看 SHOW FULL PROCESSLIST,
确认当前 SQL 是否在 Waiting for lock 或 metadata lock。

然后用 SHOW ENGINE INNODB STATUS,
或者 performance_schema.data_locks、data_lock_waits,
定位哪个事务持有锁,哪个事务在等待。

如果是死锁,
看 LATEST DETECTED DEADLOCK,
再从事务访问顺序、索引、事务范围几个方向优化。

慢查询排查不是只看 SQL,也要看它是不是在等锁。执行慢看 EXPLAIN,等待慢看锁和事务。


十三、面试回答模板

如果面试官问:

一张表查询很慢,你有哪些解决方案?

可以这样回答:

text 复制代码
我会先用慢查询日志定位慢 SQL,
再用 EXPLAIN 分析执行计划。

重点看 type、key、rows、Extra。
如果是全表扫描,就看是否缺索引或索引失效。
如果 rows 很大,就考虑减少扫描范围。
如果有 Using filesort 或 Using temporary,
就看 ORDER BY、GROUP BY 是否能通过索引优化。

然后从几个层面处理:
第一,优化索引,比如 WHERE、JOIN、ORDER BY、GROUP BY 字段建合适索引,
多个字段高频组合查询就建联合索引,并遵循最左匹配原则。

第二,避免索引失效,
比如不要左模糊、不要对索引列使用函数或表达式,
避免隐式类型转换,注意 OR 条件。

第三,优化 SQL,
避免 SELECT *,只查必要字段,尽量使用覆盖索引减少回表。
JOIN 时让小结果集驱动大结果集,
并保证被驱动表关联字段有索引。
高频 JOIN 可以通过冗余字段减少关联。

第四,优化分页,
深分页不要直接 LIMIT 大 offset,
可以改成基于 id 的位置分页,
或者先查 id 再回表。

第五,优化表结构,
大表可以考虑冷热分离、水平拆表,
宽表可以考虑垂直拆分。
但拆表不是第一选择,要先看 SQL 和索引。

第六,引入缓存,
热点数据可以放 Redis,
读请求使用旁路缓存,
写请求可以先更新数据库再删除缓存,
同时处理缓存穿透、击穿和雪崩。

如果单机数据库压力仍然大,
再考虑主从复制、读写分离、分库分表等架构方案。

最后可以补一句:

MySQL 调优不是只会加索引,而是先定位瓶颈,再分层优化。核心是减少扫描行数、减少回表、减少排序和临时表,并降低数据库整体压力。


十四、复习速记版

最后给你一版背诵用口诀:

text 复制代码
先定位:慢日志 + EXPLAIN
看计划:type、key、rows、Extra
查索引:缺不缺、准不准、失没失效
改 SQL:少查列、少回表、少 JOIN
治分页:避开深 offset
拆结构:大表拆、宽表拆、冷热分
加缓存:热点进 Redis
上架构:主从、读写分离、分库分表

再记住这几个判断:

text 复制代码
key 为 NULL        -> 没用索引
rows 很大          -> 扫描多
type = ALL         -> 全表扫描
Using filesort     -> 额外排序
Using temporary    -> 用了临时表
Using index        -> 覆盖索引,好事

相关推荐
周杰伦fans2 小时前
深入浅出AutoCAD .NET二次开发:HostApplicationServices完全解析
数据库·.net
AQin10122 小时前
【对比向】细算“成本”——Hive vs. Doris
大数据·数据库·hive·doris·实时数仓
承渊政道2 小时前
【MySQL数据库学习】MySQL基本查询(上)
linux·数据库·学习·mysql·bash·数据库开发·数据库系统
中草药z2 小时前
【RAG】工程化实战:全链路原理复盘 + 方案选型 + 实战高阶玩法
java·深度学习·机器学习·阿里云·rag·springai
Hillain2 小时前
软件设计师设计模式
java·开发语言·经验分享·笔记·算法·设计模式·软考
影寂ldy2 小时前
C# 泛型方法
java·前端·c#
运维行者_2 小时前
如何为您的企业选择最佳网络监控工具
大数据·运维·服务器·网络·数据库
摇滚侠2 小时前
Spring 零基础入门到进阶 IOC 概述 11 - 13
java·后端·spring
李少兄2 小时前
Spring Boot Test 启动类自动发现机制解析与工程实践
java·spring boot·后端