Django基础:查询优化之select_related、prefetch_related

发布于 2020-12-08  1,136 次阅读


select_relatedprefetch_related 是Django最常见的数据库层查询性能优化方案,合理的使用可以减少查库次数,从而提升查询性能。当然,不需要的时候也不要乱用,否则会事倍功半。虽然两者的设计目的一致,都是减少查库次数,不过两者的实现方法不同。通过几个例子,了解两个方法的使用。

Example models

本文的查询都基于这些models,最好可以新建一个Django demo,进行调试

from django.db import models

class City(models.Model):
    name = models.CharField(max_length=100)

class Author(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    city = models.ForeignKey(City)  # 城市

class Publisher(models.Model):
    name = models.CharField(max_length=300)

class Book(models.Model):
    name = models.CharField(max_length=300)
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    author = models.ForeignKey(Author)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

class Store(models.Model):
    name = models.CharField(max_length=300)
    book = models.ManyToManyField(Book)

Tips

为了能更直观的查看orm的查询次数,我们可以在settings文件 中加入一段logging配置,可以在console 中执行orm时,同时输出对应SQL,从而更直观的进行性能比较。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level': 'DEBUG',
        },
    }
}

select_realted 用于OneToOneFiled, ForeignKey字段,相当于SQL层面的 INNER JOIN ,在查询时将标记的相

关表关联起来,再取出需要的字段。

*fields 参数

例:查看所有书籍的出版社名字

优化:

未优化:

对比两次查询,可发现在使用select_realted后 ,SQL中使用了INNER JOIN进行连表,从而整个循环只查库一次,反观不加优化的查询,因为需要跨表,导致查库4次,当数据量增大会加大对db层面查询压力。

如果想连续连接好几个表可以如下操作,两者结果相同。

from app.models import Book

Book.objects.all().select_related('author', 'publisher')
# or
Book.objects.all().select_related('author').select_related('publisher') # Django1.7后

有时候我们会遇到更深层次的查询,可以使用__(双下划线),查询外键的外键表字段

例如: 查询所有书作者的所在城市

from app.models import Book

Book.objects.all().select_related('author__city')

无参数

如果未指定了参数,Django会尽可能深的去遍历所有OneToOneFiled, ForeignKey字段。不过不建议使用,因为这样会导致一些问题

  • Django可能会将所有表关联,造成不必要的性能浪费
  • Django本身可能会有定深度上限,会在不知道的跳出遍历,导致与结果不一致

prefetch_related适用于ManyToManyField, OneToManyField(也就是ForeignKey的反向查询),不同于select_relatedprefetch_related的优化方案是,分别查询每个表,通过Python处理表之间的关系,而不是和select_related一样在SQL层通过JOIN语句进行优化。

*lookup 参数

用法与select_related基本相同,也支持链式写法。

from app.models import Store

Store = Store.objects.all().prefetch_related('books')

例: 获取每个书店的所有书籍

优化:

未优化:

通过看使用prefetch_related的查询SQL,可以发现在第二查询的时候,加了WHERE 条件where store.id in (1,2,3),从而查库1+1次,而不是未使用优化时,每次遇到store.books.all(),才去查库,导致查库1+3次。

Tips

如果不确定是否有重复查询,可使用django-debug-toolbar查看。

django-debug-toolbar的安装

第一步:pip install django-debug-toolbar       

第二步:打开项目文件夹settings.py 文件, 把"debug_toolbar"加到INSTALLED_APP里去。

第三步: 打开项目文件夹里的urls.py, 把debug_toolbar的urls加进去。

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.conf.urls import url
from django.contrib import admin
from django.urls import path, include, re_path

from mysite import settings

urlpatterns = [
    path('admin/', admin.site.urls), # admin后台
    path('accounts/', include('allauth.urls')),
    path('accounts/', include('Myaccount.urls',namespace='accounts')),
    path('polls/', include('polls.urls')),
    path('blog/', include('blog.urls')),
]

if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [
        url(r'^__debug__/', include(debug_toolbar.urls)),
    ] + urlpatterns

第四步:  在settings.py里添加中间件

MIDDLEWARE = [
    # ...
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ...
]
第五步:  在settings.py设置本地IP, debug_toolbar只能在localhost本地测试环境下运行。
INTERNAL_IPS = [
# ...
'127.0.0.1',
# ...
]

当你安装好debug_toolbar后,启动django服务器,打开任何一个页面你都可以看到查询数据库所花时间以及是否有相似及重复的查询,如下图所示:

总结

当你查询单个主对象或主对象列表并需要在模板或其它地方中使用到每个对象的关联对象信息时,请一定记住使用select_related和prefetch_related一次性获取所有对象信息,从而提升数据库查询效率,避免重复查询。

  • select_related主要针一对多和多对多关系进行优化;
  • prefetch_related通过分别获取各个表的内容(多对多字段和反向外键关系),然后用Python处理他们之间的关系来进行优化;
  • 两者相比,能使用select_related时,尽量使用。
  • 两种方法均支持双下划线指定需要查询的关联对象的字段名

转载自:https://wzmmmmj.com/2019/01/22/select-related-and-prefetch-related/


一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。