慢查询优化实战四条套路
不用背一百个优化技巧。所有 SQL 优化就这四条路——从投入小到大排列,挨个试。
优化金字塔(从便宜到昂贵):
第 1 层:改 SQL 写法 ← 零成本,先试这个
第 2 层:改/加索引 ← 小成本,主力手段
第 3 层:改表结构 ← 中成本,需要数据迁移
第 4 层:改架构(缓存/读写分离) ← 高成本,最后手段
1.1 第 1 层:改 SQL 写法(零成本)
套路 1:SELECT * → 只查需要的列
❌ SELECT * FROM users WHERE name = '张三';
→ 把所有列的数据全拿回来,包括你根本不需要的 avatar(大 BLOB)、intro(长 TEXT)
✅ SELECT id, name, age FROM users WHERE name = '张三';
→ 只拿三列,传输数据量小 80%,更可能触发覆盖索引
套路 2:大偏移分页 → 改游标分页
❌ SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
→ MySQL 先查前 1000020 行,扔掉前 1000000 行,返回后 20 行。
→ 扫了 100 万行才吐 20 行——白做 999980 行的无用功!
✅ SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
→ 从 id=1000000 开始直接往后取 20 行,只扫了 20 行。
→ 前提:前端配合记住上一页最后一条的 id。
套路 3:JOIN 替代子查询
❌ SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
→ 子查询先生成临时结果 → 再和 users 表匹配。数据量大时临时表是瓶颈。
✅ SELECT DISTINCT u.* FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 100;
→ JOIN 比 IN 子查询更容易被优化器优化,索引利用率更高。
套路 4:COUNT(*) vs COUNT(列)
✅ SELECT COUNT(*) FROM orders; → 统计总行数(包括 NULL 行)
⚠️ SELECT COUNT(column) FROM orders; → 统计 column 不为 NULL 的行数
→ 两者语义不同!用哪个取决于你要不要排除 NULL。
套路 5:避免在 WHERE 里对列做运算
❌ SELECT * FROM orders WHERE YEAR(created_at) = 2026;
✅ SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
1.2 第 2 层:改/加索引(小成本,主力手段)
套路 1:WHERE 条件列 + ORDER BY 列 + JOIN 关联列 → 建索引
建索引口诀:哪里出现在 WHERE、ORDER BY、JOIN ON 里,哪里就该考虑建索引。
套路 2:复合索引列的顺序 = 等值在前,范围在后
WHERE name = '张三' AND age > 20 AND status = 'active' ORDER BY created_at
等值:name, status
范围:age
排序:created_at
→ 建索引 idx(name, status, age, created_at)
→ 等值条件放前面,范围条件放后面,ORDER BY 放最后
→ 这是"最佳左前缀"的精髓
套路 3:覆盖索引——让查询永远不用回表
SELECT name, age FROM users WHERE name = '张三';
→ 建 idx(name, age) 而不是只建 idx(name)
→ 查询只需要 name 和 age → 两列全在索引里 → 直接读完索引就走 → 不回表
→ Extra: Using index ✅
套路 4:长得像的索引删掉
idx_a(a) ← 冗余,idx_a_b 包含 a
idx_a_b(a, b)
删 idx_a,留 idx_a_b。覆盖 a 的查询同样能用 idx_a_b(最左前缀)。
冗余索引 = 浪费磁盘 + INSERT/UPDATE 时要多维护一份索引 = 写操作变慢。
套路 5:建完索引用 EXPLAIN 验证
→ type 从 ALL 变成 ref/range 了吗?
→ rows 减少了吗?
→ Extra 里 Using filesort / Using temporary 消失了吗?
没变 → 索引没生效 → 回头看第四章的失效场景。
1.3 第 3 层:改表结构(中成本)
场景:字段太大或设计不合理。
① 大字段拆表
users 表有一个 avatar BLOB 列(头像二进制数据),但 90% 的查询不需要它。
→ 拆成 users 表(基本信息)+ user_profiles 表(大字段)
→ users 表的行变小 → 一页能装更多行 → 全表扫描也更快
② 垂直拆分——按列的访问频率分表
热点列(经常查的)放一张表,冷门列放一张表。主键一致。
③ 冗余字段——反范式
orders 表每次查都要 JOIN users 拿 user_name。
→ 在 orders 表里直接加 user_name 冗余列。
→ 牺牲一点空间(多存一个名字)和一致性(用户改名要同步改),换取不用 JOIN。
1.4 第 4 层:改架构(高成本)
① 加 Redis 缓存 → 热点查询走缓存,不碰数据库
② 读写分离 → 主库写,从库读,分担压力
③ 分库分表 → 一张表太大(几十亿行),拆成多张表
这三个是第八九天会专门讲的,今天知道有这条退路就行。
速记卡(面试闪卡)
Q1:一句话讲清「慢查询优化实战四条套路」到底是什么?
A:慢 SQL 优化有四条从便宜到贵的万能套路:改写法、加索引、改表结构、改架构,挨个试。
Q2:第 1 层改 SQL 写法有啥招? —— 怎么理解?
A:像少跑腿少搬东西——SELECT * 改成只查需要的列(传输少 80%、易触发覆盖索引);大偏移分页 LIMIT 1000000,20 改成游标 WHERE id>上一页;子查询换 JOIN;别在 WHERE 里对列套函数(YEAR(created_at) 会让索引失效)。
Q3:第 2 层加索引怎么加才对? —— 怎么理解?
A:像在常用路口设红绿灯——WHERE、ORDER BY、JOIN ON 出现的列就该建索引;复合索引按等值在前、范围在后、排序最后(最左前缀);建覆盖索引让查询不回表;建完用 EXPLAIN 看 type/rows/Extra 验证生效。
Q4:第 3、4 层改表和改架构是啥? —— 怎么理解?
A:像店面改造和开分店——第 3 层改表结构:大字段拆表、冷热列垂直拆分、加冗余字段反范式省 JOIN;第 4 层改架构(高成本退路):加 Redis 缓存、读写分离、分库分表。
Q5:优化金字塔为什么从便宜到贵排? —— 怎么理解?
A:像看病先吃药再手术——改写法零成本先试,加索引小成本主力,改表结构中成本要迁移,改架构高成本才动。多数慢查询前两层就解决了,别一上来就分库分表。
Q6:核心速记主线有哪些?
-
第1层改SQL:只查所需列、游标分页、JOIN替子查询、别在列上套函数
-
第2层加索引:WHERE/ORDER BY/JOIN列建索引、复合索引最左前缀、覆盖索引不回表、EXPLAIN验证
-
第3层改表:大字段拆表、垂直拆分、冗余字段反范式
-
第4层改架构:Redis缓存、读写分离、分库分表(最后手段)
口诀
A:慢查询,四条路,从便宜到贵数。
先改写法零成本,只查所需分页游。
再加索引小成本,最左前缀覆盖住。
表结构,架构层,前两层不行再动土。