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。看到 ALLindex,基本就是优化信号。
  • key:实际使用的索引。如果为 NULL,说明没走索引。
  • rows:预估扫描的行数。这个数字越大,查询越慢。
  • Extra:额外信息。看到 Using filesortUsing temporary,说明排序或分组用了临时表,性能堪忧。

我曾经踩过的坑:有一次看到 type: ref,以为没问题,结果 rows 显示扫描了20万行。后来发现,虽然走了索引,但选择性太差(status字段只有3个值),索引效果几乎等于没有。所以,别只看type,要结合rows一起看。

3.3 SQL重写技巧——动手改,改到点子上

读懂执行计划之后,就该动手了。我总结了几个最常用的重写技巧,都是实战中反复验证过的。

3.3.1 避免使用SELECT *

你想想看,SELECT * 会读取所有列,包括那些你根本用不上的大字段(比如 TEXTBLOB)。这不仅增加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优化流程。每次遇到慢查询,我就按这个步骤来,基本不会漏掉关键点。

SQL查询优化核心流程 1. 慢查询日志 定位慢SQL 2. 执行计划解读 分析瓶颈 3. SQL重写 动手优化 慢查询日志分析 • 开启日志配置 • 分析Query_time • 关注Rows_examined 执行计划解读 • 关注type字段 • 检查key和rows • 分析Extra信息 SQL重写技巧 • 避免SELECT * • 索引覆盖 • 拆分大查询 核心原则:先定位,再分析,最后动手改

3.5 避坑指南

最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。

  • 别盲目加索引:索引不是越多越好。写操作频繁的表,索引太多反而拖慢插入和更新速度。我见过一个表建了20多个索引,结果写入性能惨不忍睹。
  • 注意隐式类型转换:比如 WHERE id = '123',如果id是整型,字符串'123'会触发隐式转换,导致索引失效。我曾经因为这个坑,排查了整整一个下午。
  • LIKE查询要小心LIKE '%keyword' 无法使用索引,但 LIKE 'keyword%' 可以。如果必须用前模糊匹配,考虑用全文索引。
  • 分页查询越往后越慢LIMIT 100000, 20 会扫描10万行然后丢弃。可以用「游标分页」或「子查询优化」来解决。

总结一下:SQL优化不是玄学,是有章可循的。打开慢查询日志,读懂执行计划,然后针对性地重写SQL。这三步走下来,90%的慢查询都能搞定。剩下的10%,可能需要调整表结构或业务逻辑,那就是另一章的内容了。

专注资料整理