当一个查询需要 10 分钟才能返回结果时,通常意味着它扫描的数据量过大、索引缺失或失效、SQL 写法不合理,也可能是服务器资源配置不当。优化这类查询不能靠猜,需要一套系统化的排查流程:先定位慢查询,再分析执行计划,然后从索引、SQL 写法、库表结构三个层面优化,最后必要时调整服务器参数。本文将按照这个顺序展开,配合具体示例说明每一步该怎么做。

第一步:定位慢查询并打开慢查询日志
优化的起点是找到真正慢的那条 SQL。MySQL 提供了慢查询日志(slow query log),可以记录所有执行时间超过阈值的语句。可以通过下面的方式动态开启:
-- 查看慢查询日志配置 SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; -- 开启慢查询日志(动态方式,重启失效) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 超过1秒的查询都会被记录 SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未使用索引的查询
开启后,日志文件中会记录查询耗时、扫描行数、锁等待时间等关键信息。除了原生的慢日志,也可以使用 pt-query-digest 工具对日志做汇总分析,它会按照总耗时排序,快速找出最值得优化的几条 SQL。这一步非常关键:优化对象应该是那些执行频率高且单次耗时长的查询,而不是随便挑一条看起来复杂的语句。
拿到目标 SQL 后,还要确认几个基础事实:表的数据量多大?查询是偶发慢还是持续慢?是全表慢还是某些条件慢?这些信息决定了后续优化的方向。比如一张千万行的表没有走索引,10 分钟的耗时就很正常,补上索引可能立竿见影。
第二步:用 EXPLAIN 分析执行计划
EXPLAIN 是 MySQL 优化中最重要的工具,它在语句前加上 explain 关键字即可查看优化器选择的执行方案。重点看以下几个字段:
- type:访问类型,从好到差依次是
system > const > eq_ref > ref > range > index > ALL。出现ALL表示全表扫描,通常是慢查询的元凶。 - key:实际使用的索引。如果为 NULL,说明没有索引可用。
- rows:预估扫描行数。10 分钟的查询往往扫描了几百万甚至上千万行。
- Extra:附加信息。出现
Using filesort说明排序没有用到索引,Using temporary说明用了临时表,Using index则是理想情况,表示覆盖索引生效。
EXPLAIN SELECT order_no, amount FROM t_order WHERE user_id = 10086 AND status = 2 ORDER BY create_time DESC LIMIT 20;
如果执行计划显示 type 为 ALL,rows 为上千万,那么问题就清楚了:过滤条件没有索引支持。如果 type 是 ref 或 range 但依然很慢,就要检查是否发生了大量回表,或者排序、分组操作消耗了过多资源。执行计划是后续所有优化决策的依据,务必先看懂它再动手。
另外注意 explain 结果里的 key_len 字段,它可以判断联合索引实际用了几列。如果建了三列的联合索引但 key_len 只覆盖了第一列,说明索引设计与查询条件不匹配,需要调整索引列的顺序。
第三步:索引优化,最直接有效的手段
大多数慢查询的根本原因是索引缺失或使用不当。设计索引时需要遵循几个核心原则。第一是最左前缀原则:联合索引 (a, b, c) 只对以 a 开头的条件有效,查询条件中只有 b 或 c 时无法使用该索引。第二是区分度原则:选择性高的列(如用户 ID)适合做索引前缀,而性别、状态这类只有几个取值的列单独建索引意义不大。
-- 针对上面的查询建立联合索引,等值列在前,排序列在后 ALTER TABLE t_order ADD INDEX idx_user_status_time (user_id, status, create_time);
这个索引设计包含了三层意图:user_id 和 status 作为等值条件放在前面可以精确过滤,create_time 放在最后可以让排序直接利用索引顺序,消除 filesort。建立索引后,原本需要全表扫描加排序的查询会变成一次索引范围定位,性能通常能提升几个数量级。
覆盖索引是另一个重要技巧。如果查询的列全部包含在索引中,MySQL 无需回表读取整行数据,Extra 会显示 Using index。对于高频的列表查询,可以考虑把 select 的字段扩充进索引,用一点写入开销换取查询速度的大幅提升。但要避免无节制地加索引:每个索引都会拖慢写入并占用磁盘空间,一张表保留三到五个高频索引即可。
还要警惕索引失效的常见场景:对索引列使用函数或运算(如 WHERE YEAR(create_time) = 2024)、隐式类型转换(字符串列传了数字)、前导模糊匹配 LIKE '%abc'、以及 OR 连接了无索引的列。这些写法都会让优化器放弃索引,改写 SQL 时要格外注意。
第四步:改写 SQL 与优化深分页
有些慢查询即使加了索引也不够快,问题出在 SQL 写法本身。典型的例子是深分页:LIMIT 1000000, 20 需要扫描并丢弃前一百万行数据,页码越深越慢。常见的优化方案有两种:
-- 方案一:延迟关联,先在索引上定位主键,再回表取数据
SELECT t.order_no, t.amount
FROM t_order t
INNER JOIN (
SELECT id FROM t_order
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 1000000, 20
) tmp ON t.id = tmp.id;
-- 方案二:游标分页,记住上一页最后一条的排序值,避免扫描前页
SELECT order_no, amount FROM t_order
WHERE user_id = 10086 AND create_time < 1690000000
ORDER BY create_time DESC
LIMIT 20;延迟关联方案利用了覆盖索引,子查询只在索引上完成偏移定位,回表只发生 20 次;游标分页则彻底消除了偏移量,适合下拉加载场景。除此之外,还有一些通用的改写技巧:避免 select 星号只取需要的列、用 union all 代替 or、大事务拆小、join 表的数量控制在三张以内、子查询尽量改写为 join。
对于统计类慢查询,可以考虑把实时聚合改为预聚合:用定时任务把结果写入汇总表,查询时直接读汇总数据,避免每次都扫描明细。这是以空间换时间的经典架构手段,在报表场景中效果显著。
第五步:服务器参数与架构层面的补充优化
SQL 和索引之外,服务器配置也会影响查询速度。重点关注这几个参数:innodb_buffer_pool_size 建议设为物理内存的 50% 到 70%,保证热数据尽量驻留内存;innodb_buffer_pool_instances 在高并发下可设为 4 到 8 以减少锁竞争;sort_buffer_size 和 join_buffer_size 影响排序和连接性能,但不宜设置过大以免内存耗尽。
-- 查看缓冲池命中率,理想值应接近 99% SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
如果缓冲池命中率偏低,说明大量读请求落到了磁盘,加大缓冲池或升级内存往往能直接缩短查询时间。此外还可以借助 performance_schema 和 sys 库观察锁等待、临时表使用情况,找出被阻塞的会话。
当单表数据量达到数亿级别,单机优化会触及天花板,此时需要架构层面的手段:按照业务维度做垂直拆分、按照时间或用户 ID 做水平分表、引入读写分离把分析类查询分流到从库,或者把复杂的实时统计迁移到 Elasticsearch、ClickHouse 等专用引擎。这些方案改造成本更高,建议在索引和 SQL 优化穷尽之后再考虑。
总结
把 10 分钟的查询优化到秒级,路径通常是:慢日志定位目标,explain 分析瓶颈,索引修复访问路径,SQL 改写消除不合理操作,最后辅以参数调整和架构升级。实践中超过八成的慢查询问题靠正确建索引就能解决,因此优先把前四步做扎实。同时要建立长效机制,持续监控慢查询日志,在数据量增长前预判性能风险,而不是等用户投诉后再补救。
MySQL慢查询优化索引优化SQL执行计划修改时间:2026-09-01 13:56:46