第一步:打开慢日志,圈出最贵的查询
先确认慢日志开启与阈值。线上建议把 `long_query_time` 降到 0.1~0.5 秒——默认 10 秒的阈值会把大量「正在变慢」的查询漏掉。`log_queries_not_using_indexes` 能额外记录全表扫描的语句。
优化精力有限时,别按条数排序,按「总耗时」排序:一条 3 秒、每天跑 10 万次的查询,价值远超一条 30 秒、每天 3 次的报表。
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.2;
SET GLOBAL log_queries_not_using_indexes = ON;
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log # 按总耗时取 Top10第二步:EXPLAIN 的四个关键信号
对目标 SQL 执行 `EXPLAIN`,重点看四列:type(访问类型,ALL 是全表扫描,达到 ref/range 才算走了索引);key(实际用到的索引,NULL 表示没用上);rows(预估扫描行数,数量级决定耗时数量级);Extra(Using filesort / Using temporary 都是排序与临时表的开销信号,Using index 则是好信号,覆盖索引免回表)。
EXPLAIN SELECT ... ;
-- type=ALL + key=NULL → 全表扫描,优化对象第三步:判断该不该建索引
建索引的判断标准:该列出现在 WHERE / JOIN / ORDER BY 的高频查询里,且区分度足够(性别这类两三值的列单独建索引收益很低)。写多读少的表要克制——每个索引都拖慢写入。
复合索引记住最左前缀:索引 (a, b) 能服务 WHERE a=? 与 WHERE a=? AND b=?,但服务不了 WHERE b=?。把等值条件的列放前面、范围条件的列放后面,是通用的排列策略。若 SELECT 的列都能被索引覆盖,还能得到 Using index 的免回表加速。
ALTER TABLE orders ADD INDEX idx_user_time (user_id, created_at);
-- 等值列在前、范围列在后索引失效的六种常见写法
① 对索引列做函数或运算:`WHERE YEAR(created_at)=2026`;② 隐式类型转换:字符串列用数字查(`WHERE phone=13800000000`);③ 前导模糊:`LIKE '%keyword'`(后导 `keyword%` 可以);④ OR 两边有一边没索引;⑤ 违反最左前缀直接查第二列;⑥ 数据分布倾斜时优化器主动放弃索引(统计信息过期,`ANALYZE TABLE` 刷新)。
-- 失效:函数包裹索引列
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- 改写:范围条件,可走索引
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';优化后必须做的两件事
① 复跑 EXPLAIN 对比前后执行计划(type 从 ALL → range 即有效);② 复测真实耗时,最好用线上等价数据量——测试库一万行走索引很快,生产一亿行可能反而更慢(回表次数放大)。
记录前后指标到工单或 wiki,让「为什么建这个索引」在半年后仍可追溯,避免下一个人困惑后随手删掉它。
EXPLAIN SELECT ...; -- 优化后复核
ANALYZE TABLE orders; -- 刷新统计信息什么时候索引救不了你
深分页(LIMIT 1000000, 20)、大表 JOIN 无索引、SELECT * 拉全字段、单事务写入过大——这些是架构问题:改游标分页(记住上一页末尾 id)、冗余字段代替 JOIN、只取需要的列、拆事务。识别「该停止调 SQL、开始改设计」的时机,是慢查询优化的最后一课。
-- 深分页优化:游标式
SELECT * FROM orders WHERE id > <last_id> ORDER BY id LIMIT 20;