当前位置: 首页 > 后端技术 > Python

Django笔记28:数据库查询优化总结

时间:2023-03-26 17:37:01 Python

本篇笔记将从以下几个方面介绍Django在查询过程中的一些优化操作,有的介绍如何获取Django查询转换后的SQL语句,有的是为了了解QuerySet是如何获取数据的。以下是本文的笔记目录:在性能方面,使用标准的数据库优化技术来理解QuerySet操作应该尽可能在数据库中进行,而不是使用内存中的唯一索引来查询单个对象。所需数据使用批处理方式1.性能1.connection.queriesconnection.queries的用法我们前面已经介绍过了。比如我们执行一个查询后,可以通过下面的方式找到我们刚才做的语句和耗时>>>fromdjango.dbimportconnection>>>connection.queries[{'sql':'SELECTpolls_polls.id,polls_polls.question,polls_polls.pub_dateFROMpolls_polls','time':'0.002'}]只有当系统为DEBUG时,如果参数设置为True,上面的命令才会生效,是一个数组排列在查询的顺序。数组的每个元素都是一个字典,包括两个键:sql和timesql是查询时间转换后的查询语句,是查询过程耗时因为记录是按时间顺序排列的,connection.queries[-1]总是可以查询最新记录。多数据库操作如果系统使用多个数据库,可以通过connections['db_alias'].queries进行操作。比如我们使用的数据库的别名是user:>>>fromdjango.dbimportconnections>>>connections['user'].queries如果想清除之前的记录,可以调用reset_queries()函数:从django.db导入reset_queriesreset_queries()2。explain我们也可以通过explain()函数来查看一个QuerySet的执行计划,包括索引和联表查询的一些信息的操作,和MySQL的explain是一样的。>>>print(Blog.objects.filter(title='MyBlog').explain())Seq扫描博客(cost=0.00..35.50rows=10width=12)Filter:(title='MyBlog'::bpchar)也可以添加一些参数来查看更详细的信息:>>>print(Blog.objects.filter(title='MyBlog').explain(verbose=True,analyze=True))SeqScanonpublic。blog(cost=0.00..35.50rows=10width=12)(actualtime=0.004..0.004rows=10loops=1)输出:id,title过滤器:(blog.title='MyBlog'::bpchar)规划时间:0.064ms执行时间:0.058ms在使用Django之前,我也用过一个工具silk,可以用来分析一个界面每一步的耗时。如果你有兴趣,你可以了解一下。2.使用标准的数据库优化技术数据库优化技术是指在查询操作中对SQL本身底层的优化,不涉及索引索引等Django查询操作,可以使用Meta.indexes或者Field.db_index中字段添加索引如果经常使用filter()、exclude()、order_by()等操作,建议对查询的字段添加索引,因为索引可以帮助加快查询速度3、理解QuerySet1。了解QuerySet获取数据的过程1)QuerySet的惰性加载查询的创建不会访问数据库,直到获取到这条查询语句的具体数据,系统才会访问数据库:>>>q=Entry.objects.filter(headline__startswith="What")#禁止访问数据库>>>q=q.filter(pub_date__lte=datetime.date.today())#禁止访问数据库>>>q=q.exclude(body_text__icontains="food")#不访问数据库>>>print(q)#访问数据库比如上面四个语句,只有最后一步,系统才会查询数据库。2)数据加载迭代时,使用stepsize分片,使用len()函数获取长度,使用list()将QuerySet转换为list,数据将被加载。这些要点在我们的第九篇笔记中都有详细的描述。3)数据如何存储在内存中。每个QuerySet都会有一个缓存,以减少对数据库的访问。了解运行原理可以帮助我们写出最高效的代码。当我们创建一个QuerySet并且第一次加载数据时,就会发生对数据库的查询。然后Django会保存这个QuerySet查询的结果,在后续对这个QuerySet的操作中会复用,不会再去查询数据库。当然,如果理解了这个原理,用好就OK了,否则会多次查询数据库,造成性能浪费,比如下面的操作:>>>print([e.headlineforeinEntry.objects.all()])>>>print([e.pub_dateforeinEntry.objects.all()])以上代码,同样的查询操作,系统会查询数据库两次,而for数据,两次间隔之间,可能会增加或删除Entry表中的某些数据库,导致数据不一致。为了避免此类问题,我们可以像这样重用这个QuerySet:>>>queryset=Entry.objects.all()>>>print([p.headlineforpinqueryset])#查询数据库>>>print([p.pub_dateforpinqueryset])#直接从缓存中使用,不再次查询数据库的操作系统只执行一次查询操作。使用数组切片或者根据索引(即下标)都不会缓存数据。QuerySet并不总是缓存查询结果。如果只获取一个QuerySet的一部分数据,就会查询这个QuerySet的缓存是否存在,然后直接从缓存中获取数据。如果没有,这部分数据以后不会缓存到系统中。例如下面的操作,在缓存整个QuerySet数据之前,系统在查询部分QuerySet数据时会重复查询数据库:>>>queryset=Entry.objects.all()>>>print(queryset[5])#查询数据库>>>print(queryset[5])#再次查询数据库下面的操作中,提前获取了整个QuerySet,那么根据索引下标获取的数据可以直接从缓存数据:>>>queryset=Entry.objects.all()>>>[entryforentryinqueryset]#查询数据库>>>print(queryset[5])#使用缓存>>>print(queryset[5])#使用缓存如果一个QuerySet已经被缓存在内存中,下面的操作将不会再次查询数据库:>>>[entryforentryinqueryset]>>>bool(queryset)>>>entryinqueryset>>>list(queryset)2.了解QuerySet缓存除了QuerySet缓存,单个模型对象还有缓存操作。这里我们简单理解为外键,多对多的关系。比如获取如下外键字段,blog是Entry的外键字段:>>>entry=Entry.objects.get(id=1)>>>entry.blog#Blog的实例是通过获取查询数据库>>>entry.blog#第二次获取,使用缓存信息,不会查询数据库,多对多关系的获取每次都会从数据库中重新获取:>>>entry=entry.objects.get(id=1)>>>entry.authors.all()#查询数据库>>>entry.authors.all()#再次查询数据库当然,对于上面的操作,我们可以使用select_related()和prefetch_related()来减少数据库访问,这个的用法在前面的笔记中有介绍。4、尽量在数据库中而不是在内存中完成操作。举几个例子:在大多数查询中,在获取所有数据后,使用filter()和exclude()在数据库中进行过滤,而不是在Python中使用for。循环过滤数据在同一个模型的操作中,如果有涉及其他字段的操作,可以通过F表达式使用annotate函数对数据库进行聚合操作。如果有些查询比较复杂,可以使用原生的SQL语句,这个操作在之前的完整笔记中也有介绍5.使用唯一索引查询单个对象使用get()查询单个数据时,有使用唯一索引(unique)或普通索引(db_index)是基于数据库索引的两个原因,查询会更快,另一个是如果多条数据满足查询条件,查询会多比较慢,而且在唯一索引的约束下,保证不会出现这种情况。所以用下面的id匹配会比在headline字段上匹配快很多,因为id字段是有索引的,在数据??库中是唯一的:entry=Entry.objects.get(id=10)entry=Entry.objects.get(headline="NewsItemTitle")而下面的操作可能会比较慢:entry=Entry.objects.get(headline__startswith="News")首先,标题字段没有索引,这会导致数据库变慢。第二,查询不保证只返回一个对象,如果匹配到多个对象,从数据库中取出几十万条记录返回,后果会很严重。实际上会报错。get()可接受的返回值只能是一个实例数据。6.如果你知道你需要什么数据,那就马上找出来。如果能一次查询到所有需要的相关数据,就可以一次查询完。不要在循环中进行多次查询,因为它会多次访问数据库。你需要了解和使用select_related()和prefetch_related()函数7.不要查询你不需要的数据1.使用values()和values_list()函数。如果需求只是针对某些字段的数据,可以使用的数据结构是dict或者list,可以直接使用这两个函数获取数据2.defer()和only()如果你知道你只需要或者不需要字段数据,都可以使用这两个方法,一般用在textfield3.使用count()如果想得到总数,使用count()方法代替len()来操作。如果有10000条数据,len()操作会导致将这10000条数据加载到内存中,然后统计。4.使用exists()如果只想查询是否至少有一条数据,可以使用ifQuerySet.exists()代替ifqueryset5.使用update()和delete()进行批量更新和删除操作,不推荐使用batch的方式,一条一条加载数据,更新数据,然后保存。6、直接使用外键的值如果需要外键的值,直接调用这个对象中已经存在的字段,而不是加载整个关联对象然后取其主键id,比如推荐:entry。blog_id而不是:entry.blog.id7。如果你不需要排序后的结果,就不要对order_by()中的每个字段进行排序,这是一个需要额外性能的数据库操作,所以如果你不需要,尽量不要排序。如果Meta.ordering中有默认排序,但你不需要,可以不加任何参数,通过order_by()取消排序。数据库加索引有助于提高排序性能8、使用批处理方式1.批量创建对于多个模型数据的创建,尽量使用bulk_create()方式,比去创建()一个接一个。2.批量更新的方式也比循环中forGotosave()中的一个一个的数据要好3.批量插入对于ManyToMany方式,使用add()方法时,一次添加多个参数要好于添加多次。my_band.members.add(me,my_friend)优于:my_band.members.add(me)my_band.members.add(my_friend)4.批量移除ManyToMany中移除数据时,也可以一次性操作:my_band.members.remove(me,my_friend)优于:my_band.members.remove(me)my_band.members.remove(my_friend)