文章目录
-
- 前言
- [一、先用 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 BYORDER BYDISTINCTUNION
比如:
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 会发现优化器选了不合适的索引。
不要第一反应就强制索引。
先看三个问题:
- 统计信息是否不准;
- 索引区分度是否太低;
- 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 -> 覆盖索引,好事