annotation的中文含義是”註解”。正如這名字所暗示的,傳遞給annotate函式的每個引數,都會以”註解”的形式新增到model queryset返回的每一個object裡面。
和annotate經常在一起使用的是aggregation函式。
舉個例子
Blog Model有一個外來鍵entry指向Entry model。我們想計算每個blog有多少個entry:
>>> from django.db.models import Count
>>> q = Blog.objects.annotate(Count(`entry`))
# The name of the first blog
>>> q[0].name
`Blogasaurus`
# The number of entries on the first blog
>>> q[0].entry__count
42
複製程式碼
我們一起break down上面這部分程式碼:
q = Blog.objects.annotate(Count(`entry`))
複製程式碼
這裡使用了Count這個aggregation函式,作用是對一個指定的Blog object,計算它對應的Entry object有多少個。Blog.objects.annotate(Count(`entry`))就是對每個Blog object,計算一下與之對應entry有幾個。返回值是一個queryset。與
Blog.objects.all()
複製程式碼
的區別在於,Blog.objects.annotate(Count(`entry`))中的每一項,都多了一個entry__count欄位,這就是我們想要的那個資料。
q[0].name
q[0].entry__count
複製程式碼
q是一個queryset,q[0]就是獲取第一個object,他裡面多了一個entry__count欄位。
舉個反栗子
如果你不知道annotate這個東西,你肯定會想到一種”pythonic”的方法:
q = Blog.objects.all()
for blog in q:
entry__count = blog.entry.count()
print(blog.name)
print(entry__count)
複製程式碼
這種方法更容易理解,但是會殺死你的效能。假如你有10W條blog,q = Blog.objects.all()
這裡進行了一次查詢,for迴圈那裡,對每一個blog都要進行一次查詢,所以總查詢次數是10W+1次。我們知道:django orm是對sql進行的一層封裝,有封裝自然就會有效能損失。每一次django的查詢,都要從Python層進入資料庫層,然後再從資料庫層進入Python層,即使這樣的一次轉換時間是很短的,但是這麼多次累計起來,消耗的無意義時間是很可觀的。
而前面那種方法,總查詢次數只有一次,從Python層進入資料庫層再回到Python層的次數只有一次,效率當然要高很多!
django orm有一個效能優化技巧:儘可能減少Python層和資料庫層轉換的次數。而Python的for迴圈天然會增加這種轉換次數。所以對於一些簡單的邏輯,可以考慮使用annotate取代for迴圈。
勘誤
很感謝有些朋友指出的,annotate並不一定能減少IO次數。
其實是書本(《資料庫原理及應用》)第九章的問題,查詢優化的問題,用了annotation和不用,看底層如何儲存和存取方法是什麼?文中舉的例項是10w條,第二條是順序遍歷,annotation也不一定會一次都讀到記憶體裡啊,還要看預留緩衝區的大小,每個物理塊存多少條資料,才能決定io次數,查詢效率的高低與查詢邏輯或查詢語句的優略有關,但到最後還是要歸結到底層。
所以用IO次數來解釋效能差異是不嚴謹的,應該用Python層到資料庫層的轉換次數來解釋。
下面來看一個我實際做的一個測試,看看使用annotate和使用for迴圈,效能差異到底有多大:
資料庫中WX_User這個model一共有15W條資料。其中有一個ManyToManyField欄位:
selected_stocks = models.ManyToManyField(Company, blank=True)
複製程式碼
我們想知道每個使用者有多少個selected_stocks。
方法一:annotate
def annotate_test(reuqest):
from django.db.models import Count
import time
start = time.time()
q = WX_User.objects.annotate(
stock_count=Count(`selected_stocks`)
)
data = []
for user in q:
data.append(user.stock_count)
end = time.time()
return JsonResponse({
`spent`: end - start
})
複製程式碼
耗時10.7 s。
方法二:使用for迴圈
def annotate_test2(reuqest):
import time
start = time.time()
q = WX_User.objects.all()
data = []
for user in q:
data.append(user.selected_stocks.count())
end = time.time()
return JsonResponse({
`spent`: end - start
})
複製程式碼
耗時457s。
二者的效能差距是巨大的。
打個廣告
關注我的微信公眾號