当你发现页面加载变慢,或者数据库连接池频繁打满时,可以先检查Django的数据库查询日志。启用DEBUG=True后,在django.db.backends的日志中观察每条SQL的执行时间和次数。如果发现同一个查询在短时间内反复出现多次,比如超过几十次,说明存在N+1查询问题,这是最常见的性能瓶颈。
这种日志分析是最直接的切入方式,不需要额外工具。先确认现象,再谈优化。很多团队一上来就加缓存或升级数据库,但根本原因是ORM使用不当。建议在本地开发环境或预发布环境开启DEBUG,模拟高负载场景,查看SQL执行情况。
容易误判的地方:N+1查询与批量预加载
新手常误以为少量慢查询才是问题,忽略大量小查询的累积效应。一个典型的例子是在模板中循环关联字段:{% for article in author.articles.all %},如果没有提前预加载,每次循环都会产生一条单独SQL。这种模式在数据量小时不易察觉,当文章数超过几十篇后,页面会明显变慢。
另一个容易误判的是,使用了filter或exclude后关联查询失效。例如Author.objects.prefetch_related('articles').filter(age__gt=30)只会预加载满足条件的作者的文章,之后如果再对单个作者执行.articles.all()可能会再次查询,因为缓存的是原始查询集。正确做法是先过滤再预加载,或者使用Prefetch对象定制子查询。
建议的处理顺序:从select_related和prefetch_related入手
使用select_related和prefetch_related来减少查询次数。对于一对一或外键关联,用select_related一次性JOIN;对于多对多或反向关联,用prefetch_related分两次查询但合并。例如,获取所有作者及其文章时,可以用Author.objects.select_related('profile').prefetch_related('articles')代替循环中逐个查询。注意prefetch_related会额外生成一条IN查询,如果关联数据量超过几百条,需评估是否适合。
具体操作时,先列出当前页面或接口中使用了哪些关联字段,然后判断哪些是一对一或外键,哪些是多对多。每加一个预加载,都通过日志确认查询次数是否减少。如果查询次数降到了个位数,说明优化有效。
配置与检查:使用django-debug-toolbar和EXPLAIN
利用django-debug-toolbar可以在页面侧边栏看到每个请求的SQL数量和耗时。重点关注那些执行次数多但单次快的小查询,它们累积起来会拖慢整体。另外,可以安装django-silk来记录慢查询,并查看调用堆栈,快速定位罪魁祸首的代码行。
对于复杂的过滤和排序,还需要检查索引是否被使用。在数据库层面执行EXPLAIN SELECT ...,查看type列是否为range、ref等。如果出现ALL全表扫描,考虑在过滤字段上添加索引。使用Django ORM的Meta.indexes或db_index字段选项。在复杂过滤条件(如多个字段组合排序、外键加时间范围)上创建联合索引。
风险边界:预加载的滥用与索引平衡
过度使用select_related可能导致JOIN的数据量过大,尤其在关联多张表时,SQL会变得复杂且返回冗余字段,反而增加数据库内存和网络开销。通常建议只预加载当前页面确实会用到的关联,对于不常用的大字段(如TextField),可以用only或defer延迟加载。另外,prefetch_related的查询会在Python层面进行合并,如果关联数据超万条,内存占用会上升,需谨慎。
索引也不是越多越好。写操作频繁的表过多索引会影响插入性能。对于只读且频繁查询的字段,优先加。可以通过EXPLAIN对比添加索引前后的执行计划,确认是否真正消耗资源。
后续维护:持续监控与代码审查
ORM优化不是一次性工作。建议在CI流程中加入SQL查询数量的检查,例如使用pytest-django搭配assertNumQueries。每次添加新功能时,检查对应的ORM查询是否合理。
另外,定期审查日志中的慢查询,结合django-silk的记录,追踪调用来源。如果发现某些预加载导致冗余数据过多,考虑分页或改用values_list获取部分字段。
回滚也不复杂:如果添加预加载后出现性能下降,移除对应的select_related或prefetch_related即可。但要注意,如果其他部分依赖这些预加载的结果,回滚后可能出现N+1查询,需要同步调整模板或序列化器。