生产SQL超时主因是WHERE未走索引致全表扫描,如函数操作、隐式转换;JOIN需驱动表限流;ORDER BY+LIMIT须覆盖索引;相关子查询应转LEFT JOIN;排查需结合EXPLAIN与慢日志Rows_examined。

WHERE 条件没走索引,全表扫描卡死连接
生产里最常看到的超时,不是因为数据量大,而是 WHERE 里的字段根本没建索引,或者建了但写法绕过了索引。比如对索引列用函数:WHERE YEAR(create_time) = 2024,MySQL 就没法用 create_time 上的索引;又比如隐式类型转换:WHERE user_id = '123'(而 user_id 是 INT),也可能触发全表扫描。
- 检查执行计划必须用
EXPLAIN,重点关注type是否为ALL或index,key列是否为空 - 字符串字段做等值查询,注意前后空格和大小写(如
COLLATION是utf8mb4_bin时'abc'≠'ABC') - 时间范围查询优先用
BETWEEN或闭区间,避免DATE(create_time) = '2024-01-01'这类写法
JOIN 多张大表没加驱动表限制,笛卡尔积爆炸
JOIN 本身不危险,危险的是没控制驱动表顺序、没加过滤条件就直接连三张百万级表。比如 SELECT * FROM orders o JOIN users u ON o.user_id = u.id JOIN products p ON o.product_id = p.id,如果 orders 表没加 WHERE,MySQL 可能先扫全表 orders(100 万行),再对每行去联查 users 和 products,实际扫描行数轻松破亿。
- 始终让最有限制条件的表做驱动表(即
FROM后第一个表),且该表的WHERE条件必须能命中索引 - 避免
SELECT *在多表 JOIN 中出现,只取真正需要的字段,减少临时表和网络传输开销 - 如果必须关联多张大表,考虑先用子查询或 CTE 把驱动表结果集收窄(但注意 MySQL 8.0 以前 CTE 不支持下推条件)
ORDER BY + LIMIT 混用却没覆盖索引,排序成本飙升
ORDER BY created_at DESC LIMIT 20 看似简单,但如果 created_at 没索引,或有索引但 WHERE 条件导致无法复用该索引,MySQL 就得把所有匹配行捞出来再排序——哪怕最后只取 20 行,中间可能已排序 50 万行。
- 覆盖索引要包含
WHERE条件字段 +ORDER BY字段 +SELECT中的非主键字段(否则回表) - 如果是分页深翻(如
LIMIT 10000, 20),即使有索引也慢,应改用游标分页(WHERE id ) -
ORDER BY中混用 ASC/DESC(如ORDER BY a ASC, b DESC)在 MySQL 8.0 之前无法用联合索引优化
子查询写成相关子查询,N+1 查询变 N×M
SELECT id, (SELECT COUNT(*) FROM logs l WHERE l.order_id = o.id) cnt FROM orders o 这种写法,表面看简洁,实则是灾难:外层查 1 万条订单,内层就执行 1 万次 logs 表扫描。比显式 JOIN 还慢,且难以加索引优化。
- 相关子查询尽量转成
LEFT JOIN+GROUP BY,例如上例可改写为SELECT o.id, COUNT(l.order_id) cnt FROM orders o LEFT JOIN logs l ON o.id = l.order_id GROUP BY o.id - 如果子查询只用于判断存在性(如
EXISTS),确认内层WHERE是否能走索引;IN (subquery)在 MySQL 中可能退化为物化临时表,不如EXISTS稳定 - MySQL 8.0.23+ 支持
LATERAL,可安全表达依赖关系,但线上版本普遍不支持,慎用
实际排查时,光看 SQL 写法不够,得结合 SHOW PROCESSLIST 看状态(Sorting result、Creating sort index、Copying to tmp table 都是危险信号),以及慢日志里的 Rows_examined —— 它比执行时间更能暴露问题本质。










