数据库连接数突然暴涨、应用频繁报连接超时,排查到最后发现是一条慢SQL在作怪——这种情况在实际运维中太常见了。一条执行十几秒的查询,会一直占着连接不释放,连接池很快就被耗尽。所以说,优化查询语句不只是为了跑得快,更是为了保证连接资源的健康运转。这篇文章就围绕MySQL查询优化展开,从定位问题到具体改写方法,一步步讲清楚。

一、先定位:找出真正拖慢系统的慢SQL
优化不能靠猜,得先用数据说话。MySQL自带慢查询日志,把执行时间超过阈值的SQL记录下来,这是最直接的入口。可以通过下面的配置开启:
-- 查看慢查询配置 SHOW VARIABLES LIKE 'slow_query%'; -- 开启慢查询日志,超过1秒的记录下来 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 记录未走索引的查询,方便发现全表扫描 SET GLOBAL log_queries_not_using_indexes = ON;
日志开启后,用mysqldumpslow或者pt-query-digest工具分析,按执行次数、总耗时排序,优先处理总耗时最高的几条。这里有个容易踩的坑:long_query_time默认值是10秒,很多人以为开了日志却看不到记录,其实是阈值太高了,建议压到1秒甚至0.5秒。
另一种情况是SQL本身不慢,但执行频率极高,累计消耗大量连接时间。这类SQL单看日志不容易发现,需要通过performance_schema或者show processlist观察。如果processlist里大量连接处于Sending data状态,基本可以确定是查询扫描数据量过大导致的。
二、读懂explain执行计划,判断SQL的健康状态
拿到慢SQL后,先别急着改写,用explain看一眼执行计划,弄清楚MySQL到底是怎么执行这条语句的。重点关注这几个字段:
- type:访问类型,从好到差大致是
const、eq_ref、ref、range、index、ALL。出现ALL说明全表扫描,必须处理。 - key:实际使用的索引,如果是
NULL,说明没走索引。 - rows:预估扫描行数,这个数字越大越危险,几百万行的扫描会直接把连接卡死。
- Extra:附加信息,出现
Using filesort和Using temporary要特别留意,说明排序或分组需要额外开销。
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 ORDER BY create_time DESC LIMIT 10;
如果这条语句的type是ALL,rows接近表总量,那就是典型的全表扫描。常见的解法是建联合索引(user_id, create_time),让查询先按用户过滤再按时间排序,type会变成ref,扫描行数直接降到个位数。建索引时注意最左前缀原则,user_id必须放在前面,单独按create_time查询时这个索引用不上。
三、避开索引失效的常见写法
索引建了但没生效,是新手最常遇到的问题。下面几种写法都会导致索引失效,改写时要格外注意。
第一种是隐式类型转换。比如phone字段是varchar类型,查询时写成WHERE phone = 13800138000,数字和字符串比较时MySQL会把每行的phone转成数字,等于对索引列做了函数操作,索引直接失效。正确写法是加引号:WHERE phone = '13800138000'。
第二种是左模糊匹配。LIKE '%abc'以通配符开头,B+树无法定位起点,只能全表扫描。如果确实有按后缀搜索的需求,可以把数据反转一份存到冗余列上,用LIKE 'cba%'查询,或者借助全文索引。
第三种是对索引列使用函数或运算。比如WHERE DATE(create_time) = '2024-01-01',函数作用在列上会破坏索引的有序性。改写成范围查询即可:
-- 错误写法:索引失效 SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 正确写法:可以走 range 扫描 SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';
类似的还有WHERE id + 1 = 100这种对列做运算的写法,改成WHERE id = 99就好。原则就是让索引列保持原样,把变换放到常量那一侧。
四、深分页、join关联与批量写入的优化
分页查询随着页码变深会越来越慢,LIMIT 1000000, 10需要先扫描并丢弃前一百万行。优化的思路是用上一页的末尾记录做游标:
-- 优化前:扫描并丢弃100万行 SELECT * FROM orders ORDER BY id LIMIT 1000000, 10; -- 优化后:利用主键定位,直接从目标位置开始扫描 SELECT * FROM orders WHERE id > 上页最后一条id ORDER BY id LIMIT 10; -- 无法拿到游标时,先用覆盖索引查出主键再回表 SELECT o.* FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) t ON o.id = t.id;
join关联方面,务必保证关联字段上有索引,被驱动表的关联列尤其重要。小表驱动大表的思路依然适用,MySQL 8.0之后优化器大多能自动调整驱动顺序,但如果关联字段类型不一致(比如一边int一边varchar),同样会发生隐式转换导致索引失效,这是join场景里非常隐蔽的坑。
批量写入也不容忽视。循环单条INSERT会产生大量小事务,占用连接还刷 binlog 频繁。改成批量插入,一次提交几百条,事务数量下降两个数量级,连接占用时间也随之缩短。对于超大批量,可以配合LOAD DATA或者分批提交,避免单事务过大导致的锁竞争和回滚段膨胀。
总结一下,查询优化的路径是:先通过慢日志定位问题SQL,再用explain分析执行计划,找出全表扫描和索引失效的原因,最后结合业务场景做针对性改写。语句执行快了,连接自然释放得快,连接池压力和超时问题也就随之缓解。优化是一个持续的过程,建议把慢日志分析纳入日常运维 routine,问题早发现早处理。