目录
[二、EXPLAIN 深度解读](#二、EXPLAIN 深度解读)
[三、单条 SQL 改写技巧](#三、单条 SQL 改写技巧)
[1. 拒绝 SELECT *](#1. 拒绝 SELECT *)
[2. 深分页](#2. 深分页)
[3. COUNT 优化](#3. COUNT 优化)
[4. ORDER BY 优化(规避 filesort)](#4. ORDER BY 优化(规避 filesort))
[5. GROUP BY 优化(规避 temporary)](#5. GROUP BY 优化(规避 temporary))
[6. JOIN 优化:小表驱动大表](#6. JOIN 优化:小表驱动大表)
[7. 子查询转 JOIN](#7. 子查询转 JOIN)
[8. OR 改 UNION](#8. OR 改 UNION)
[9. 别让索引失效](#9. 别让索引失效)
[五、完整案例:一条慢 SQL 的救活过程](#五、完整案例:一条慢 SQL 的救活过程)
[六、面试:被问"这条 SQL 怎么优化"的回答套路](#六、面试:被问"这条 SQL 怎么优化"的回答套路)
一句话:索引优化是"地基",SQL 优化是"施工"------同样的索引,写法不同性能差百倍。这篇讲的是"拿到一条慢 SQL,怎么一步步把它救活"。
一、优化第零步:先定位,别靠猜
最大的误区是"线上慢了就加索引"。正确姿势是先确认慢在哪、再动手。
开慢查询日志
|-----------------------|-------------------------------|
| 参数 | 含义 |
| slow_query_log | 是否开启慢日志 |
| long_query_time | 超过多少秒算"慢"(默认 10s,建议调到 1s 抓隐患) |
| slow_query_log_file | 日志文件路径 |
分析慢日志
⚠️ 没有慢日志就优化,等于蒙眼修车。先
EXPLAIN那条被抓出来的 SQL,再决定改法。
二、EXPLAIN 深度解读
EXPLAIN SELECT ... 输出的列,面试和实战都看这几项:
|-----------------|--------------------------------------------------------------------------|
| 列 | 看什么 |
| id | 执行顺序,id 相同从上往下,id 越大越先执行(子查询/派生表) |
| select_type | 查询类型:SIMPLE 简单查询、PRIMARY 主查询、SUBQUERY 子查询、DERIVED 派生表 |
| type | 访问类型,核心指标 ,好→坏:system > const > eq_ref > ref > range > index > ALL |
| key | 实际用到的索引,NULL = 没走索引 |
| key_len | 索引使用的字节数,判断联合索引用到了几列 |
| ref | 索引的匹配条件(常量或某列) |
| rows | 预估扫描行数,越小越好 |
| filtered | 存储引擎返回后,Server 层再过滤的比例 |
| Extra | 关键补充信息(见下) |
type 看到 ALL = 全表扫描,红色警报 ;业务查询至少要到 ref / range。
Extra 重点:
Using index✅ 覆盖索引,最优
Using where正常,引擎返回后再过滤
Using index condition✅ 索引条件下推(ICP),在引擎层就用索引过滤,减少回表
Using filesort⚠️ 额外排序,排序没走索引
Using temporary⚠️ 用了临时表(GROUP BY / DISTINCT 高频中招)
key_len 速算 (utf8mb4 下每字符 4 字节):varchar(20) 可空 → 20*4 + 2(变长) + 1(可空) = 83;int → 4;bigint → 8。用它判断"联合索引到底用到了第几列"。
三、单条 SQL 改写技巧
这部分是"索引建好了但仍慢"的主战场。
1. 拒绝 SELECT *
只查需要的列,才有机会走 覆盖索引,省掉回表 IO。
2. 深分页
LIMIT 100000, 20 要先丢 10 万行。用主键游标 或延迟关联改:
3. COUNT 优化
COUNT(*) 与 COUNT(1) 等价且最快(MySQL 优化过);COUNT(列) 只数非 NULL。要"近似值"可走 EXPLAIN 的 rows 或用 information_schema,别对大表 COUNT(*) 还加各种条件硬扛。
4. ORDER BY 优化(规避 filesort)
排序字段要和索引顺序一致才能走索引排序:
⚠️
ORDER BY多个字段的方向要一致(ASC/DESC混用会导致 filesort)。MySQL 8.0 支持降序索引,可以建(a, b DESC)。
5. GROUP BY 优化(规避 temporary)
分组字段走索引,且 WHERE 先过滤再分组。写法上 GROUP BY 的列尽量与 ORDER BY 一致,避免"先分组再排序"的双重代价。
6. JOIN 优化:小表驱动大表
Nested Loop Join 中,外层(驱动表)越小,内层被扫描次数越少 。连接字段必须建索引,否则退化为笛卡尔积。
7. 子查询转 JOIN
相关子查询是 O(n×m)(外层每行跑一次内层)。能用 JOIN / 派生表先聚合再回连的,尽量改:
8. OR 改 UNION
WHERE a=1 OR d=2(d 无索引)常导致整条 SQL 放弃索引走全表。拆成 UNION 各自走索引:
9. 别让索引失效
对列用函数、LIKE '%x'、隐式类型转换、违反最左前缀------这些都会让前面建的索引白建。详见《MySQL索引优化》篇。
四、表设计与数据类型优化
很多慢 SQL 根子在"表设计",靠改写 SQL 救不回来:
- 选最小够用的类型 :
tinyint够别用int,int够别用bigint;varchar长度按需给,别一律varchar(255)。
- 避免 NULL :NULL 让索引、统计、比较都更复杂;用默认值代替(如
0、'')。
- 字符集统一 utf8mb4:避免 JOIN 时因字符集/排序规则不一致触发隐式转换、索引失效。
- 长字符串用前缀索引 :
INDEX(col(20)),取前 20 字符建索引,省空间;注意前缀选择性要够高。
- 适度反三范式:高频跨表统计可冗余字段,用空间换 JOIN 开销。
五、完整案例:一条慢 SQL 的救活过程
原始 SQL(0.8s,订单表 500 万行):
第 1 步 · EXPLAIN :type=ALL(orders 全表扫描)+ Using filesort + rows≈5000000。
第 2 步 · 加联合索引 :ALTER TABLE orders ADD INDEX idx_status_ct (status, create_time); → type 变 ref,Extra 出现 Using index condition,排序也走索引,filesort 消失。
第 3 步 · 改深分页:用延迟关联,先查 id 再回表。
第 4 步 · 缩小返回列 :业务其实只要 order_no, amount, name,把 SELECT * 换成具体列,命中覆盖索引、减少回表。
结果:0.8s → 30ms,扫描行数从 500 万降到 20。
这条优化串起了三件事:加对索引(03 篇)→ 改写分页(03 篇)→ 缩小列 + JOIN 优化(本篇)。这就是"定位 → 分析 → 改写 → 验证"的完整闭环。
六、面试:被问"这条 SQL 怎么优化"的回答套路
固定顺序,别一上来就加索引:
- 先定位 :慢日志抓出来?
EXPLAIN看一下。
- 看执行计划 :
type是不是ALL?key用没用上?rows多大?Extra有没有 filesort/temporary?
- 看索引是否失效 :函数、隐式转换、
LIKE '%x'、最左前缀。
- 减数据量:先过滤再 JOIN?覆盖索引避免回表?
- 再谈改写:子查询转 JOIN、深分页改游标、OR 改 UNION。
- 看表设计:类型/字符集/NULL/反三范式。
- 最后才是架构层:缓存、冗余、分库分表。
七、小结
SQL 优化的知识地图:定位(慢日志)→ 解读(EXPLAIN 各列)→ 改写九式(SELECT * / 深分页 / COUNT / ORDER BY / GROUP BY / JOIN / 子查询转 JOIN / OR 改 UNION / 防失效)→ 表设计(类型/NULL/字符集/前缀索引/反三范式)→ 案例闭环 → 面试套路。