

你好,我是空哥,又跟小伙伴们见面了~
"五一"上线期间,遇到了很多 SQL 查询的性能问题。这次记录一个典型的 SQL 慢查询的踩坑与填坑过程。

通过数据库监控工具 DBdoctor 可以看到:

SELECT
id,
...
FROM perf_execute
WHERE channel_company_id IN (?)
AND report_status = ?
AND (debt_amount > ? OR overdue_amount > ?);
优化前:约1秒

DBdoctor 给出的分析:

发现的坑:

DBdoctor 推荐的索引:
ALTER TABLE `perf_execute`
ADD INDEX `dbdoctor_idx__channel_company_id`(`channel_company_id`),
ALGORITHM=INPLACE, LOCK=NONE;
可以减少 SQL 代价消耗 cost 从 10385.3 降至 430.01,SQL 性能提升 2315.13%。
查询耗时:50ms


执行计划
字段 | 值 | 含义 |
|---|---|---|
id | 1 | 查询中的第1个SELECT |
select_type | SIMPLE | 简单查询,不包含子查询或UNION |
table | perf_execute | 操作的表名 |
type | range | 范围扫描,比全表扫描好,但比ref差 |
possible_keys | dbdoctor_idx__channel_company_id | 可能使用的索引 |
key | dbdoctor_idx__channel_company_id | 实际使用的索引 |
key_len | 259 | 使用的索引长度(字节) |
ref | (空) | 因为是range类型,没有等值匹配 |
rows | 393 | 估算需要扫描的行数 |
filtered | 5.56% | 经过条件过滤后剩余的比例 |
Extra | Using index condition; Using where | 使用了索引下推和WHERE过滤 |
虽然加了索引,但 filtered: 5.56% 说明只填了一半的坑:
channel_company_idfiltered 指标。如果太低,说明还有坑没填完。把 report_status、debt_amount、overdue_amount 纳入索引,减少回表。
ALTER TABLE `perf_execute`
ADD INDEX `idx_perf_optimize` (
`channel_company_id`,
`report_status`,
`debt_amount`,
`overdue_amount`
), ALGORITHM=INPLACE, LOCK=NONE;
查询耗时:28ms

指标 | 单列索引 | 复合索引 | 填坑效果 |
|---|---|---|---|
possible_keys | dbdoctor_idx__channel_company_id | dbdoctor_idx__channel_company_id, idx_perf_optimize | 选择更多 |
key | dbdoctor_idx__channel_company_id | idx_perf_optimize | 更优索引 |
key_len | 259 | 261 | 多用了2字节,走了更多列 |
rows | 393 | 390 | 扫描行数略减 |
filtered | 5.56% | 55.55% | 提升10倍 |
Extra | Using index condition; Using where | Using index condition | 去掉Using where |
改善项 | 说明 |
|---|---|
filtered 5.56% → 55.55% | 索引过滤效率提升10倍,回表后有效数据比例大幅提高 |
key_len 259 → 261 | 复合索引使用了更多列(report_status 被用上) |
Extra 简化 | 去掉了 Using where,更多条件在引擎层完成过滤 |
ALTER TABLE `perf_execute`
DROP INDEX `dbdoctor_idx__channel_company_id`;
减少索引维护开销,节省存储空间。
阶段 | 耗时 | filtered | 坑的状态 |
|---|---|---|---|
优化前 | ~1s | - | 全表扫描,大坑 |
单列索引 | ~50ms | 5.56% | 填了一半,还有回表坑 |
复合索引 | ~28ms | 55.55% | 基本填平,推荐长期方案 |
当前 filtered: 55.55%,仍有 44.45% 的数据需要回表过滤,瓶颈在于:
(debt_amount > ? OR overdue_amount > ?)
MySQL 的坑:OR + 两个范围列无法同时在索引中高效利用。
当前 50ms 已很好,复合索引已大幅优化。55.55% 的 filtered 在实际生产中是可以接受的。
1. 拆分 OR 为 UNION(改写 SQL)
SELECT id, ... FROM perf_execute
WHERE channel_company_id IN (?)
AND report_status = ?
AND debt_amount > ?
UNIONALL
SELECTid, ... FROM perf_execute
WHERE channel_company_id IN (?)
AND report_status = ?
AND overdue_amount > ?
AND debt_amount <= ?;
配合两个单列/复合索引,每条子查询的 filtered 有望接近 100%。
2. 覆盖索引(如果查询列不多)
ALTER TABLE `perf_execute`
ADD INDEX `idx_perf_cover` (
`channel_company_id`, `report_status`,
`debt_amount`, `overdue_amount`,
`id`, `...其他select列`
);
让 Extra 变成 Using index,彻底消除回表。
channel_company_id 加单列索引,快速止血filtered,减少回表技巧 | 说明 |
|---|---|
别只看耗时 | filtered 指标更能反映索引的真实过滤效率 |
复合索引有讲究 | 等值条件在前,范围条件在后 |
定期 ANALYZE TABLE | 确保优化器统计信息准确 |
监控 filtered | 长期低于 10%,说明还有坑没填 |