本文首發于公眾號:Hunter后端
原文鏈接:Django筆記二十八之資料庫查詢優化匯總
這一篇筆記將從以下幾個方面來介紹 Django 在查詢程序中的一些優化操作,有一些是介紹如何獲取 Django 查詢轉化的 sql 陳述句,有一些是理解 QuerySet 是如何獲取資料的,
以下是本篇筆記目錄:
- 性能方面
- 使用標準的資料庫優化技術
- 理解 QuerySet
- 操作盡量在資料庫中完成而不是在記憶體中
- 使用唯一索引來查詢單個物件
- 如果知道需要什么資料,那么就立刻查出來
- 不要查詢你不需要的資料
- 使用批量的方法
1、性能方面
1. connection.queries
前面我們介紹過 connection.queries 的用法,比如我們執行了一條查詢之后,可以通過下面的方式查到我們剛剛的陳述句和耗時
>>> from django.db import connection
>>> connection.queries
[{'sql': 'SELECT polls_polls.id, polls_polls.question, polls_polls.pub_date FROM polls_polls',
'time': '0.002'}]
僅僅當系統的 DEBUG 引數設為 True,上述命令才可生效,而且是按照查詢的順序排列的一個陣列
陣列的每一個元素都是一個字典,包含兩個 Key:sql 和 time
sql 為查詢轉化的查詢陳述句
time 為查詢程序中的耗時
因為這個記錄是按照時間順序排列的,所以 connection.queries[-1] 總能查詢到最新的一條記錄,
多資料庫操作
如果系統用的是多個資料庫,那么可以通過 connections['db_alias'].queries 來操作,比如我們使用的資料庫的 alias 為 user:
>>> from django.db import connections
>>> connections['user'].queries
如果想清空之前的記錄,可以呼叫 reset_queries() 函式:
from django.db import reset_queries
reset_queries()
2. explain
我們也可以使用 explain() 函式來查看一條 QuerySet 的執行計劃,包括索引以及聯表查詢的的一些資訊
這個操作就和 MySQL 的 explain 是一樣的,
>>> print(Blog.objects.filter(title='My Blog').explain())
Seq Scan on blog (cost=0.00..35.50 rows=10 width=12)
Filter: (title = 'My Blog'::bpchar)
也可以加一些引數來查看更詳細的資訊:
>>> print(Blog.objects.filter(title='My Blog').explain(verbose=True, analyze=True))
Seq Scan on public.blog (cost=0.00..35.50 rows=10 width=12) (actual time=0.004..0.004 rows=10 loops=1)
Output: id, title
Filter: (blog.title = 'My Blog'::bpchar)
Planning time: 0.064 ms
Execution time: 0.058 ms
之前在使用 Django 的程序中還使用到一個叫 silk 的工具,它可以用來分析一個介面各個步驟的耗時,有興趣的可以了解一下,
2、使用標準的資料庫優化技術
資料庫優化技術指的是在查詢操作中 SQL 底層本身的優化,不涉及 Django 的查詢操作
比如使用 索引 index,可以使用 Meta.indexes 或者欄位里的 Field.db_index 來添加索引
如果頻繁的使用到 filter()、exclude()、order_by() 等操作,建議為其中查詢的欄位添加索引,因為索引能幫助加快查詢
3、理解 QuerySet
1. 理解 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) 資料什么時候被加載
迭代、使用步長分片、使用len()函式獲取長度以及使用list()將QuerySet 轉化成串列的時候資料才會被加載
這幾點情況在我們的第九篇筆記中都有詳細的描述,
3) 資料是怎么被保存在記憶體中的
每一個 QuerySet 都會有一個快取來減少對資料庫的訪問操作,理解其中的運行原理能幫助我們寫出最有效的代碼,
當我們創建一個 QuerySet 的之后,并且資料第一次被加載,對資料庫的查詢操作就發生了,
然后 Django 會保存 QuerySet 查詢的結果,并且在之后對這個 QuerySet 的操作中會重復使用,不會再去查詢資料庫,
當然,如果理解了這個原理之后,用得好就OK,否則會對資料庫進行多次查詢,造成性能的浪費,比如下面的操作:
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])
上面的代碼,同樣一個查詢操作,系統會查詢兩遍資料庫,而且對于資料來說,兩次的間隔期之間,Entry 表可能的某些資料庫可能會增加或者被洗掉造成資料的不一致,
為了避免此類問題,我們可以這樣復用這個 QuerySet :
>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # 查詢資料庫
>>> print([p.pub_date for p in queryset]) # 從快取中直接使用,不會再次查詢資料庫
這樣的作業系統就只執行了一遍查詢操作,
使用陣列的切片或者根據索引(即下標)不會快取資料
QuerySet 也并不總是快取所查詢的結果,如果只是獲取一個 QuerySet 部分資料,會查詢有是否這個 QuerySet 的快取
有的話,則直接從快取中獲取資料,沒有的話,后續也不會將這部分資料快取到系統中,
舉個例子,比如下面的操作,在快取整個 QuerySet 資料前,查詢一個 QuerySet 的部分資料時,系統會重復查詢資料庫:
>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # 查詢資料庫
>>> print(queryset[5]) # 再次查詢資料庫
而在下面的操作中,整個 QuerySet 都被提前獲取了,那么根據索引的下標獲取資料,則能夠從快取中直接獲取資料:
>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # 查詢資料庫
>>> print(queryset[5]) # 使用快取
>>> print(queryset[5]) # 使用快取
如果一個 QuerySet 已經快取到記憶體中,那么下面的操作將不會再次查詢資料庫:
>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)
2. 理解 QuerySet 的快取
除了 QuerySet 的快取,單個 model 的 object 也有快取的操作,
我們這里簡單理解為外鍵和多對多的關系,
比如下面外鍵欄位的獲取,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 回圈里篩選資料
- 在同一個 model 的操作中,如果有涉及到其他欄位的操作,可以用到 F 運算式
- 使用 annotate 函式在資料庫中做聚合(aggregate)的操作
如果某些查詢比較復雜,可以使用原生的 SQL 陳述句,這個操作也在前面有過一篇完整的筆記介紹過
5、使用唯一索引來查詢單個物件
在使用 get() 來查詢單條資料的時候,有兩個理由使用唯一索引(unique)或 普通索引(db_index)
一個是基于資料庫索引,查詢會更快,
另一個是如果多條資料都滿足查詢條件,查詢會慢得多,而在唯一索引的約束下則保證這種情況不會發生
所以使用下面的 id 進行匹配 會比 headline 欄位匹配快得多,因為 id 欄位在資料庫中有索引且是唯一的:
entry = Entry.objects.get(id=10)
entry = Entry.objects.get(headline="News Item Title")
而下面的操作可能會更慢:
entry = Entry.objects.get(headline__startswith="News")
首先, headline 欄位上沒有索引,會導致資料庫獲取速度慢
其次,查詢并不能保證只回傳一個物件,如果匹配上來多個物件,且從資料庫中檢索并回傳數百數千條記錄,后果會很嚴重,其實就會報錯,get() 能接受的回傳只能是一個實體資料,
6、如果知道需要什么資料,那么就立刻查出來
能一次性查詢所有需要的相關的資料的話,就一次性查詢出來,不要在回圈中做多次查詢,因為那樣會多次訪問資料庫
所以這就需要理解并且用到 select_related() 和 prefetch_related() 函式
7、不要查詢你不需要的資料
1. 使用 values() 和 values_list() 函式
如果需求僅僅是需要某幾個欄位的資料,可以用到的資料結構為 dict 或者 list,可以直接使用這兩個函式來獲取資料
2. 使用 defer() 和 only()
如果明確知道只需要,或者不需要什么欄位資料,可以使用這兩個方法,一般常用在 textfield 上,避免加載大資料量的 text 欄位
3. 使用 count()
如果想要獲取總數,使用 count() 方法,而不是使用 len() 來操作,如果資料有一萬條,len() 操作會導致這一萬條資料都加載到記憶體里,然后計數,
4. 使用 exists()
如果僅僅是想查詢資料是否至少存在一條可以使用 if QuerySet.exists() 而不是 if queryset 的形式
5. 使用 update() 和 delete()
能夠批量更新和洗掉的操作就使用批量的方法,挨個去加載資料,更新資料,然后保存是不推薦的
6. 直接使用外鍵的值
如果需要外鍵的值,直接呼叫早就在這個 object 中的欄位,而不是加載整個關聯的 object 然后取其主鍵id
比如推薦:
entry.blog_id
而不是:
entry.blog.id
7. 如果不需要排序的結果,就不要order_by()
每一個欄位的排序都是資料庫的操作需要額外消耗性能的,所以如果不需要的話,盡量不要排序
如果在 Meta.ordering 中有一個默認的排序,而你不需要,可以通過 order_by() 不添加任何引數的方法來取消排序
為資料庫添加索引,可以幫助提高排序的性能
8、使用批量的方法
1. 批量創建
對于多條 model 資料的創建,盡可能的使用 bulk_create() 方法,這是要優于挨個去 create() 的
2. 批量更新
bulk_update 方法也優于挨個資料在 for 回圈中去 save()
3. 批量 insert
對于 ManyToMany 方法,使用 add() 方法的時候添加多個引數一次性操作比多次 add 要好
my_band.members.add(me, my_friend)
要優于:
my_band.members.add(me)
my_band.members.add(my_friend)
4. 批量 remove
當去除 ManyToMany 中的資料的時候,也是能一次性操作就一次性操作:
my_band.members.remove(me, my_friend)
要好于:
my_band.members.remove(me)
my_band.members.remove(my_friend)
如果想獲取更多后端相關文章,可掃碼關注閱讀:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550850.html
標籤:其他
上一篇:多執行緒(一)
下一篇:返回列表
