跳转至

案例:业务卡顿,用 dbskiter 5 分钟定位慢查询

场景背景

  • 时间:2026-06-10 周二 14:30
  • 现象:客服反馈系统响应慢,用户查询订单页面加载超过 8 秒
  • 数据库:MySQL 8.0.32,业务库 jump
  • 环境:开发团队无专职 DBA,由后端工程师兼管数据库

生活化比喻:数据库就像厨房,订单查询是高峰期点单,慢查询就是厨师(MySQL)在一大堆食材(数据)里翻找,没找到合适的工具(索引),导致上菜慢。


排查步骤

1. 先看整体健康状态(30 秒)

像体检先做血常规,排查先看数据库整体指标:

dbskiter --database=jump monitor health

关键输出

健康评分: 66.4/100(需要关注)
严重问题: 1
高风险问题: 3

重点关注: - 临时表磁盘使用率:61.96%(警告) - 行锁等待次数:147(警告) - 排序合并次数:5753(警告)

解读:这三项指标同时偏高,通常意味着有大量复杂查询在内存里排不下,落到磁盘排序,同时触发了锁竞争。这就像是厨房里既有人在翻箱倒柜(全表扫描),又有人在排队等同一个砧板(锁等待)。


2. 抓取慢查询(1 分钟)

直接定位"最慢的厨师":

dbskiter --database=jump diagnose slow-queries

发现 Top 1 慢查询

SELECT o.*, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.create_time DESC
LIMIT 100;
指标 数值 含义
执行时间 3.8s 远超用户忍耐极限(1s)
扫描行数 1,247,000 全表扫描,没走索引
临时表 磁盘临时表 内存 tmp_table_size 不够,落盘排序
排序方式 filesort 无索引可用,手动排序

3. 分析执行计划(2 分钟)

用 dbskiter 的 SQL 审核功能看 MySQL 是怎么执行这条语句的:

dbskiter --database=jump audit sql "
SELECT o.*, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.create_time DESC
LIMIT 100;
"

审核结果

[WARNING] 全表扫描 detected: orders 表扫描 1,247,000 行
[WARNING] 缺少索引: orders.status + orders.create_time 联合索引
[INFO] JOIN 使用索引: users.id(主键)
[WARNING] SELECT * 返回 28 列,实际可能只需 4 列

4. 推荐索引与优化(1 分钟)

dbskiter --database=jump audit recommend-indexes "
SELECT o.*, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.create_time DESC
LIMIT 100;
"

推荐方案

-- 方案A:覆盖索引(推荐)
CREATE INDEX idx_orders_status_ctime ON orders(status, create_time, user_id);

-- 方案B:如需覆盖 SELECT * 的列,可用覆盖索引
CREATE INDEX idx_orders_covering 
ON orders(status, create_time, user_id, id, amount, status, create_time);

原理说明:索引就像书的目录,把 statuscreate_time 排好序,MySQL 直接翻到对应章节,不用逐页翻(全表扫描)。


解决方案与验证

执行优化

-- 步骤1:创建索引(业务低峰期执行,如凌晨)
CREATE INDEX idx_orders_status_ctime ON orders(status, create_time);

-- 步骤2:改写 SQL,减少返回列(减轻网络传输)
SELECT o.id, o.amount, o.status, o.create_time, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.create_time DESC
LIMIT 100;

验证效果

dbskiter --database=jump audit sql "
SELECT o.id, o.amount, o.status, o.create_time, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.create_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.create_time DESC
LIMIT 100;
"

优化后指标

指标 优化前 优化后 提升
执行时间 3.8s 0.04s 95x
扫描行数 1,247,000 1,200 99.9%
临时表 磁盘 内存 避免磁盘 IO
索引使用 idx_orders_status_ctime 覆盖查询

复盘要点

1. 排查口诀(记住这个顺序)

业务慢 → 看健康 → 抓慢查 → 看执行计划 → 加索引 → 验证

2. 关键判断指标

  • 扫描行数 / 返回行数 > 100:大概率没走索引,要优化
  • 临时表落盘tmp_table_size 不够或查询太复杂
  • filesortORDER BY 字段没索引

3. 常见误区

误区 正确做法
"执行时间 3 秒不算慢" 扫描行数,100 万行扫描就算慢
"SELECT * 方便" 只取需要的列,减少 IO 和网络传输
"加个单列索引就行" 联合索引(status + create_time)比单列索引高效
"索引越多越好" 索引增加写入成本,按需创建,定期清理冗余索引

4. 后续预防

# 启用慢查询日志,长期监控
dbskiter --database=jump inspector run --type performance

# 每周巡检一次,提前发现性能退化
dbskiter --database=jump inspector report --output weekly_report.html

延伸阅读