在项目中如何优化SQL慢查询?
在项目中如何优化SQL慢查询?
月伴飞鱼慢查询优化的本质是:让数据库用更少的行、更少的随机 I/O、更短的锁持有时间,完成同样的业务语义。
项目里做优化,优先按“定位口径统一 → 找到真实瓶颈 → 用索引与改写降低扫描与回表 → 控制事务与并发 → 验证回归”这条线推进,避免靠猜。
统一定位口径:慢在数据库还是慢在链路
项目现场常见“SQL 慢”的误判来自链路抖动与等待堆积。定位前先把口径对齐到可复现、可量化的指标。
- 关键时间拆分
- 执行时间:数据库真正执行 SQL 的耗时(含 CPU、I/O、锁等待)。
- 排队时间:连接池排队、线程池排队、数据库并发打满后的等待。
- 传输与反序列化时间:结果集过大导致网络与应用端开销显著。
- 先看“慢的形态”
- 偶发尖刺:多为锁冲突、buffer pool 抖动、计划漂移、冷数据 I/O。
- 稳定偏慢:多为缺索引、索引不命中、扫描行数过大、排序/分组开销大。
- 随数据增长变慢:典型是谓词不可用索引、分页深、统计信息滞后、分区策略缺失。
用可证据的方式锁定根因:EXPLAIN 与运行时信息
优化不是“加索引试试”,而是围绕执行计划与运行时指标做取舍。
- 执行计划关注点(以 MySQL/InnoDB 为例)
- type:从好到差通常是
const/ref/range优于index优于ALL(全表扫描)。 - rows:预估扫描行数,越大越危险;和真实返回行数差距大时,统计信息可能失真。
- Extra:
Using filesort:排序未走索引,可能触发磁盘排序或临时表。Using temporary:分组/去重产生临时表,数据量大时非常慢。Using index:覆盖索引(不回表)通常更快。
- type:从好到差通常是
- 运行时补充证据
- 慢日志(slow log)+
EXPLAIN ANALYZE(支持的版本)看真实耗时分布。 SHOW ENGINE INNODB STATUS、performance_schema 看锁等待与热点。- 观察 buffer pool 命中率与磁盘读,判断是 CPU 绑定还是 I/O 绑定。
- 慢日志(slow log)+
索引优化:让过滤与连接“先缩小集合”
大部分慢查询来自“扫描太多行”。索引策略要围绕 WHERE、JOIN、ORDER BY、GROUP BY 的组合语义设计。
1)复合索引按“过滤优先、排序靠后”的原则排
- 经验法则
- 等值过滤列在前,范围过滤列在后;
ORDER BY能与索引顺序一致则放在范围列之后往往失效。 - 能显著减少行数的列优先(高选择性)。
- 等值过滤列在前,范围过滤列在后;
- 常见示例
1 | WHERE tenant_id=? AND status=? AND created_at BETWEEN ? AND ? ORDER BY created_at DESC |
* 候选索引:`(tenant_id, status, created_at)`,减少扫描并支持按 created_at 范围与排序(与具体版本、排序方向相关)。
- 误区
- 只给单列索引,导致优化器无法同时利用多个条件,最终仍大范围扫描。
- 给低选择性列(如性别、布尔值)单独建索引,收益低且写入成本高。
2)覆盖索引:用更窄的索引减少回表
回表:通过二级索引找到主键后,再回到聚簇索引取其它列。回表次数大时很慢。
- 做法
- 将查询必要列尽量放进索引(在 MySQL 中索引列本身构成覆盖,不同于部分数据库的 INCLUDE 语法)。
- 控制索引宽度,避免把大字段(TEXT/BLOB)放入索引。
- 适用场景
- 列表页只展示少量字段:
SELECT id, name, created_at ...很适合做覆盖索引。
- 列表页只展示少量字段:
3)JOIN 索引:驱动表小、被驱动表走索引
- 核心点
- JOIN 条件列必须有索引,尤其是被驱动表的关联键。
- 让过滤更强的一侧做驱动表,先过滤再 JOIN,减少嵌套循环次数。
- 典型坑
- JOIN 条件上对列做函数/类型转换,索引失效(如
DATE(created_at)=...、CAST(id AS CHAR)=...)。
- JOIN 条件上对列做函数/类型转换,索引失效(如
SQL 改写:减少“不可索引”的表达式与不必要的工作
索引不是万能的,很多慢查询需要改写语义等价但更“可执行”。
- 避免对索引列做函数与隐式转换
- 坏:
WHERE DATE(created_at)='2026-01-13' - 好:
WHERE created_at >= '2026-01-13 00:00:00' AND created_at < '2026-01-14 00:00:00'
- 坏:
- 用 EXISTS 替代不必要的 DISTINCT/去重 JOIN
- 当目的只是“是否存在关联记录”,
EXISTS往往更省。
- 当目的只是“是否存在关联记录”,
- 控制返回列与结果集大小
- 禁用
SELECT *在高频接口中尤为关键;字段越多,I/O、网络与反序列化越重。
- 禁用
- 深分页改造
- 坏:
LIMIT 100000, 20会扫描并丢弃大量行。 - 好:基于“游标/最后一条记录”的 Keyset Pagination:
WHERE (created_at,id) < (?,?) ORDER BY created_at DESC, id DESC LIMIT 20(需要相应复合索引)。
- 坏:
- OR 条件拆分
OR常导致全表扫描;可改为UNION ALL分支分别走索引,再在应用或外层聚合去重(视业务语义)。
事务与锁:慢的另一半来自等待
线上慢查询经常不是“算得慢”,而是“等得久”。
- 缩短事务
- 减少事务内的业务逻辑与远程调用;把“读-算-写”拆成更短的写事务。
- 降低锁冲突
- 热点行更新(计数器、余额、库存)用分片计数、批量合并、异步化等方式降冲突。
- 避免范围更新误伤:无索引的
UPDATE ... WHERE会锁大量行甚至升级为更重的锁行为。
- 隔离级别与一致性取舍
- 在允许的业务场景用更弱的一致性策略(如读写分离、快照读),换取更低锁等待。
架构层“止血”:把不可避免的重查询移出主库
当 SQL 已较优但业务压力仍大,改造重点转向“减少查询次数、减少主库压力”。
- 缓存
- 热点读用 Redis/本地缓存,关键是失效策略与一致性边界清晰。
- 预聚合与物化
- 报表类 GROUP BY 大查询用汇总表、物化视图(或离线/准实时 ETL)。
- 分区与分库分表
- 时间序列数据用分区表降低单次扫描范围;超大表用水平拆分控制单表尺寸与索引高度。
- 读写分离
- 将读流量导向只读副本,注意主从延迟对“读己之写”的影响。
验证与回归:用数据证明“更快且更稳”
优化后必须验证性能与副作用,避免“某条 SQL 快了,整体更慢”。
- 必做检查点
- 执行计划是否稳定(避免参数变化导致计划漂移)。
- QPS 上升时 P95/P99 是否改善,锁等待是否下降。
- 新增索引对写入延迟、磁盘、备份窗口的影响。
- 线上灰度观察:错误率、CPU、I/O、连接数、慢日志条数。
常见“高收益”清单(项目里最常见的前 10 类)
- 缺少 JOIN/WHERE 关键索引导致
type=ALL - 复合索引顺序不当,过滤不生效或排序走
filesort - 深分页
LIMIT offset SELECT *与超大结果集- 索引列函数/隐式转换
OR导致全表扫描- GROUP BY/ORDER BY 触发临时表与磁盘排序
- 长事务与热点更新导致锁等待
- 统计信息过期或参数敏感导致计划漂移
- 冷数据查询引发大量随机 I/O(可用分区、冷热分离、归档缓解)














