优化前先测量
不要凭感觉改 SQL。任何优化都必须有前后对比数据,否则你不知道自己是改好了还是改坏了。
sql
-- 看实际执行耗时
SET profiling = 1;
SELECT ... ;
SHOW PROFILES;
-- 看执行计划
EXPLAIN SELECT ... ;
把优化前的耗时记下来,优化后再跑一次,这才是完整的优化。
怎么看执行计划
EXPLAIN 的输出字段很多,真正需要关注的是四个:
| 字段 | 看什么 |
|---|---|
type | 访问类型,从好到坏:const > ref > range > index > ALL |
key | 实际用了哪个索引,为 NULL 说明没走索引 |
rows | 预估扫描行数,越小越好 |
Extra | 关键提示,见下表 |
Extra 里的信息最容易被忽略,但价值最高:
| Extra 内容 | 含义 | 该做什么 |
|---|---|---|
Using filesort | 需要额外排序 | 考虑给排序字段建索引 |
Using temporary | 用了临时表 | 常见于 GROUP BY,考虑改写 |
Using index | 覆盖索引,不用回表 | 好现象 |
Using where | 拿到行之后才过滤 | 说明过滤字段没走索引 |
看到 type=ALL 加 rows 接近全表,基本可以确定问题所在。
索引的三种用法
第一种,等值查找。 最常见,直接定位。
sql
CREATE INDEX idx_status ON orders (status);
SELECT * FROM orders WHERE status = "paid";
第二种,范围查找。 索引列参与范围比较。
sql
SELECT * FROM orders WHERE created_at >= "2026-01-01";
第三种,覆盖索引。 查询需要的列全在索引里,不用回表读数据行。这是最快的形态。
sql
-- 索引 (status, created_at)
SELECT status, created_at FROM orders WHERE status = "paid";
-- Extra 会显示 Using index,不需要回表
联合索引的顺序
联合索引遵循最左前缀原则:索引 (a, b, c) 能支持 a、a,b、a,b,c 三种前缀查询,但单独查 b 用不上。
顺序怎么定?两条经验:
- 等值条件在前,范围条件在后。因为范围条件一旦出现,它后面的列就无法再用于精确定位
- 区分度高的在前(在满足第一条的前提下)
sql
-- 查询:WHERE status = ? AND created_at >= ?
-- 正确:(status, created_at)
-- 错误:(created_at, status) —— 范围列在前,status 用不上
什么情况下索引失效
| 写法 | 为什么失效 |
|---|---|
WHERE YEAR(created_at) = 2026 | 对索引列做了函数运算 |
WHERE name LIKE "%abc" | 前导通配符无法定位起点 |
WHERE a = 1 OR b = 2 | OR 两侧列不同,通常退化为全表 |
WHERE age + 1 = 20 | 表达式运算,改成 age = 19 |
WHERE varchar_col = 123 | 类型隐式转换 |
第一条和第五条是最常见的。类型不匹配导致的隐式转换尤其隐蔽——看起来完全正常的查询,因为字段是字符串而参数是数字,索引直接失效。
函数那条的通用解法是把运算移到参数一侧:
sql
-- 失效
WHERE DATE(created_at) = "2026-09-23"
-- 有效:改成范围
WHERE created_at >= "2026-09-23 00:00:00"
AND created_at < "2026-09-24 00:00:00"
连接查询的顺序
多表连接时,优化器通常会自己选驱动表,但选错时需要人工干预。原则是:让小结果集驱动大表。
sql
-- 假设 users 有 10 万行,orders 里某用户只有 20 行
-- 好的顺序:先用条件把 users 缩到 1 行,再去 orders 里查
SELECT u.name, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.email = "a@b.com";
配套要求是连接列必须有索引,否则每次匹配都要全表扫描被驱动表。
深翻页为什么越翻越慢
sql
-- 第 1 页很快
SELECT * FROM article ORDER BY id LIMIT 10 OFFSET 0;
-- 第 10000 页很慢
SELECT * FROM article ORDER BY id LIMIT 10 OFFSET 100000;
原因是 OFFSET 并不是「跳过」,而是先取出前 100010 行,再丢掉前 100000 行。翻得越深,白读的行越多。
解法有两种:
游标分页(推荐):记住上一页最后一条的排序值,从它之后开始取。
sql
SELECT * FROM article
WHERE id > 100000
ORDER BY id LIMIT 10;
延迟关联:先用覆盖索引取出主键,再回表取数据。
sql
SELECT a.* FROM article a
JOIN (SELECT id FROM article ORDER BY id LIMIT 10 OFFSET 100000) t
ON a.id = t.id;
第一种更适合无限滚动,第二种适合必须支持跳页的场景。
小结
SQL 优化的通用流程是:看执行计划 → 确认瓶颈 → 调整索引或改写语句 → 重新测量。
三条最常用的经验:等值在前范围在后、避免对索引列做运算、深翻页改用游标。
#性能优化#SQL#数据库