SQL 写完后必须自查的 10 条军规:避开 90% 的慢查询
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
上次我们聊了“SQL 为什么会越查越慢”,讲了 5 个最容易踩的坑。 评论区最多的一条反馈是:“道理我都懂,但写的时候还是忘。” 所以今天这一篇,我把它做成一份可以照着用的 Check List——10 条军规,每条都给出:
你可以收藏到代码片段里,写完 SQL 直接对一遍;也能贴在 Code Review 模板前,每次合 PR 前扫一遍。
军规 0:先看 |
type | ALLindex(全索引扫描) |
key | NULL |
rows | |
Extra | Using filesort、Using temporary |
如果 4 个字段都健康,这条 SQL 可以直接发布,不用再纠结。
判断标准:type 不是 ALL / index,且 key 不为 NULL。
错误写法:
SELECT id, name FROM users WHERE status = 1;如果 status 只有 0/1 两个值,索引区分度太低,MySQL 优化器会放弃索引。
正确写法:
-- 方案 A:组合索引,把区分度低的字段放后面
ALTER TABLE users ADD INDEX idx_created_status (created_at, status);
SELECT id, name
FROM users
WHERE created_at >= '2026-07-01'
AND status = 1;
-- 方案 B:枚举型筛选交给 ES / 归档表一句话:索引不是越多越好,而是要让 MySQL 觉得“值得用”。
判断标准:WHERE 条件里的字段是“裸字段”,没有被函数包过。
错误写法:
-- 索引列上用了函数
SELECT user_id FROM login_log
WHERE DATE(login_time) = CURDATE();
-- 字符串字段传了数字(隐式类型转换)
SELECT * FROM orders WHERE mobile = 13800000000;正确写法:
-- 把函数挪到常量侧
SELECT user_id FROM login_log
WHERE login_time >= CURDATE()
AND login_time < CURDATE() + INTERVAL 1 DAY;
-- 类型保持一致
SELECT * FROM orders WHERE mobile = '13800000000';执行计划对比:type: ALL → type: range。
一句话:任何让索引列“变形”的写法,索引都会失效。
LIKE 别让通配符打头判断标准:LIKE '前缀%',永远不要 LIKE '%关键词%'。
错误写法:
SELECT id, name FROM products WHERE name LIKE '%牛奶%';执行计划:
type: ALL
key: NULL正确写法:
-- 简单搜索:让前缀是确定的
SELECT id, name FROM products WHERE name LIKE '牛奶%';
-- 复杂搜索:用全文索引(中文需配合 ngram)
ALTER TABLE products
ADD FULLTEXT INDEX ft_name (name) WITH PARSER ngram;
SELECT id, name
FROM products
WHERE MATCH(name) AGAINST('+牛奶' IN BOOLEAN MODE);
-- 终极方案:丢给 Elasticsearch / MeilisearchOR 改 IN,跨列就别用 OR判断标准:能用 IN 就别用 OR;跨列 OR 直接重写为 UNION ALL。
错误写法:
SELECT id FROM orders
WHERE status = 'PENDING' OR status = 'CANCELLED';某些版本能走 index_merge,但跨列 OR 几乎一定走全表:
-- 跨列 OR:典型反模式
SELECT id FROM orders
WHERE status = 'PENDING' OR user_id = 1001;正确写法:
-- 同列 OR 改 IN
SELECT id FROM orders
WHERE status IN ('PENDING', 'CANCELLED');
-- 跨列 OR 拆成 UNION ALL
SELECT id FROM orders WHERE status = 'PENDING'
UNION ALL
SELECT id FROM orders WHERE user_id = 1001;SELECT * 是慢查询的温床判断标准:SELECT 后面只列出真正用到的列。
反模式:
SELECT * FROM orders WHERE id = 1001;如果表里有 TEXT / JSON / BLOB 字段,会把这些大字段一起读出来,走网络、走 ORM 映射、走 JSON 序列化,都会变慢。
正确写法:
SELECT id, user_id, amount, status, created_at
FROM orders WHERE id = 1001;执行计划对比:返回同样行数,Extra 会少 Using where 后面的隐式开销。
一句话:写多少字段,就取多少字段。ORM 里的
SELECT *是隐藏的慢查询。
offset判断标准:LIMIT N, M 中 N < 1000;否则用游标分页。
反模式:
SELECT *
FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC
LIMIT 1000000, 20;执行计划:
type: ALL
rows: 5000000
Extra: Using where; Using filesort正确写法:
-- 游标分页:上一次的最大 created_at 作为起点
SELECT *
FROM orders
WHERE status = 'PAID'
AND created_at < '2026-08-01 10:00:00'
ORDER BY created_at DESC
LIMIT 20;
-- 数据量再大一点:先取 id,再回表
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC
LIMIT 1000000, 20
) t USING (id);COUNT(*) 不一定慢,但要选对写法判断标准:MySQL 8.0 直接用 COUNT(*);不要在 COUNT 里塞表达式。
反模式:
-- 给每行做一次计算
SELECT COUNT(DISTINCT user_id) FROM orders;
-- 用子查询套一层
SELECT COUNT(*) FROM (SELECT user_id FROM orders) t;正确写法:
-- 8.0 下 COUNT(*) 已经被优化为最优路径
SELECT COUNT(*) FROM orders WHERE status = 'PAID';
-- 复杂去重才用 DISTINCT,但要先确认业务真需要
SELECT COUNT(DISTINCT user_id) FROM orders;JOIN 别超过 3 张表,带上连接条件判断标准:
JOIN 都有 ON反模式:
SELECT *
FROM a
JOIN b ON a.b_id = b.id
JOIN c ON b.c_id = c.id
JOIN d ON c.d_id = d.id
JOIN e ON d.e_id = e.id
WHERE a.status = 1;正确写法:
STRAIGHT_JOIN 强制驱动顺序时,先 EXPLAIN 验证SELECT a.id, b.name, c.value
FROM a
JOIN b ON a.b_id = b.id -- b.id 是主键
JOIN c ON b.c_id = c.id -- c.id 是主键
WHERE a.status = 1;ORDER BY 要走索引,否则必慢判断标准:Extra 不出现 Using filesort。
反模式:
SELECT id FROM orders WHERE status = 1 ORDER BY created_at;如果 status 和 created_at 没有联合索引,MySQL 会先过滤,再排序。
正确写法:
-- 联合索引,排序列放最后(最左前缀)
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
SELECT id FROM orders WHERE status = 1 ORDER BY created_at;执行计划对比:
# 错误
type: ref
Extra: Using where; Using filesort
# 正确
type: ref
Extra: Using where一句话:索引的顺序就是排序的顺序。
判断标准:批量操作 ≥ 100 条时,必须合并。
反模式(应用层 for 循环):
for order in orders:
db.execute("INSERT INTO orders (...) VALUES (...)", order)1000 条 = 1000 次网络往返 + 1000 次事务提交。
正确写法:
# 单条 INSERT 多 VALUES
db.execute("INSERT INTO orders (col1, col2) VALUES (?,?), (?,?), (?,?)", rows)
# 或事务包起来
with db.transaction():
for order in orders:
db.execute("INSERT INTO orders (...) VALUES (...)", order)EXPLAIN 再优化typeALLkeyNULLLIKEkeyNULLORIN 或 UNION ALLtype: rangeSELECT *ExtrarowsCOUNT(*)JOIN ≤ 3keyNULLORDER BYUsing filesort
EXPLAIN 一遍 → 对照军规type + rows,再回到对应军规改写法SQL 优化不是“艺术”,是工程。
军规的本质只有一句话:让 MySQL 尽可能少地扫描、尽可能多地走索引、尽可能少地做排序和回表。
记住上面这 10 条,能挡掉 90% 的慢查询;剩下 10% 是真功夫,那就要靠 EXPLAIN 和 SHOW PROFILE 去一点点抠。
阅读原文:点击这里