3、SQL查询优化:慢查询日志分析、执行计划解读、SQL重写技巧
说到SQL优化,我见过太多人一上来就改索引、调参数,结果折腾半天没效果。其实,真正的优化应该从「找到问题」开始。这一章,我就带你走一遍我自己的排查流程——从慢查询日志入手,到读懂执行计划,最后动手重写SQL。
核心思路:先定位慢SQL → 分析瓶颈 → 针对性重写。别跳步骤,跳了容易白忙活。
3.1 慢查询日志分析——先找到「谁」在拖后腿
我刚开始做优化时,习惯直接看业务代码,觉得慢SQL肯定写得烂。后来发现,很多慢查询其实是「偶发」的——平时跑得挺快,一到高峰期就卡死。所以,第一步永远是打开慢查询日志。
3.1.1 开启慢查询日志
在MySQL里,你可以这样配置:
-- 查看当前设置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(临时生效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过2秒就算慢
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录没走索引的查询
嗯,这里要注意:生产环境别直接改全局变量,最好写到配置文件里。我曾经有一次在线上临时开启,结果日志文件疯长,差点把磁盘撑爆。
3.1.2 分析日志内容
慢查询日志长什么样?举个例子:
# Time: 2025-03-15T10:23:45.123456Z
# User@Host: root[root] @ localhost []
# Query_time: 12.345678 Lock_time: 0.001234 Rows_sent: 1000 Rows_examined: 500000
SET timestamp=1742029425;
SELECT * FROM orders WHERE status = 'PENDING' AND create_time > '2025-01-01';
看到没?Query_time 12秒,Rows_examined 50万行,但只返回了1000行。这说明什么?扫描了大量数据,但大部分都过滤掉了。说白了,索引没用好。
我的小技巧:用 pt-query-digest 工具自动分析慢查询日志,它会按「总耗时」排序,帮你快速找到最该优化的SQL。我每次接手新系统,第一件事就是跑一遍这个工具。
3.2 执行计划解读——看懂数据库的「内心戏」
找到慢SQL之后,别急着改。先问问数据库:「你打算怎么执行这条SQL?」答案就在执行计划里。
3.2.1 使用EXPLAIN
在SQL前面加个 EXPLAIN,就能看到执行计划:
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING' AND create_time > '2025-01-01' \G
输出结果大概长这样:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | orders | ALL | idx_status, idx_create_time | NULL | 500000 | Using where |
看到 type: ALL 了吗?这就是全表扫描。明明有索引,为什么没用?
3.2.2 关键字段解读
- type:访问类型。从好到差依次是
system > const > eq_ref > ref > range > index > ALL。看到ALL或index,基本就是优化信号。 - key:实际使用的索引。如果为
NULL,说明没走索引。 - rows:预估扫描的行数。这个数字越大,查询越慢。
- Extra:额外信息。看到
Using filesort或Using temporary,说明排序或分组用了临时表,性能堪忧。
我曾经踩过的坑:有一次看到 type: ref,以为没问题,结果 rows 显示扫描了20万行。后来发现,虽然走了索引,但选择性太差(status字段只有3个值),索引效果几乎等于没有。所以,别只看type,要结合rows一起看。
3.3 SQL重写技巧——动手改,改到点子上
读懂执行计划之后,就该动手了。我总结了几个最常用的重写技巧,都是实战中反复验证过的。
3.3.1 避免使用SELECT *
你想想看,SELECT * 会读取所有列,包括那些你根本用不上的大字段(比如 TEXT、BLOB)。这不仅增加IO,还可能让排序、分组变慢。
-- 慢
SELECT * FROM orders WHERE status = 'PENDING';
-- 快
SELECT id, order_no, amount FROM orders WHERE status = 'PENDING';
3.3.2 合理使用索引覆盖
如果查询的所有字段都在索引里,数据库就不需要回表了。这叫「索引覆盖」,性能提升非常明显。
-- 假设有联合索引 (status, create_time, amount)
-- 这个查询可以完全走索引,不需要回表
SELECT status, create_time, amount FROM orders
WHERE status = 'PENDING' AND create_time > '2025-01-01';
3.3.3 拆分大查询
有时候,一个复杂查询拆成多个简单查询,反而更快。我在项目中遇到过这样一个案例:一个关联5张表的查询跑了30秒,拆成3个简单查询后,总耗时不到2秒。
-- 原SQL(太复杂)
SELECT o.*, u.name, p.product_name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE o.status = 'PENDING' AND o.create_time > '2025-01-01';
-- 拆解后
-- 第一步:查出符合条件的订单ID
SELECT id, user_id, product_id FROM orders
WHERE status = 'PENDING' AND create_time > '2025-01-01';
-- 第二步:分别查询用户和产品信息(用IN或批量查询)
SELECT id, name FROM users WHERE id IN (1,2,3...);
SELECT id, product_name FROM products WHERE id IN (1,2,3...);
我的习惯:拆解查询时,尽量让每一步都走索引。如果第一步能过滤掉90%的数据,后面的查询压力就小多了。
3.3.4 优化排序和分组
排序和分组是最容易出问题的环节。看到 Using filesort,就要警惕了。
-- 慢:排序字段没索引
SELECT * FROM orders WHERE status = 'PENDING' ORDER BY create_time DESC;
-- 快:给 (status, create_time) 建联合索引
-- 这样排序可以直接用索引,避免filesort
3.4 知识体系总览
下面这张图,是我自己总结的SQL优化流程。每次遇到慢查询,我就按这个步骤来,基本不会漏掉关键点。
3.5 避坑指南
最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。
- 别盲目加索引:索引不是越多越好。写操作频繁的表,索引太多反而拖慢插入和更新速度。我见过一个表建了20多个索引,结果写入性能惨不忍睹。
- 注意隐式类型转换:比如
WHERE id = '123',如果id是整型,字符串'123'会触发隐式转换,导致索引失效。我曾经因为这个坑,排查了整整一个下午。 - LIKE查询要小心:
LIKE '%keyword'无法使用索引,但LIKE 'keyword%'可以。如果必须用前模糊匹配,考虑用全文索引。 - 分页查询越往后越慢:
LIMIT 100000, 20会扫描10万行然后丢弃。可以用「游标分页」或「子查询优化」来解决。
总结一下:SQL优化不是玄学,是有章可循的。打开慢查询日志,读懂执行计划,然后针对性地重写SQL。这三步走下来,90%的慢查询都能搞定。剩下的10%,可能需要调整表结构或业务逻辑,那就是另一章的内容了。