SQL性能事故主因是15类高危写法,如WHERE未走索引(含隐式类型转换)、函数包裹字段、OFFSET深分页等,修复后QPS稳定、P99延迟降30%~90%。

绝大多数生产 SQL 性能事故,不是因为数据量突增或硬件故障,而是几行看似无害的写法在高并发下被放大成锁、死锁、超时甚至 OOM。下面这 15 类写法,在真实线上环境反复复现过,且修复后 QPS 稳定、P99 延迟下降 30%~90%。
WHERE 条件未走索引(含隐式类型转换)
MySQL / PostgreSQL 中,user_id = '123'(字段是 BIGINT)会触发全表扫描 + 隐式转换,导致该行锁升级为表级锁等待;SQL Server 的 WHERE name = 123(name 是 VARCHAR)同样失效索引。这类语句在压测时可能不显眼,但上线后一并发就卡住。
- 检查执行计划:MySQL 用
EXPLAIN FORMAT=JSON,确认key和rows字段是否合理 - 禁止字符串和数字混用:
id = '1'→ 改为id = 1 - 函数包裹字段:如
WHERE DATE(created_at) = '2024-01-01'→ 改为WHERE created_at >= '2024-01-01' AND created_at
UPDATE / DELETE 无 LIMIT 或无主键条件
在 MySQL 中,UPDATE orders SET status = 'done' WHERE user_id = 123 若 user_id 无索引,会锁全表;即使有索引,若匹配行数达数万,也会持锁时间过长,阻塞后续事务。PostgreSQL 虽无“锁表”概念,但大范围 UPDATE 会显著延长 tuple 锁持有时间,引发等待链。
- 所有线上
UPDATE/DELETE必须带LIMIT(如分批处理)或确保 WHERE 含唯一/主键字段 - 批量更新优先用
IN (id1, id2, ...)替代模糊条件,单次不超过 500 行 - 避免
WHERE status != 'done'这类无法利用索引的谓词
SELECT ... FOR UPDATE / LOCK IN SHARE MODE 在非事务块中执行
Spring Boot 默认 @Transactional 传播行为是 REQUIRED,但若开发者在非事务方法里直接调用 JdbcTemplate.queryForObject("SELECT ... FOR UPDATE", ...),MySQL 会自动开启隐式事务,且不自动提交——锁持续到连接关闭或超时(默认 8 小时),极易堆积成锁等待雪崩。
- 强制所有
FOR UPDATE语句包裹在显式事务中,且事务粒度尽量短 - 禁止在 MyBatis 的
@Select注解中写FOR UPDATE,改用@Update+ 显式事务控制 - PostgreSQL 中对应的是
SELECT ... FOR UPDATE NOWAIT,必须加NOWAIT并捕获SQLState 55P03异常
大字段(TEXT / BLOB)参与 SELECT * 或 ORDER BY
MySQL 的 innodb_buffer_pool_size 默认只缓存索引和热数据页,一旦 SELECT * 返回含 content TEXT 的百万行,会迅速耗尽内存,触发大量磁盘 I/O,最终导致连接池打满、OOM Killer 杀进程。ORDER BY 时若用到了未索引的大字段,还会触发 Using filesort + 临时磁盘表(tmp_table_size 不够时)。
- 永远用明确列名代替
*,尤其避开TEXT/BLOB字段 -
ORDER BY只允许出现在已建索引的字段上;若必须按大字段排序,提前生成摘要字段(如content_md5)并建索引 - 应用层做分页时,禁用
OFFSET大值(如OFFSET 100000),改用游标分页(WHERE id > last_id LIMIT 50)
真正难排查的不是慢 SQL,而是“看起来快、并发一上来就崩”的 SQL。比如一个 UPDATE 平均 20ms,但锁持有时间取决于扫描行数而非执行时间;又比如一个 SELECT FOR UPDATE 在单线程下秒返回,但 50 并发时锁等待队列指数增长——这些细节,往往藏在执行计划的 Extra 列和 INFORMATION_SCHEMA.INNODB_TRX 的 TRX_ROWS_LOCKED 里。










