慢SQL排查手册

慢 SQL 排查手册(面试向)

核心心法一句话:返回行数 ≠ 数据库的工作量 。

查询慢,慢在四个维度之一(或组合):扫得多、问得碎、被问爆、在排队。

复制代码
总耗时 = 单次耗时 × 执行次数
单次耗时 ≈ 扫描行数(索引决定) + 回表/排序成本 + 网络往返次数(N+1决定) + 锁等待

一、问题分类总表

类型 病根 典型场景 慢查询日志能抓到吗
A. 扫得多 扫描行数远大于返回行数 无索引、索引失效、深分页 ✅ 能(单条就慢)
B. 问得碎 一次请求发一堆 SQL N+1、循环内逐条查询 ❌ 抓不到(每条都快)
C. 被问爆 执行次数过多 高频轮询、热点行、无缓存 ❌ 单条快,看总量
D. 在排队 锁等待 / 连接池耗尽 长事务、事务里嵌 RPC 部分(lock wait 日志)

面试金句 :排查慢 SQL 不是找"单条最慢的查询",而是按总耗时 = 单次耗时 × 次数排序找真凶------榜首往往是一条单条 2ms 但每秒执行几千次的蠢查询。


二、A 类:扫得多(扫描量问题)

A1. 无索引全表扫

sql 复制代码
-- 1000 万行订单表,status 没索引
SELECT * FROM orders WHERE status='cancelled' ORDER BY created_at DESC LIMIT 20;
-- 返回 20 行,但全表扫 1000 万行 + 排序
  • 症状 :EXPLAIN 显示 Seq Scan(PG)/ type=ALL(MySQL)
  • 解决 :建索引 (status, created_at)------过滤列在前、排序列在后,扫描和排序一起治
  • 口诀 :WHERE 决定索引前半段,ORDER BY 决定后半段

A2. 索引失效(建了 = 没建)

四种最常见的失效写法:

sql 复制代码
WHERE DATE(created_at) = '2026-09-19'      -- ① 列上套函数
WHERE created_at = '2026-09-19'            -- ② 隐式类型转换(列是 timestamp 传字符串比较时部分场景失效;varchar 列传数字必失效)
WHERE drug_name LIKE '%地平%'               -- ③ 前缀通配(后缀匹配无法走 B-tree)
WHERE a=1 OR b=2                            -- ④ OR 两边非都有索引 → 退化为全表
  • 解决 :
    ① 改范围:created_at >= '2026-09-19' AND created_at < '2026-09-20'
    ② 类型对齐,驱动传对类型
    ③ 前缀匹配用 LIKE '地平%';真要全文检索上 trigram 索引 / ES
    ④ 两边都建索引让优化器 BitmapOr,或改 UNION

A3. 深分页

sql 复制代码
LIMIT 20 OFFSET 999980;   -- 翻到第 5 万页
-- 数据库要"数着跳过"999980 行,摸过 100 万行只为返回 20 行
  • 解决(游标式分页 / 延迟关联):
sql 复制代码
-- 上一页最后一行的 id 作为游标
WHERE id > :last_seen_id ORDER BY id LIMIT 20;   -- 走索引直接定位,永远只摸 20 行

-- 不能改游标时,先窄后宽(延迟关联):
SELECT * FROM orders o
JOIN (SELECT id FROM orders ORDER BY created_at LIMIT 20 OFFSET 999980) t
  ON o.id = t.id;    -- 内层只扫索引拿 20 个 id,外层只回表 20 次
  • 产品侧釜底抽薪:禁止跳页,只给"下一页"(微信/抖音都是这么干的)

三、B 类:问得碎(N+1 问题)

B1. 经典 N+1

复制代码
查 20 条订单          1 条 SQL,5ms ✓
循环每条:查用户、查店铺、查明细   20×3 = 60 条 SQL
页面只返回 20 行,但发了 61 条查询、占连接 100ms+
  • 为什么慢查询日志抓不到 :每条子查询 2ms,远低于阈值;它慢在往返次数,不是单条执行
  • 发现方式:ORM 日志(echo=True / SQLAlchemy event 监听)数一个请求内的 SQL 条数;APM 看接口下挂的 span 数量
  • 解决三板斧 (本项目设备列表实战,见第五节):
    1. JOIN 取回 :一对一关联直接 LEFT JOIN 带出列
    2. IN 批量 :一对多附属数据 WHERE parent_id IN (...) 一次取回,内存分组
    3. GROUP BY 下推:计数/求和永远让数据库聚合,别拉明细回 Python 数
  • 复杂度语言 :往返次数从 O(N) 到 O(1),与数据规模解耦

B2. "取每组最新一条"(N+1 的地狱变体)

sql 复制代码
-- 每台设备取最近一条告警:循环里 N 次 ORDER BY time DESC LIMIT 1
  • 解决:窗口函数一次算完
sql 复制代码
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY device_id ORDER BY created_at DESC) rn
  FROM device_events WHERE event_type='alarm'
) t WHERE rn = 1;
-- 或 PG 专属写法:LATERAL JOIN / DISTINCT ON

四、C 类:被问爆(执行次数问题)

C1. 单条健康,总量爆炸

复制代码
一条查询 3ms 很健康吧?
每秒 3000 次 = 每秒需要 9 秒的数据库时间 → 一台机器算不过来,全线排队
  • 解决优先级 :
    1. 缓存:热点读走 Redis(商品详情、配置类),注意过期策略与一致性
    2. 读写分离:报表/列表打到从库,主库只扛写
    3. 合并请求:前端轮询改 SSE/WebSocket 推送;批量接口代替逐条调用
    4. 限流降级:入口挡住超出容量的请求,保护 DB 是底线

C2. 报表查询混进 OLTP 主库

sql 复制代码
SELECT ... FROM medication_records GROUP BY ... -- 全年明细,扫 500 万行
  • 一条 SQL 打崩在线库的经典姿势
  • 解决:报表走从库 / 离线数仓 / 预聚合汇总表(每天定时算好,查询只读结果)

五、D 类:在排队(锁与连接)

D1. 锁等待

复制代码
事务A: UPDATE devices SET ... WHERE id=5   (没提交,持行锁)
事务B: UPDATE devices SET ... WHERE id=5   (干等 → 表现为"慢查询")
  • 症状 :SQL 本身有索引也慢;pg_stat_activity 里 waiting=true / pg_locks 能看谁堵谁
  • 根因:长事务------事务里嵌了 RPC、人工操作、循环外呼
  • 解决 :事务尽量短小,外部调用移出事务 ;设置 lock_timeout 快速失败

D2. 连接池耗尽(本项目 MQTT 场景同款)

复制代码
每个请求占一条连接 200ms(N+1 版)→ 并发 76 就烧干 15 条的池
每个请求占一条连接 15ms(批量版) → 同样的池能扛 1000 并发
  • 症状:应用侧报"获取连接超时",DB 侧却看不到高负载
  • 解决:缩短单请求连接占用时间(消灭 N+1)、池加监控、排队上限 + 快速失败

六、本项目实战对照:设备列表接口

维度 优化前(commit cadfb87) 优化后(commit 80b2559)
问题类型 B 类 N+1(1+3N 条/请求) 恒定 4 条
写法 循环内逐台查老人/槽位/开仓数(JOIN 写了但只用于过滤,没用上) LEFT JOIN 带列 + IN 批量 + GROUP BY 下推
单条耗时 2~5ms(慢查询日志抓不到) 同左
500 台总耗时 10.6s 174ms
连接占用 随设备数线性增长 恒定小常数

面试完整叙事:

"设备列表要聚合设备、老人、药仓、事件 5 张表,原实现是每台设备 3 条子查询------单条 2ms,

慢查询日志永远抓不到它,但 500 台就是 1501 次往返、10 秒响应。我按'总耗时=单次×次数'

的思路定位到它才是榜首,重构成 JOIN + 批量 IN + GROUP BY 恒定 4 条,压测验证 10.6s→174ms。

同时事件明细走详情页懒加载、列表只留覆盖索引的今日计数,日志大表不进列表路径。

下一步演进是服务端分页------分页治无界扫描,批量治往返次数,两层互补。"


七、排查工具箱与流程

工具

工具 用途 关键用法
慢查询日志 / log_min_duration_statement 抓单条慢的 阈值设 100ms 起步
pg_stat_statements 按总耗时排序找真凶 ORDER BY total_exec_time DESC,次数×均值一起看
EXPLAIN (ANALYZE, BUFFERS) 看单条的执行计划 重点看 Seq Scan、估算行数偏差、排序落盘
APM(SkyWalking/ARMS) SQL 挂回接口 看一个请求 span 里 SQL 条数(抓 N+1 神器)
pg_stat_activity / pg_locks 抓锁等待 谁堵谁一目了然

决策树(拿到"页面慢"的报障)

复制代码
1. 是偶发还是稳定慢?
   偶发 → 查锁等待/长事务(D类)、执行计划漂移
   稳定 → 下一步
2. EXPLAIN 看单条:
   Seq Scan / 大量行被过滤 → A类(索引问题)
   计划正常但接口整体慢 → 数这个请求发了几条 SQL:
     几十上百条 → B类(N+1)
     就几条且单条快 → 看 QPS 和总耗时排名 → C类(被问爆)或 D类(连接池)

八、速背卡

类型 一句话病因 一句话药方
无索引 扫 1000 万返回 20 WHERE+ORDER BY 联合索引
索引失效 函数/类型转换/前缀通配 改写谓词让列"裸奔"
深分页 OFFSET 数着跳 游标 id > last / 延迟关联
N+1 每条快、次数爆炸 JOIN + IN 批量 + GROUP BY 下推
每组最新一条 循环 LIMIT 1 ROW_NUMBER / LATERAL
热点读 单条健康、QPS 太高 缓存 / 读写分离 / 合并请求
长事务 持锁连坐 外部调用移出事务
连接池耗尽 占连接时间太长 缩短单请求 SQL 往返 + 监控

总纲 :总耗时 = 单次耗时 × 执行次数;单次耗时的构成 = 扫描量 + 往返数 + 锁等待。

所有慢 SQL 问题都是这三元的组合,排查就是逐个变量做排除法。

相关推荐
专注API从业者39 分钟前
告别复杂页面解析,OpenClaw 快速搭建电商商品监控与数据分析脚本
大数据·数据库·python·数据挖掘·数据分析
王立志_LEO1 小时前
HTTPX 完整用法总结
python
ggb喔1 小时前
52pojie 的图片工具:为什么无损转换后体积涨了 6 倍、照片方向还是歪的
图像处理·python·算法·计算机视觉·图形渲染·媒体
hnxaoli1 小时前
win10小程序(二十三)新建带时间文件夹
python
企业数字化笔记1 小时前
AI工具参数很多怎么办?预设、表单校验、危险参数与配置审计
java·spring boot·python·音视频
2601_962885722 小时前
如何用 Python 计算 BBI 多空指标?
开发语言·python
必须得开心呀2 小时前
为什么 python-docx 打不开加密的 .docx,Word COM 却能打开
python·word
VidDown2 小时前
上传 2GB 文件别让 Django 接:分片上传、对象存储直传与秒传
python·django·音视频·状态模式·实时音视频·视频编解码·视频
打工仔折腾 AI2 小时前
FaceFusion本地换脸实战:Windows整合包、模型选择与遮罩调参记录
人工智能·windows·后端·python·深度学习·性能优化·ai agent 实战
yi0112 小时前
DAY24: LeetCode 167:两数之和 II|从暴力枚举到左右双指针
笔记·python·算法·leetcode·二分查找·双指针