作者:玄同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 优化指南