慢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_activitywaiting=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 问题都是这三元的组合,排查就是逐个变量做排除法。

相关推荐
troy1281 小时前
告别 Copilot?Codex、Claude Code、DeepSeek Harness、Kimi、ChatGPT 哪个更适合本地化部署,更有性价比?
人工智能·python·开源软件
IvanCodes1 小时前
Python 文件操作(十二):文件与目录的读写
开发语言·python
Y幽谷客9 小时前
Python加载本地大模型(Qwen3.5 8B)
python·大模型
伞伞悦读10 小时前
【第36期】Python 目录与路径详解:pathlib、文件遍历、创建、复制、移动和删除风险
开发语言·python
线上放牧人10 小时前
Windows删除图标缓存
windows·python·pyqt
qq_54702617911 小时前
Python 变量和简单数据类型
python
智搜广告12 小时前
智搜广告:科技行业AI回答优化公司如何破局
大数据·python·elasticsearch·geo
IpdataCloud12 小时前
AI智能体调用工具怎么核验来源IP?归属地、网络类型与代理风险识别(含Python代码)
数据库·python·tcp/ip
2601_9669496513 小时前
只拿到股票代码还不够:量化程序到底还需要哪些标的信息?——从数据建模开始理解股票元数据
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash