使用Django annotation,提升django查詢效能

liaochangjiang發表於2019-02-05

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
    })
複製程式碼

使用Django annotation,提升django查詢效能

耗時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
    })

複製程式碼

使用Django annotation,提升django查詢效能

耗時457s。

二者的效能差距是巨大的。

打個廣告

關注我的微信公眾號

使用Django annotation,提升django查詢效能

相關文章