文章目录
- 你的接口为什么慢?(上)——Django Debug Toolbar + EXPLAIN:从看到慢查询到读懂它
- 导入语
- 1 ~> Django Debug Toolbar——把你写的每个 View 的 SQL 全部摊在桌面上
- 1.1 安装与配置
- 1.2 打开页面——看见"SQL"面板
- 1.3 Debug Toolbar 的四个核心面板
- 1.4 从 SQL 面板定位到代码
- 2 ~> EXPLAIN——读懂一条 SQL 的执行计划
- 2.1 基础用法
- 2.2 `type` 字段——最重要的一列
- 2.3 `key` 字段——实际用到的索引
- 2.4 `rows` 字段——预估扫描行数
- 2.5 `Extra` 字段——优化器的补充注释
- 3 ~> 真实案例——一个报表页面的慢查询定位
- 3.1 背景
- 3.2 用 EXPLAIN 定位
- 3.3 修复
- 4 ~> 如何在 Django 中直接用代码输出 EXPLAIN
- 4.1 Java 开发者的类比
- 思考 && 总结
- 结尾
你的接口为什么慢?(上)——Django Debug Toolbar + EXPLAIN:从看到慢查询到读懂它
📖文章简介:接口突然从 50ms 变成 800ms,你第一反应是什么?“加缓存”“改索引”“换 SSD”——但你没看到实际的慢查询长什么样。上篇聚焦性能排查的第一步:用 Django Debug Toolbar 把所有 SQL 查询摊在桌面上,再通过 MySQL 的 EXPLAIN 命令读懂每条查询的执行计划。从安装配置 Debug Toolbar 开始,到定位出具体是哪个 View 的哪条 ORM 触发了慢查询,再到 EXPLAIN 输出中的 type / key / rows / Extra 四个核心字段的解读。配有两个真实案例——一个 N+1 问题的可视化暴露,一个因为缺少索引导致的全表扫描。
🎬 个人主页:源码骑士
❄专栏传送门:《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
2022 年的一天,产品经理在群里发了一张截图——用户详情页加载了 3 秒才出来。DBA 说数据库负载正常,运维说服务器 CPU 才 15%。我打开浏览器访问了一下——确实慢,但不知道慢在哪。
这时候最蠢的做法是"猜"——“可能是索引没建”“可能是 ORM 写得不好”“可能是网络问题”。正确的做法是:先看清楚到底执行了哪些 SQL,每条 SQL 花了多少时间。眼睛看得到,手才改得对。上篇讲清两步——用 Debug Toolbar 把慢查询"抓出来",再用 EXPLAIN 把慢查询"读明白"。
1 ~> Django Debug Toolbar——把你写的每个 View 的 SQL 全部摊在桌面上
1.1 安装与配置
pipinstalldjango-debug-toolbar# settings.pyINSTALLED_APPS+=["debug_toolbar"]MIDDLEWARE=["debug_toolbar.middleware.DebugToolbarMiddleware",]+MIDDLEWARE# 放最前面——尽早介入请求INTERNAL_IPS=["127.0.0.1"]# 只对本地请求显示# urls.pyfromdjango.urlsimportinclude,path urlpatterns+=[path("__debug__/",include("debug_toolbar.urls")),]1.2 打开页面——看见"SQL"面板
部署完后,打开任意页面,右侧会出现一个可折叠的 Debug Toolbar 面板。点开 “SQL” ——你会看到类似这样的东西:
共 47 条查询,总耗时 123.45 ms 1. SELECT "auth_user"."id" FROM "auth_user" WHERE ... 0.23 ms 2. SELECT "books"."id", "books"."title" FROM "books" 0.15 ms 3. SELECT "books"."id" FROM "books" WHERE ... 0.12 ms (重复 99 次...) 47. SELECT "books"."id" FROM "books" WHERE ... 0.11 ms第 3~47 条是重复的!这就是 N+1 的可视化铁证——Debug Toolbar 让你不用猜,一眼就能看到重复查询。
1.3 Debug Toolbar 的四个核心面板
| 面板 | 告诉你什么 |
|---|---|
| SQL | 执行了哪些 SQL、用了多长时间、从哪里发出的(追溯代码位置) |
| Profiling | 哪个 Python 函数耗时最长 |
| Cache | 缓存命中了多少次、miss 了多少次 |
| Request Vars | View 拿到了哪些 GET/POST 参数和 Session 信息 |
1.4 从 SQL 面板定位到代码
Debug Toolbar 的每条 SQL 旁边都有一个"追溯"链接——点击后能看到这条查询是在哪个文件的哪一行调用的:
D:\myproject\books\views.py:23 in book_detail books = Book.objects.filter(author_id=author.id)这就是你真正的排查起始点——不是靠经验蒙,是靠工具指出来的。
2 ~> EXPLAIN——读懂一条 SQL 的执行计划
找到了哪条 SQL 慢,下一步是理解 MySQL 具体是怎么执行它的。EXPLAIN 是数据库的"执行方案 PDF"——它告诉你 MySQL 准备怎么执行这条 SQL。
2.1 基础用法
EXPLAINSELECT*FROMbookWHEREauthor_id=42;你会得到一张这样的表:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | book | ALL | NULL | NULL | 50000 | Using where |
核心字段解释:
2.2type字段——最重要的一列
type代表 MySQL 的访问方式——也就是"怎么找到数据的"。从最好到最差排列:
NULL → 不需要查表(如 SELECT 1) system → 表里只有一行 const → 主键/唯一索引查一行 eq_ref → JOIN 中通过主键关联 ref → 非唯一索引等值匹配 range → 索引范围扫描(用了 > < BETWEEN 等) index → 全索引扫描(扫了整个索引) ALL → 全表扫描 ← 罪魁祸首目标是把查询中的 ALL 和 index 降到至少 range 或 ref。看到type = ALL就是信号——这行查询扫描了整个表。
2.3key字段——实际用到的索引
如果key = NULL——没有用到索引。如果possible_keys不为空但key是 NULL——有索引但优化器选择不用它。这通常意味着索引的选择性太差或者统计信息过期。
2.4rows字段——预估扫描行数
优化器估算的"需要检查的行数"。rows不是实际返回的行数,而是扫描行数。rows = 500000即使最终只返回 10 行——速度仍然很慢。
2.5Extra字段——优化器的补充注释
| Extra 内容 | 含义 | 严重程度 |
|---|---|---|
Using index | 覆盖索引——只扫描了索引,没碰数据行 | ✅ 完美 |
Using where | 在 server 层使用了 WHERE 过滤 | ⚠️ 普通 |
Using filesort | 额外排序操作——没用到排序索引 | 🔴 需要优化 |
Using temporary | 使用了临时表(常用于 GROUP BY) | 🔴 需要优化 |
Using index condition | 使用了索引条件下推(ICP) | ✅ 还不错 |
3 ~> 真实案例——一个报表页面的慢查询定位
3.1 背景
一个日报报表——显示"最近 30 天借出过的图书列表"。Django View 代码:
defdaily_report(request):thirty_days_ago=timezone.now()-timedelta(days=30)records=BorrowRecord.objects.filter(borrowed_at__gte=thirty_days_ago)books=Book.objects.filter(borrow_records__in=records)returnrender(request,"report.html",{"books":books})Debug Toolbar 抓到 SQL 面板:266 条查询,总耗时 1.8 秒。
3.2 用 EXPLAIN 定位
EXPLAINSELECT*FROMborrowrecordWHEREborrowed_at>="2026-06-17";输出:
| type | key | rows | Extra |
|---|---|---|---|
| ALL | NULL | 487230 | Using where |
全表扫描,48 万行。因为borrowed_at列上没有索引——MySQL 被迫扫整个表去找最近 30 天的记录。
3.3 修复
# models.pyclassBorrowRecord(models.Model):borrowed_at=models.DateTimeField(db_index=True)# ← 加索引# 或者用迁移:# ALTER TABLE borrowrecord ADD INDEX idx_borrowed_at (borrowed_at);加了索引后 EXPLAIN 变成:
| type | key | rows | Extra |
|---|---|---|---|
| range | idx_borrowed_at | 1523 | Using index condition |
扫描行数从 48 万降到 1500,查询耗时从 1.8 秒降到 48ms。
4 ~> 如何在 Django 中直接用代码输出 EXPLAIN
fromdjango.dbimportconnection qs=Book.objects.filter(author_id=42)# 获取 Django 生成的 SQLsql,params=qs.query.sql_with_params()# 用原生查询执行 EXPLAINwithconnection.cursor()ascursor:cursor.execute(f"EXPLAIN{sql}",params)forrowincursor.fetchall():print(row)4.1 Java 开发者的类比
如果你来自 Java 背景——Hibernate 的show_sql相当于 Django 的connection.queries,但远没有 Debug Toolbar 好用。MySQL 的 EXPLAIN 在 Java 的 MyBatis 中也同样使用——原理一模一样。只是 Django Debug Toolbar 把它集成到了浏览器面板中,体验更直观。
思考 && 总结
慢查询排查的第一步不是"加索引"——而是:
- 用 Debug Toolbar 把 SQL 全部抓出来——看清楚到底有哪些查询、哪些是重复的。
- 用 EXPLAIN 看懂执行计划——
type=ALL和rows=500000是红旗,key=NULL是没用到索引。 - 确认问题点后再加索引——不要猜,要靠工具指出具体是哪条查询、哪张表。
结尾
上篇到这里。下篇进入索引实战——什么列该建索引、联合索引的最左前缀原则、以及 EXPLAIN 进阶优化技巧。
源码骑士 — 源码级拆解,从底层看透技术
👀关注:跟博主一起从源码视角深耕底层原理
❤️点赞:让优质内容被更多人看见
⭐收藏:核心知识点存好,随用随查
💬评论:分享你的经验或疑问,一起交流
🔄一键四连:别忘了给博主一键四连!
🗡️寄语:工具先告诉你问题在哪,你再动手改——才是正确的排查姿势。
结语:上篇的光是 Debug Toolbar + EXPLAIN——让你的慢查询不再是盲猜。下篇进入索引实战优化。一键四连!