2. 慢查询成因分析:索引缺失、数据量过大、锁竞争、SQL编写不当
慢查询是数据库性能劣化的核心诱因,也是触发应用层 Circuit Breaker 降级的直接导火索。只有精准识别慢查询的根因,才能制定有效的连接池调优与熔断策略。本章从四个维度剖析慢查询的典型成因,并给出诊断方法与量化指标。
2.1 索引缺失:最普遍的“隐形杀手”
索引缺失导致数据库执行全表扫描(Full Table Scan),当表数据量超过百万行时,单次查询的 I/O 开销会从毫秒级飙升到秒级甚至分钟级。
| 场景 | 无索引执行计划 | 有索引执行计划 | 性能差异(百万行) |
|---|---|---|---|
| 等值查询(WHERE id = ?) | TABLE ACCESS FULL | INDEX UNIQUE SCAN | 1000x ~ 10000x |
| 范围查询(WHERE date > ?) | TABLE ACCESS FULL | INDEX RANGE SCAN | 100x ~ 500x |
| 排序(ORDER BY col) | FILESORT(磁盘临时表) | INDEX SCAN(有序输出) | 50x ~ 200x |
| JOIN 关联 | NESTED LOOPS(驱动表全表) | INDEX JOIN | 100x ~ 1000x |
诊断命令:MySQL 使用
EXPLAIN 查看 type 字段,若为 ALL 或 index(全索引扫描)则代表索引缺失或设计不当;PostgreSQL 使用 EXPLAIN ANALYZE 关注 Seq Scan 节点。
避坑指南:并非所有列都适合建索引。高选择性列(如用户ID、订单号)收益最大;低选择性列(如性别、状态码)建索引反而可能因回表开销劣化性能。复合索引应遵循“最左前缀”原则。
2.2 数据量过大:分页与聚合的陷阱
当单表数据量突破千万级甚至亿级时,即使有索引,深度分页(LIMIT 100000, 20)和全表聚合(COUNT(*)、SUM())依然会引发严重的性能问题。
- 深度分页原理:数据库需要扫描并丢弃前 N 行,偏移量越大,扫描行数越多。例如
LIMIT 1000000, 10需要扫描 1,000,010 行。 - 聚合函数陷阱:
COUNT(*)在 InnoDB 中无法像 MyISAM 那样直接读取行数,必须走索引扫描,亿级表耗时可达数十秒。 - 临时表溢出:
GROUP BY或DISTINCT操作若无法使用索引,会在磁盘创建临时表,性能急剧下降。
| 数据量级 | 查询类型 | 典型耗时 | 连接池影响 |
|---|---|---|---|
| 100 万行 | 简单索引查询 | < 10ms | 几乎无影响 |
| 1000 万行 | 深度分页(偏移 50 万) | 500ms ~ 2s | 连接占用时间延长 |
| 1 亿行 | COUNT(*) | 5s ~ 30s | 连接池快速耗尽 |
| 1 亿行 | GROUP BY 无索引 | 10s ~ 60s | 触发 Circuit Breaker |
生产案例:某电商订单表 8000 万行,后台管理页面使用
LIMIT 200000, 20 分页查询,单次查询耗时 12 秒,导致 50 个连接全部被阻塞,前端接口超时率达到 70%。解决方案:改用游标分页(WHERE id > ? LIMIT 20),耗时降至 3ms。
2.3 锁竞争:并发写入的“交通堵塞”
锁竞争在高并发写入场景下尤为突出。行锁、间隙锁、意向锁、元数据锁(MDL)之间的相互等待,会导致查询被阻塞,形成“雪崩式”慢查询。
锁监控命令:
-- MySQL 查看当前锁等待
SELECT * FROM information_schema.INNODB_TRX\G
SELECT * FROM performance_schema.data_lock_waits\G
-- PostgreSQL 查看锁等待
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks ON ...;
2.4 SQL 编写不当:隐式转换与函数陷阱
不规范的 SQL 写法会导致索引失效,即使表上有索引,查询优化器也无法使用。常见陷阱包括:
- 隐式类型转换:字段类型为
varchar,传入数字123,MySQL 会将字段全部转为数字再比较,索引失效。 - 函数包裹索引列:
WHERE DATE(create_time) = '2024-01-01'导致无法使用create_time索引,应改为WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。 - 前导通配符:
LIKE '%keyword'无法使用 B+Tree 索引,应改用全文索引或搜索引擎。 - OR 条件不当:
WHERE a = 1 OR b = 2若 a、b 均有独立索引,优化器可能选择全表扫描,应改为UNION ALL。
| 错误写法 | 索引失效原因 | 正确写法 | 性能提升 |
|---|---|---|---|
WHERE phone = 13800138000 |
隐式转换(varchar→int) | WHERE phone = '13800138000' |
100x+ |
WHERE DATE(create_time) = '2024-01-01' |
函数包裹索引列 | WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02' |
50x+ |
WHERE name LIKE '%张三' |
前导通配符 | 全文索引 MATCH(name) AGAINST('张三') |
100x+ |
WHERE a = 1 OR b = 2 |
OR 导致索引合并低效 | SELECT ... WHERE a = 1 UNION ALL SELECT ... WHERE b = 2 |
10x+ |
生产案例:某金融系统查询语句
WHERE trade_time >= NOW() - INTERVAL 7 DAY 使用了函数 NOW(),导致优化器无法预估行数,选择了错误的执行计划。改写为 WHERE trade_time >= '2024-06-01 00:00:00'(应用层计算好时间常量)后,查询耗时从 3.2s 降至 12ms。
2.5 慢查询成因总结与优先级排序
在实际生产环境中,慢查询往往由多个因素叠加导致。以下按影响范围和修复成本给出优先级排序:
| 优先级 | 成因 | 影响范围 | 修复成本 | 推荐动作 |
|---|---|---|---|---|
| P0 | 索引缺失 | 全局 | 低(加索引) | 立即执行 |
| P1 | SQL 编写不当 | 局部 | 低(改写 SQL) | 代码审查 + 慢查询日志 |
| P2 | 数据量过大 | 局部 | 中(分库分表/归档) | 制定数据生命周期策略 |
| P3 | 锁竞争 | 全局 | 高(业务逻辑改造) | 缩短事务 + 降低隔离级别 |
实战建议:在开启慢查询日志(
slow_query_log = 1,long_query_time = 1)后,使用 pt-query-digest 或 mysqldumpslow 工具定期分析慢查询分布,优先处理出现频率最高的 TOP 10 查询。这是连接池调优和 Circuit Breaker 阈值设定的前置条件。