数据访问案例:慢 SQL
案例说明: 本页日期、业务背景、指标和工具输出均为构造或匿名化教学数据;除引用一手资料外,不应视为可核验的真实事件。排查方法与命令仍需结合目标数据库版本和生产变更流程验证。
这组案例关注 SQL 延迟、执行计划变化与锁等待。应用日志报的 SQL 耗时和数据库自身看到的执行耗时不一定一致,差异常来自连接等待、网络往返或事务包裹。先判断慢在数据库还是应用侧,再深挖。
案例一:执行计划突变导致全表扫描
1.1 事故背景
某查询接口在数据量增长后突然变慢,SQL 语句和索引都没有改过,上线数月一直正常。
1.2 第一步:拿真实执行计划
以 MySQL 为例:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = ? AND status = ?;
-- 或 EXPLAIN FORMAT=JSON ...(不同版本语法不同)输出显示 type=ALL(全表扫描),而历史上是 ref。没有改 SQL,但优化器在新数据分布下选错了索引。
1.3 第二步:看统计信息与数据分布
-- 查看表行数与索引基数(示例)
SHOW TABLE STATUS LIKE 'orders';
SHOW INDEX FROM orders;user_id 的选择性随业务变化大幅下降(某批刷单导致少量 user_id 占了大量行),优化器据此放弃索引。这不是 SQL 写错,是统计信息与实际分布脱节。
1.4 根因与修复
根因是数据分布变化触发执行计划回退到全表扫描。修复是更新统计信息(ANALYZE TABLE,具体以数据库为准),并评估更合适的复合索引或 SQL 改写。教训是「SQL 没动却变慢」要先看执行计划是否漂移,而不是默认加索引。
案例二:锁等待把单行更新拖慢
2.1 事故背景
某库存扣减接口偶发超时,数据库整体负载不高,但个别 SQL 耗时长。
2.2 第一步:看锁等待
-- 查看当前锁等待(以 MySQL InnoDB 为例,视图名随版本变化)
SELECT * FROM performance_schema.data_lock_waits;
-- 或 SHOW ENGINE INNODB STATUS 看 TRANSACTIONS 段存在事务 A 持有行锁未提交,事务 B 的更新被阻塞。阻塞方的事务持有了远超必要范围的锁。
2.3 第二步:回应用看事务边界
// 反例:事务内做非必要操作,延长锁持有
@Transactional
public void deduct(Long skuId) {
updateStock(skuId); // 持行锁
notifyExternal(skuId); // ❌ 远程调用延长锁持有,他人更新被堵
}把远程调用留在事务内,行锁被持有到远程调用结束,并发扣减时大量更新排队等锁。
2.4 根因与修复
根因是事务内远程调用延长了锁持有时间,导致锁等待雪崩。修复是把远程调用移出事务,缩短锁窗口。慢 SQL 日志只能看到更新慢,真正根因在锁等待,必须结合锁视图判断。
案例三:应用侧耗时混入 SQL 耗时
3.1 事故背景
应用监控显示某 SQL 平均 800ms,但数据库端同类语句执行只需 50ms,差距无法解释。
3.2 第一步:拆开获取连接与执行
long t0 = System.nanoTime();
Connection conn = dataSource.getConnection(); // 含获取连接等待
long t1 = System.nanoTime();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
long t2 = System.nanoTime();
// 若 (t1 - t0) 远大于 (t2 - t1),慢在等连接而非执行拆分后发现 750ms 花在 getConnection(),真正执行只有 50ms。应用侧把「等待连接」算进了 SQL 耗时。
3.3 第二步:确认连接等待根因
# 连接池等待线程数与等待时间(示例)
# WaitingThreadCount 高、MaxWait 接近上限连接等待高回指连接池供给问题,而不是 SQL 本身慢。这与 连接池耗尽案例 同源。
3.4 根因与修复
根因是连接池等待被误算进 SQL 耗时,误导了优化方向。修复是分别埋点连接获取与执行阶段,把「等连接」和「真执行」分开统计。教训是应用层 SQL 耗时指标必须扣除连接获取时间,否则会把池问题错判成数据库问题。