作者:玄同765 | 适用场景:MySQL/PostgreSQL/Oracle 主流数据库 | 核心价值:覆盖所有生产级慢 SQL 优化场景,附官方文档依据 + 可复现代码

开篇:真实慢 SQL 踩坑现场某游戏公司120 万日活,玩家充值记录表player_recharge有320 万条数据,单条记录约 1KB。原 SQL 用于后台查询「玩家 30 天内充值记录」,在玩家量破 100 万后:

代码语言:javascript复制SELECT * FROM player_recharge

WHERE player_id = '123456'

AND recharge_time > '2024-01-01 00:00:00'

ORDER BY recharge_time DESC;执行时间:14.7 秒(全表扫描 320 万行)报错原因:后台网关超时(默认 10 秒)优化后结果:98 毫秒(扫描 112 行)📌 优化层级 1:基础语法层(成本最低,见效最快)1.1 绝对禁忌:SELECT *危害读取冗余大字段(如recharge_remark文本字段),浪费70%+ 网络 / 内存带宽;无法触发「覆盖索引」。

优化仅查询业务需要的字段:

代码语言:javascript复制-- 优化前:SELECT * (返回18个字段,含3个大文本字段)

-- 优化后:仅查询业务需要的4个字段

SELECT recharge_id, player_id, amount, recharge_time

FROM player_recharge

WHERE player_id = '123456';1.2 严禁在索引字段上做「函数 / 计算」规则对索引字段直接做函数 / 计算,会导致索引失效 ——MySQL 优化器无法预估函数结果的分布,只能全表扫描。

官方依据:MySQL 8.0 Reference Manual 8.3.1: "If you use a function on an indexed column, MySQL may not use the index because it cannot know in advance what the function will return."

错误案例代码语言:javascript复制-- 错误:recharge_time是索引字段,用DATE()函数导致索引失效

SELECT * FROM player_recharge WHERE DATE(recharge_time) = '2024-01-01';正确优化代码语言:javascript复制-- 优化1:直接写时间范围(推荐,兼容所有版本)

SELECT * FROM player_recharge

WHERE recharge_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59.999';

-- 优化2:MySQL 8.0+ 可用「函数索引」(仅当必须用函数时)

CREATE INDEX idx_recharge_time_date ON player_recharge((DATE(recharge_time)));1.3 UNION ALL 替代 UNION区别UNION:自动去重→需要排序 + 对比(O (nlogn) 时间复杂度);UNION ALL:保留重复→直接合并(O (n) 时间复杂度)。优化代码语言:javascript复制-- 无去重需求:必须用UNION ALL

SELECT player_id FROM player_recharge WHERE type=1

UNION ALL

SELECT player_id FROM player_recharge_log WHERE type=1;

-- 有去重需求:用UNION,加索引提升排序效率

SELECT player_id FROM player_recharge WHERE type=1 ORDER BY player_id

UNION

SELECT player_id FROM player_recharge_log WHERE type=1 ORDER BY player_id;1.4 避免 ORDER BY RAND()危害数据量 **>1 万 ** 时,RAND()会为所有行生成随机数并排序,导致 CPU / 内存过载;

优化代码语言:javascript复制-- 适用场景1:表有连续自增ID

SELECT * FROM player

WHERE player_id >= FLOOR(RAND() * (SELECT MAX(player_id) FROM player))

LIMIT 10;

-- 适用场景2:表无连续ID(如UUID)

SELECT * FROM player

ORDER BY create_time LIMIT 1 OFFSET FLOOR(RAND() * (SELECT COUNT(*) FROM player));1.5 批量操作替代循环规则MySQL 批量插入的单条 SQL 值数量建议≤1000(超过可调整max_allowed_packet参数)。

优化代码语言:javascript复制-- 批量插入(兼容所有数据库)

INSERT INTO player (player_id, username) VALUES

('123', '张三'), ('456', '李四'), ('789', '王五');

-- 批量更新(MySQL 8.0+ 用VALUES(),低版本用CASE WHEN)

UPDATE player_recharge

SET status = CASE

WHEN recharge_id = 1 THEN 1

WHEN recharge_id = 2 THEN 2

ELSE status

END

WHERE recharge_id IN (1, 2);📌 优化层级 2:索引核心层(效率提升的「核武器」)2.1 覆盖索引InnoDB 的索引是「B + 树结构」,覆盖索引的叶节点存储索引字段 + 查询所需的所有字段,无需回表查原始数据 —— 效率提升 5-10 倍。

开篇慢 SQL 的覆盖索引优化代码语言:javascript复制-- 联合索引顺序:过滤性最强的player_id→排序字段recharge_time(DESC与查询一致)

-- INCLUDE:将查询所需字段存入索引,避免回表

CREATE INDEX idx_player_recharge_pid_rtime ON player_recharge (player_id, recharge_time DESC) INCLUDE (amount, recharge_id); 验证覆盖索引:用EXPLAIN查看,若Extra字段显示Using index,则为覆盖索引。

2.2 联合索引的「最左匹配原则」核心规则联合索引(a, b, c)的生效条件是从左到右匹配,中间不能断:

查询条件

索引是否生效

生效字段

a=1

a

a=1 AND b=2

a,b

a=1 AND c=3

a(c 断了,仅 a 生效)

b=2 AND c=3

例外:字符串前缀匹配(LIKE 'abc%' 触发索引,LIKE '%abc%' 不触发)2.3 索引避坑指南单表索引≤5 个(太多会增加INSERT/UPDATE/DELETE的维护开销);删除冗余索引:如已有联合索引(a,b),则(a)为冗余索引;唯一索引优先于普通索引(查询效率更高,保证数据一致性);定期维护:用ALTER TABLE table_name ENGINE=InnoDB重建碎片索引。📌 优化层级 3:执行计划层(定位慢 SQL 的「X 光机」)3.1 EXPLAIN 工具使用代码语言:javascript复制-- 基础执行计划

EXPLAIN SELECT * FROM player_recharge WHERE player_id = '123456';

-- 包含执行时间的详细计划(MySQL 5.7+)

EXPLAIN ANALYZE SELECT * FROM player_recharge WHERE player_id = '123456';3.2 执行计划核心字段字段

含义

优化标准

type

访问类型

从坏到好:ALL→index→range→ref→eq_ref→const 目标:至少达到 range

key

实际使用的索引

非 NULL 为正常,NULL 为未使用索引

rows

预计扫描的行数

越小越好,目标:≤1000 行

Extra

额外信息

Using index(覆盖索引)= 优;Using filesort/Using temporary= 需优化

3.3 开篇慢 SQL 的执行计划对比指标

优化前

优化后

type

ALL(全表扫描)

ref(等值匹配)

key

NULL

idx_player_recharge_pid_rtime

rows

3201256

112

Extra

Using filesort

Using index

📌 优化层级 4:高级优化层(大数据量场景)4.1 分库分表适用阈值:单表数据量≥1000 万条垂直分表:将大字段 / 低频字段拆分(如player表的profile拆到player_profile);水平分表:按规则拆成小表(如按player_id取模:player_recharge_0-player_recharge_9);分片规则:推荐按业务主键(如player_id)或时间(如recharge_time)分片。4.2 延迟关联适用场景:大表分页查询(LIMIT offset, size 中 offset≥10000)优化逻辑:将大偏移量的分页拆分为「主键查询 + 关联查询」,减少回表行数。代码语言:javascript复制-- 优化前:慢!

SELECT * FROM player WHERE type=1 ORDER BY create_time DESC LIMIT 1000000, 10;

-- 优化后:快!

SELECT p.* FROM player p

JOIN (

SELECT player_id FROM player WHERE type=1 ORDER BY create_time DESC LIMIT 1000000, 10

) t ON p.player_id = t.player_id;4.3 缓存热点数据适用场景:高频查询(QPS≥1000)且数据更新频率低的场景;失效策略:时间失效:如缓存 30 分钟;主动失效:数据更新时,主动删除对应缓存。🎯 开篇慢 SQL 的完整优化方案优化后 SQL代码语言:javascript复制SELECT recharge_id, amount, recharge_time

FROM player_recharge

WHERE player_id = '123456'

AND recharge_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 23:59:59.999'

ORDER BY recharge_time DESC;验证结果执行时间:98 毫秒(原 14.7 秒);扫描行数:112 行(原 320 万行);执行计划:type=ref,Extra=Using index(覆盖索引生效)。📌 SQL 优化黄金原则从简单到复杂:先优化语法→索引→执行计划→高级优化;用数据说话:优化前用EXPLAIN ANALYZE,优化后用SHOW PROFILES对比时间;结合业务场景:冷数据用「历史归档」,实时数据用「读写分离 + 缓存」;避免过度优化:数据量 < 10 万时,无需复杂索引 / 分库分表;兼容版本:优化方案需兼容生产环境的数据库版本。结尾:SQL 优化的本质SQL 优化的本质不是「写复杂的语法」,而是「减少数据库的 IO 操作」——IO 是数据库最慢的操作,每回表一次就会增加一次 IO。所有优化手段(覆盖索引、延迟关联、分库分表)都是为了减少 IO 次数。

工具推荐:

执行计划:MySQL EXPLAIN ANALYZE、PostgreSQL EXPLAIN ANALYZE;索引优化:Percona Toolkit pt-index-usage;慢 SQL 监控:MySQL slow_query_log、Prometheus+Grafana;官方文档:MySQL 8.0 优化指南