使用Django annotation,提高django查詢性能

annotation的中文含義是"註解"。正如這名字所暗示的,傳遞給annotate函數的每一個參數,都會以"註解"的形式添加到model queryset返回的每個object裏面。python

和annotate常常在一塊兒使用的是aggregation函數。sql

舉個栗子

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上面這部分代碼:django

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字段。app

舉個反栗子

若是你不知道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。

兩者的性能差距是巨大的。

打個廣告

關注個人微信公衆號

相關文章
相關標籤/搜索