Django 4.2 MySQL 慢查询需分三层定位:先查 N+1(通过 DEBUG=True + connection.queries)、再验索引是否生效(.explain() + 最左匹配)、最后判分页/聚合是否全表扫描(禁用 OFFSET,改用游标分页)。

直接说结论:Django 4.2 下 MySQL 慢查询不是靠“加索引”或“换写法”单点突破能解决的,而是要分三层定位——先确认是不是 N+1、再看有没有走对索引、最后判断分页或聚合是否触发全表扫描;漏掉任何一层,优化都可能白做。
查出 N+1 查询的真实 SQL 长什么样
Django 的慢,80% 藏在模板或序列化器里,而不是 view 函数里。光看 for obj in qs: 没用,得看到底发了几条 SQL。
- 必须开启
DEBUG = True,然后在 view 执行后立刻查connection.queries,别等响应返回再翻日志 -
connection.queries[-1]是最新一条,但 N+1 是一堆相似 SQL 连续出现,比如 1 条SELECT * FROM article+ 50 条SELECT * FROM tag WHERE article_id = %s - 用
.explain()看单条 QuerySet 的执行计划:Article.objects.select_related("author").explain(),输出里type: ref和key: author_id才算走对索引 - DRF 序列化器里写了
author.name却没select_related("author"),就必然触发 N+1 —— 这种问题connection.queries一眼就能揪出来
select_related 和 prefetch_related 到底该用哪个
选错一个,性能不升反降,还可能报错。
-
select_related只能用于ForeignKey和OneToOneField正向关系,靠 JOIN 拉数据;prefetch_related专治ManyToManyField、反向xxx_set、自定义related_name的集合字段,靠额外查询 + Python 归组 - 错误示范:
Post.objects.select_related("tags")直接抛FieldError;正确写法是Post.objects.prefetch_related("tags") - 嵌套链别贪深:
Order.objects.select_related("user__profile__company")在 MySQL 上可能触发ERROR 1116: Too many tables,尤其部署在共享主机或低配 RDS 时 - 用了
prefetch_related却在模板里只写{{ post.tags.all|join:", " }}是没用的 —— 必须实际迭代或调count()才会触发预取,否则还是 N+1
为什么加了索引还是慢?检查这三件事
索引不是建上就生效,MySQL 有自己的一套“嫌弃逻辑”。
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
立即学习“Python免费学习笔记(深入)”;
- WHERE 条件里对字段用了函数就失效,比如
filter(created_at__date="2023-01-01")→ 改成filter(created_at__gte="2023-01-01", created_at__lt="2023-01-02") - 联合索引必须严格遵循最左匹配:
Index(fields=["status", "created_at"])支持filter(status=1)和filter(status=1, created_at__gt=...),但不支持纯filter(created_at__gt=...) -
.values()或.values_list()会废掉所有select_related/prefetch_related,因为返回的是字典或元组,不是模型实例,后续访问obj.author.name又变 N+1 - 用
EXPLAIN验证:Extra字段出现Using filesort或Using temporary就说明排序/分组没走索引,得补复合索引
深分页卡死?别碰 OFFSET
百万级表下,LIMIT 100000, 20 不是慢,是“在扫表”。游标分页不是可选项,是必选项。
- 用
CursorPagination替代PageNumberPagination,前端传cursor(Base64 编码的排序值),后端只查id__gt=12345这种范围条件 - 排序字段必须有索引,且最好是主键或带索引的
created_at;如果按created_at排序,索引得建为(created_at DESC, id DESC)(MySQL 8.0+) - 同一秒内多条记录时,仅靠
created_at游标会漏数据或重复,必须把id作为第二排序字段并写进 WHERE 条件 - 别在游标查询里混
status=1这种未建索引的条件,否则索引失效,回表严重,游标反而比 OFFSET 还慢
最容易被忽略的点:N+1 常发生在 DRF 的 SerializerMethodField 或模板的 {% for tag in post.tags.all %} 里,而不是 view 的 for 循环中;而游标分页的边界问题(如删除中间数据导致断层)往往在线上压测时才暴露,开发阶段很难复现。

















