REJAVA / LEARNING NOTES
第15课速查:慢 SQL 定位与优化
随时回查术语、规则和示例;完整解释见对应课程。
根据课程材料整理
本次更新说明
- 整理为公开学习笔记,保留要点、对照表与练习入口。
五步排查
① 慢查询日志找到它 → 看行数比 → ② EXPLAIN(查看执行计划的语句)读计划 → 四列 → ③ 说出原因 → 改一处 → ④ 改写或调整索引 → 同条件 → ⑤ 实际执行验证
没变快就回到第②步重新看计划,不要继续猜着加索引。办件表 case_item 约100万行(教学设定),联合索引 (dept_id, status, submitted_at),办件号 case_no 是VARCHAR(20)(可变长字符串)并有索引。
慢查询日志读什么
| 字段 | 含义 | 怎么用 |
|---|---|---|
Query_time | 执行时间(秒);判断是否超过阈值时,不计开始执行前取锁的时间 | 确认确实慢 |
Lock_time | 获取锁花的时间 | 很小时通常可以先排除锁等待;很大时先按第13课的锁等待查 |
Rows_sent 与 Rows_examined | 返回给客户端的行数;服务器层检查过的行数 | 返回20行却检查几十万行,说明大量无用读取 |
慢查询日志默认关闭;long_query_time 默认10秒、最小0。阈值按业务设定。
执行计划先看四列
| 列 | 看什么 | 易错点 |
|---|---|---|
type(访问方式) | 从好到差:const → ref → range → index → ALL | index是读完整棵索引树,不是“用得好”;ref匹配几十万行一样慢 |
key(实际使用的索引) | 实际用了哪个索引,NULL表示没用 | possible_keys只是候选 |
rows(估计检查行数) | 估计要检查的行数 | InnoDB(事务存储引擎)下是估计值;深分页的白读不一定反映出来 |
Extra(额外信息) | Using index=覆盖索引;Using index condition=索引条件下推;Using filesort(额外排序)=额外排序;Using temporary=临时表 | filesort(额外排序)不一定写磁盘 |
索引用不上:都是“找不到起点”
B树索引按列的原始值排序。能用索引,是因为数据库能在这个顺序里找到起点,连续读一段就停。
| 写法 | 为什么找不到起点 | 改法 |
|---|---|---|
DATE(submitted_at) = '2026-10-08' | 索引存原始时间,不存DATE()结果,只能逐条计算 | submitted_at >= '2026-10-08' AND submitted_at < '2026-10-09' |
case_no = 2026100800123 | 字符串列和数字比较:’…123’、’ …123’、’…123a’ 都等于这个数,分散在多处 | 传字符串;Java参数用String(字符串类型),不用Long |
applicant LIKE '%张%' | 以通配符开头,不知道从哪里读起 | 业务允许就改前缀 '张%';否则另评估全文搜索 |
写法正确,key(实际使用的索引)仍为NULL | 匹配行太多、每行都要回表,optimizer(查询优化器)判断全表扫描更便宜 | 先看rows和数据分布,再决定 |
WHERE里条件的书写顺序不影响最左前缀;要看的是用到了索引的哪些列。
排序:什么时候出现 Using filesort(额外排序)
| 查询 | 结果 | 原因 |
|---|---|---|
WHERE dept_id=10 AND status=0 ORDER BY submitted_at DESC LIMIT 20 | 按索引顺序倒着读,够20条就停 | 前两列都是等值,剩下这一段正好按时间有序 |
WHERE dept_id=10 ORDER BY submitted_at DESC LIMIT 20 | 取出部门10所有行,额外排序后取20条 | 跳过了status,两段各自有序、合起来不有序 |
改法先问业务:只看待审批就补上 status = 0;确实要所有状态混排,再评估新索引 (dept_id, submitted_at) 的写入代价。
深分页三种处理
| 做法 | 怎样读 | 限制 |
|---|---|---|
LIMIT 19980, 20(原写法) | 读过前19980条再丢掉;列不在索引里时这些行也要回表(JavaGuide说明) | 页码越大越慢 |
| cursor pagination(游标分页) | 记住上一页最后一条的(提交时间, id),从它之后读20条;InnoDB(事务存储引擎)二级索引末尾自带主键,(submitted_at, id) 的顺序索引里本来就有 | 适合下一页/加载更多,不能跳页;要带id保证顺序唯一 |
| deferred join(延迟关联) | 子查询在覆盖索引上翻页只取id,外层只对20个id回表 | 仍要数过前面的索引项;和产品约定最大页数 |
验证的四条规矩
- EXPLAIN(查看执行计划的语句)只给计划和估计;EXPLAIN ANALYZE(实际执行并分析)会真的执行语句,给出每一步实际行数和耗时。只在测试环境、只对查询语句用。
- 改前改后用同样的数据和参数。
- 一次只改一处。
- 算上新索引的空间和写入代价;上线后看这条语句在慢查询日志里的耗时是否下降。
一分钟表达
我会先在慢查询日志里找到那条语句,看返回行数和检查行数是否差很多、等锁时间是否很长。再用EXPLAIN(查看执行计划的语句)看type、key(实际使用的索引)、rows、Extra,判断是全表扫描、索引用不上、额外排序还是深分页。索引用不上常见于对索引列套函数、字符串列拿数字比较、LIKE以通配符开头,本质都是数据库在索引顺序里找不到起点;排序要让等值条件和ORDER BY的列对上索引顺序;深分页按业务改成游标分页或延迟关联。改完在测试环境用EXPLAIN ANALYZE(实际执行并分析)以同样条件对比实际行数和耗时,没做过的优化不编成经历。