SQL 优化实战:从慢查询到索引设计#
线上"接口突然变慢"的头号原因:慢 SQL 。一条 3 秒的查询在高峰能把连接池打满,拖垮整个服务。这篇文章是 SQL 优化的完整实战:定位 → 分析 → 优化 → 验证。
一、定位慢查询#
1
2
3
4
5
6
7
8
9
10
11
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = ON ;
SET GLOBAL long_query_time = 1 ; -- 超过 1 秒记录
-- 2. 查看最近慢查询
SHOW GLOBAL STATUS LIKE 'Slow_queries' ;
-- 慢查询日志里看到:SQL 文本 + 执行时间 + 扫描行数
-- 3. 生产常用:performance_schema
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10 ; -- Top 慢 SQL 排行
二、执行计划解读#
1
2
3
EXPLAIN SELECT * FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY created_at DESC LIMIT 10 ;
关
键
字
t
k
r
E
段
y
e
o
x
:
p
A
y
w
t
U
U
U
e
L
:
s
r
s
s
s
:
L
用
:
a
i
i
i
访
到
扫
:
n
n
n
问
=
的
描
g
g
g
类
索
行
型
全
引
数
f
t
i
(
表
(
(
i
e
n
c
扫
N
越
l
m
d
o
描
U
小
e
p
e
n
(
L
越
s
o
x
s
重
L
好
o
r
t
点
)
r
a
=
优
=
t
r
>
化
y
覆
对
没
=
盖
r
象
用
=
索
e
)
索
排
引
f
引
序
用
(
!
没
了
最
>
)
走
临
优
索
时
)
r
引
表
a
(
(
n
要
要
g
优
优
e
化
化
)
)
>
i
n
d
e
x
>
A
L
L
)
三、索引设计原则#
1. 最左前缀#
1
2
3
-- 联合索引 (user_id, status, created_at)
-- 能用:user_id;user_id+status;user_id+status+created_at
-- 不能用:status(跳过了最左列)!status+created_at 也不行
2. 覆盖索引#
1
2
3
4
-- 查询的字段都在索引里 → 不用回表
CREATE INDEX idx_user_status ON orders(user_id, status, amount);
SELECT user_id, status, amount FROM orders WHERE user_id = 123 ;
-- Extra: Using index(全从索引拿,不回表)→ 快很多
3. 索引下推(ICP)#
1
2
3
4
-- MySQL 5.6+:联合索引里,对二级字段的过滤在索引层做
CREATE INDEX idx_user_status ON orders(user_id, status);
SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID' ;
-- status 的过滤在索引层完成(减少回表次数)
四、常见反模式与修复#
反模式 1:函数导致索引失效#
1
2
3
4
5
-- ❌ 对索引列用函数 → 索引失效
WHERE DATE(created_at) = '2026-09-07'
-- ✅ 改为范围查询(索引可用)
WHERE created_at >= '2026-09-07 00:00:00'
AND created_at < '2026-09-08 00:00:00'
反模式 2:隐式类型转换#
1
2
3
4
-- ❌ user_id 是 varchar 索引列,却传数字
WHERE user_id = 12345678901 -- 数字 → 隐式转 → 索引失效
-- ✅ 传字符串
WHERE user_id = '12345678901'
反模式 3:前缀模糊#
1
2
3
4
5
-- ❌ % 开头的模糊 → 索引失效
WHERE name LIKE '%李%'
-- ✅ 后缀模糊可用索引
WHERE name LIKE '李%'
-- 真需要全文模糊 → 用 ES / 全文索引
反模式 4:OR 连接#
1
2
3
-- ❌ OR 两边字段不同索引 → 可能全表扫
WHERE user_id = 1 OR status = 'PAID'
-- ✅ 拆 UNION 或确认两边都有索引
反模式 5:SELECT *#
1
2
-- ❌ SELECT * → 回表 + 传大字段
-- ✅ 只查需要的字段(可能命中覆盖索引)
五、分页优化(深分页)#
1
2
3
4
-- ❌ 深分页:LIMIT 100000, 20 → 扫 10 万行丢弃
SELECT * FROM orders ORDER BY id LIMIT 100000 , 20 ;
-- ✅ 游标分页(用 id 条件代替 offset)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20 ;
六、一个完整案例#
慢
S
W
O
优
结
E
H
R
化
果
S
L
E
D
:
1
2
3
:
Q
E
R
E
扫
.
.
.
3
L
C
E
R
描
.
:
T
加
L
只
2
查
c
B
1
索
I
查
询
i
Y
2
引
K
必
秒
t
0
:
E
要
3
F
y
u
i
字
→
.
R
=
p
万
d
前
段
2
O
'
d
行
x
缀
(
8
M
杭
a
(
_
模
减
秒
州
t
全
c
糊
少
毫
r
'
e
表
i
问
回
秒
e
_
)
t
题
表
(
s
A
t
,
y
:
)
-
u
N
i
U
_
e
9
m
D
m
s
u
x
9
e
e
i
p
p
.
e
n
d
e
7
x
D
g
a
c
%
p
E
t
t
)
e
S
f
e
_
,
c
C
i
p
扫
t
l
(
o
描
_
L
e
c
s
行
p
I
s
i
i
数
o
M
o
t
t
s
I
r
y
i
1
i
T
t
,
o
2
t
n
0
i
2
u
o
0
p
精
万
n
;
d
确
a
匹
→
L
t
配
I
e
场
2
K
_
景
E
t
走
万
i
'
m
E
%
e
S
后
)
(
端
复
%
杂
'
检
索
)
等
值
+
排
序
七、踩坑记录#
加索引不验证 :加了索引但查询没走(函数/类型转换)→ 用 EXPLAIN 确认 key 用上了
冗余索引 :idx_a、idx_ab、idx_abc 三个索引 → 写放大 + 占用 → 只留 (a,b,c) 和必要单列
只优化单条 SQL 不看整体 :一个查询快了,其他查询被挤掉缓存 → 压测验证整体吞吐
忽略写放大 :核心表加 5 个索引 → 写入慢 3 倍 → 索引要覆盖"读写平衡"
SQL 优化的核心认知:
定位靠慢查询日志 (Top SQL 排行)
解读靠 EXPLAIN (type/key/rows/Extra 四个关键)
索引三原则 :最左前缀、覆盖索引、索引下推
反模式五类 :函数、隐式转换、前缀模糊、OR、SELECT *
验证闭环 :EXPLAIN + 压测,别只"觉得快了"
SQL 优化 80% 是索引问题,20% 是写法问题。 会看 EXPLAIN、懂索引原理,慢 SQL 基本都能压到毫秒级——这是后端的基本功,也是排查线上问题的第一手武器。
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。