Django官方文档小结(三) -- ORM数据库性能优化

Django ORM性能优化

#1 环境

Python3.7.3
Django==2.0.7

#2 QuerySet优化

#2.1 性能优化1 – select_related

#2.1 select_related()函数

select_related()函数优化QuerySet只针对一对一字段(OneToOneField)/外键字段(ForeignKey)

from django.db import models
class test(models.Model):
    name = models.CharField(max_length=100)

class Tag(models.Model):
    kk = models.OneToOneField(test,on_delete=models.CASCADE,default="")
    name = models.CharField(max_length=100)

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

class Entry(models.Model):
    blog = models.ForeignKey(
        Blog,
        on_delete=models.CASCADE,
        related_name="entryblogs",
        related_query_name="entryqueryblogs"
    )
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()
    tag = models.OneToOneField(Tag,on_delete=models.CASCADE,default="")

  1. 标准查找
queryset_entry = models.Entry.objects.all()
for foo in queryset_entry:
    foo.blog.name

SQL语句

SELECT "app_entry"."id", "app_entry"."blog_id", "app_entry"."headline", "app_entry"."body_text", "app_entry"."pub_date", "app_entry"."mod_date", "app_entry"."n_comments", "app_entry"."n_pingbacks", "app_entry"."rating", "app_entry"."_order" FROM "app_entry" ORDER BY "app_entry"."_order" ASC; args=()
SELECT "app_blog"."id", "app_blog"."name", "app_blog"."tagline" FROM "app_blog" WHERE "app_blog"."id" = 1; args=(1,)
SELECT "app_blog"."id", "app_blog"."name", "app_blog"."tagline" FROM "app_blog" WHERE "app_blog"."id" = 1; args=(1,)
SELECT "app_blog"."id", "app_blog"."name", "app_blog"."tagline" FROM "app_blog" WHERE "app_blog"."id" = 1; args=(1,)
SELECT "app_blog"."id", "app_blog"."name", "app_blog"."tagline" FROM "app_blog" WHERE "app_blog"."id" = 1; args=(1,)

Entry表中存放外键blog,并且有四条数据,所以在迭代时,会执行4条SQL语句查询

20190527111335-image.png


20190527110903-image.png

  1. 使用select_related()优化
queryset_entry = models.Entry.objects.select_related('blog').all()
for foo in queryset_entry:
    foo.blog.name

SQL语句:

SELECT "app_entry"."id", "app_entry"."blog_id", "app_entry"."headline", "app_entry"."body_text", "app_entry"."pub_date", "app_entry"."mod_date", "app_entry"."n_comments", "app_entry"."n_pingbacks", "app_entry"."rating", "app_entry"."_order", "app_blog"."id", "app_blog"."name", "app_blog"."tagline" FROM "app_entry" INNER JOIN "app_blog" ON ("app_entry"."blog_id" = "app_blog"."id") ORDER BY "app_entry"."_order" ASC; args=()

20190527110931-image.png

由此得出,通过使用 select_related 减少SQL查询的次数来进行优化、提高性能

#2.1.2 select_related()使用
def select_related(self, *fields)
  1. 不传参数

默认会认为是表中的所有外键,包括多层外键

models.Entry.objects.select_related().all()
  1. 传参数

传什么参数,就会优化相应的外键,没有传递的参数,不会被优化

关联多层外键,可以使用双下划线__

models.Entry.objects.select_related('blog').all()
models.Entry.objects.select_related('tag__kk').all()
#2.1.3 总结
  • select_related主要针一对一和多对一关系进行优化。
  • select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  • 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
  • 也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
  • 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。
#2.1.4 打印SQL语句

这里使用 logger 打印SQL语句

在setting.py添加以下代码即可在终端看到执行的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',
        },
    }
}

#2.2 性能优化2 – prefetch_related

prefetch_related()函数优化QuerySet只针对多对多字段(ManyToManyField)

  1. 标准查找
queryset_entry = models.Entry.objects.all()
for foo in queryset_entry:
    foo.authors.all()

SQL语句

SELECT "app_entry"."id", "app_entry"."blog_id", "app_entry"."headline", "app_entry"."body_text", "app_entry"."pub_date", "app_entry"."mod_date", "app_entry"."n_comments", "app_entry"."n_pingbacks", "app_entry"."rating", "app_entry"."tag_id", "app_entry"."_order" FROM "app_entry" ORDER BY "app_entry"."_order" ASC; args=()
SELECT "app_author"."id", "app_author"."name", "app_author"."email" FROM "app_author" INNER JOIN "app_entry_authors" ON ("app_author"."id" = "app_entry_authors"."author_id") WHERE "app_entry_authors"."entry_id" = 1 LIMIT 21; args=(1,)
SELECT "app_author"."id", "app_author"."name", "app_author"."email" FROM "app_author" INNER JOIN "app_entry_authors" ON ("app_author"."id" = "app_entry_authors"."author_id") WHERE "app_entry_authors"."entry_id" = 2 LIMIT 21; args=(2,)
SELECT "app_author"."id", "app_author"."name", "app_author"."email" FROM "app_author" INNER JOIN "app_entry_authors" ON ("app_author"."id" = "app_entry_authors"."author_id") WHERE "app_entry_authors"."entry_id" = 3 LIMIT 21; args=(3,)
SELECT "app_author"."id", "app_author"."name", "app_author"."email" FROM "app_author" INNER JOIN "app_entry_authors" ON ("app_author"."id" = "app_entry_authors"."author_id") WHERE "app_entry_authors"."entry_id" = 4 LIMIT 21; args=(4,)

Entry表中存放外键authors,并且有四条数据,所以在迭代时,会执行4条SQL语句查询

20190527142834-image.png

  1. 使用 prefetch_related()优化
queryset_entry = models.Entry.objects.prefetch_related('authors').all()
for foo in queryset_entry:
    foo.authors.all()

SQL语句

SELECT "app_entry"."id", "app_entry"."blog_id", "app_entry"."headline", "app_entry"."body_text", "app_entry"."pub_date", "app_entry"."mod_date", "app_entry"."n_comments", "app_entry"."n_pingbacks", "app_entry"."rating", "app_entry"."tag_id", "app_entry"."_order" FROM "app_entry" ORDER BY "app_entry"."_order" ASC; args=()
SELECT ("app_entry_authors"."entry_id") AS "_prefetch_related_val_entry_id", "app_author"."id", "app_author"."name", "app_author"."email" FROM "app_author" INNER JOIN "app_entry_authors" ON ("app_author"."id" = "app_entry_authors"."author_id") WHERE "app_entry_authors"."entry_id" IN (1, 2, 3, 4); args=(1, 2, 3, 4)

20190527143114-image.png

prefetch_related() 的使用和 select_related()相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增量和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi行的结果表.prefetch_related

#2.3 性能优化3 – django F()表达式

F表达式在优化中使用场景如下:

在某个表中存在一个字段,这个字段的用途适用于记录博客访问量,字段会随着访客而自增(+1)

一个F()对象表示一个模型字段或注释的列的值。它可以引用模型字段值并使用它们执行数据库操作,而无需将它们从数据库中拉出到Python内存中。相反,Django使用该F()对象生成一个SQL表达式,该表达式描述了数据库级别所需的操作。

  1. 普通自增
obj_entry = models.Entry.objects.all().first()
obj_entry.n_comments +=  1
obj_entry.save()

在这里,我们 obj_entry.n_comments 将数据库的值从数据库中提取到内存中并使用熟悉的Python运算符对其进行操作,然后将对象保存回数据库。


  1. 使用F()表达式优化
obj_entry = models.Entry.objects.all().first()
obj_entry.n_comments = F("n_comments")  1
obj_entry.save()
  • 虽然看起来像是对实例属性的普通Python赋值,但实际上它是描述数据库操作的SQL构造。obj_entry.n_comments = F(“n_comments”) 1
  • 当Django遇到一个实例时F(),它会覆盖标准的Python运算符来创建一个封装的SQL表达式; 在这种情况下,指示数据库增加由…表示的数据库字段 obj_entry.n_comments。
  • 无论是什么价值obj_entry.n_comments,Python都不会知道它 - 它完全由数据库处理。所有Python都通过Django的F()类创建SQL语法来引用该字段并描述操作。
  1. 优点
  • 直接在数据库中操作而不是python
  • 减少一些操作所需的数据库查询次数
  1. 注意

F()操作在 obj.save() 后会持续存在

  • 如果times的值是1,那么经过n次save()之后,times的值是2
obj_entry = models.Entry.objects.all().first()
obj_entry.n_comments +=  1
obj_entry.save() # n_comments == 2
obj_entry.save() # n_comments == 2
obj_entry.save() # n_comments == 2
  • 如果times的值是1,那么经过n次save()之后,times的值是1+n,而不是2,就是因为F()操作在 obj.save() 后会持续存在
obj_entry = models.Entry.objects.all().first()
obj_entry.n_comments = F("n_comments")  1
obj_entry.save() # n_comments == 2
obj_entry.save() # n_comments == 3
obj_entry.save() # n_comments == 4
obj_entry.save() # n_comments == 5

#2.4 性能优化4 – 使用唯一的索引列检索单个对象

使用唯一的索引列检索单个对象

>>> entry = Entry.objects.get(id=10)

会比以下更快:

>>> entry = Entry.objects.get(headline="News Item Title")

因为它id是由数据库索引的,并保证是唯一的。

执行以下操作可能非常慢:

>>> entry = Entry.objects.get(headline__startswith="News")

首先,headline没有索引,这将使底层数据库获取更慢。

其次,查找不保证只返回一个对象。如果查询匹配多个对象,它将从数据库中检索并传输所有对象。如果返回数百或数千条记录,这种惩罚可能会很大。如果数据库位于单独的服务器上,那么惩罚将会加剧,其中网络开销和延迟也是一个因素。

#2.5 性能优化5 – 判断queryset是否为空

QuerySet.exists()

效果好于:

len(queryset)

#2.6 性能优化6 – 获取外键数据对应的id

如果只需要外键值,请使用已有对象上的外键值,而不是获取整个相关对象并获取其主键。即做:

entry.blog.id

好于:

entry.blog_id

#2.7 性能优化7 – 批量插入

在可能的情况下创建对象时,请使用该 bulk_create()方法来减少SQL查询的数量。例如:

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

好于:

Entry.objects.create(headline='This is a test')
Entry.objects.create(headline='This is only a test')

#2.8 性能优化8 – 数据库持久连接

持久连接避免了在每个请求中重新建立与数据库的连接的开销。它们由CONN_MAX_AGE定义连接最大生命周期的参数控制 。可以为每个数据库单独设置。

没有持久化连接,每一个网站的请求都会与数据库建立一个连接。如果数据库不在本地,尽管网速很快,这也将花费20-75ms.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'xxx',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'CONN_MAX_AGE': 600, # 秒 
    }
}
CONN_MAX_AGE描述
0(默认)保留在每个请求结束时关闭数据库连接的历史行为
600(正秒数)要启用持久连接的秒数
None对于无限制的持久连接


未完待续 ! ! !

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值