聚合在任何類型的ORM中都是混亂的根源,Django也不例外。其文檔提供了各種示例和備忘表,演示了如何使用ORM對數據進行分組和聚合,但我決定從另一個角度來處理這個問題。
在本文中,我將查詢集和SQL並排放在一起。如果你更熟悉SQL,那麼這就是你的Django GROUP BY速查表。
為了演示不同的GROUP BY查詢,我將使用Django內置Django .contrib.auth應用程式中的模型。
Django ORM會生成帶有長別名的SQL語句。為了簡單起見,我將展示一個Django所執行操作的清理後的、但等效的版本。
要查看Django實際執行SQL的過程,你可以在Django設置中打開SQL日誌。如何計數行我們來看看我們有多少用戶:
計數行是如此常見,以至於Django在QuerySet上為它提供了一個函數。與我們接下來將看到的其他QuerySet不同,count會返回一個數字。
如何使用聚合函數Django還提供了另外兩種計數表中行數的方法。
我們將從aggregate開始:
為了使用aggregate,我們導入了聚合函數Count。該函數接受一個表達式進行計數。在本例中,我們使用主鍵列id的名稱來計數表中的所有行。
聚合會忽略NULL值。有關聚合如何處理NULL的更多信息,請參見《SQL中12個常見錯誤和容易錯過的優化機會》一文。aggregate的結果是一個字典:
鍵的名稱派生自欄位名稱和聚合名稱。在本例中,它是id_count。最好不要依賴這種命名約定,而是提供你自己的名稱:
要aggregate的參數的名稱也就是生成的字典中的鍵的名稱。
如何按欄位分組使用aggregate,我們得到了對整個表應用聚合函數後的結果。這很有用,但通常我們希望對行的分組應用聚合過程。
讓我們通過用戶的活動狀態來對它們進行計數:
這次我們使用了函數annotate。我們使用values和annotate的一個組合去生成一個GROUP BY :
順序很重要:在annotate之前對values的調用失敗的話將不會產生聚合結果。
與aggregate一樣,要annotate的參數的名稱是經過計算的的QuerySet結果中的鍵。在本例中是total。
如何使用Group By篩選一個QuerySet要對一個篩選後的查詢應用聚合,你可以在查詢中的任何位置使用filter。例如,僅根據員工用戶的活動狀態來對他們進行計數:
就像篩選器一樣,你可以在查詢的任何地方使用order_by來對一個查詢集進行排序:
注意,你可以按照Group By鍵和聚合欄位對該查詢集進行排序。
如何合併多個聚合要生成同一個分組的多個聚合,請添加多個註解:
該查詢將生成活動用戶和非活動用戶的數量,以及用戶加入每個組的最後日期。
如何按多個欄位分組就像執行多個聚合一樣,我們可能也想要按照多個欄位進行分組。例如,按活動狀態和員工狀態進行分組:
這個查詢的結果包括is_active、is_staff和每個分組中的用戶數量。
如何按一個表達式分組GROUP BY的另一個常見用例是按照一個表達式進行分組。例如,計數每年加入的用戶數:
請注意,為了獲取數據的年份,我們在第一次values()調用中使用了特別的表達式<field>__year。此查詢的結果是一個字典,該字典的鍵名稱將是date_joined__year。
有時,內置表達式還不夠,你需要聚合一個更複雜的表達式。例如,按註冊後已經登錄的用戶進行分組:
這裡的表達式相當複雜。我們首先使用annotate來構建這個表達式,並通過在接下來的values()調用中引用該表達式來將其標記為一個GROUP BY鍵。從這裡開始,一切都是一樣的了。
如何使用條件聚合使用條件聚合,你只能聚合分組的一部分。當你有多個聚合時,條件就派上用場了。例如,通過註冊的年份來計數員工和非員工用戶:
上面的SQL語句來自PostgreSQL,它和SQLite是目前唯一支持FILTER語法快捷方式(正式名稱為「選擇性聚合」)的資料庫後端。對於其他資料庫後端,ORM則使用CASE ... WHEN來代替。
我之前寫過關於聚合和篩選器的文章。請參閱我的《處理資料庫的9個Django提示》如何使用HavingHAVING子句用於對一個聚合函數的結果進行篩選。例如,查找超過100個用戶加入的年份:
註解後的欄位total上的filter在生成的SQL中添加了一個HAVING子句。
如何按照不同之處分組對於一些聚合函數,比如COUNT,有時最好是只計數不同的出現特徵。例如,每個用戶的活動狀態有多少個不同的姓氏:
請注意Count調用中distinct=True的使用。
如何使用聚合欄位創建表達式聚合欄位通常只是一個更大問題的第一步。例如,不同的姓氏除以用戶活動狀態的百分比是多少:
第一個annotate()定義了聚合欄位。第二個annotate()使用聚合函數來構造表達式。
如何跨關係分組到目前為止,我們只在單個模型中使用了數據,但是聚合通常是跨關係使用的。比較簡單一點的場景是一對一或外鍵關係。例如,假設我們有一個UserProfile,它與用戶是一對一的關係,我們想要根據profile的類型來計數用戶數量:
就像GROUP BY表達式一樣,在值中使用關係將會根據該關係欄位進行分組。請注意,結果中的用戶profile類型名稱將會是「user_profile_ type」。
如何按多對多關係分組一個更複雜的關係類型是多對多關係。例如,計數每個用戶是多少個分組的成員:
一個用戶可以是多個分組的成員。為了計數該用戶所屬分組的數量,我們在User模型中使用了相關的名稱「groups」。如果我們沒有顯式地設置相關名稱(也沒有顯式地禁用),Django將會自動以{related model model}_set的格式生成一個名稱。例如,group_set。
延伸閱讀想要更深入地了解ORM和GROUP BY,請查看以下連結:
《如何在Django中使用分組集》:一篇關於高級分組技術的文章,比如按cube分組、按rollup分組和按分組集分組。
《如何在SQL中使用Group By獲取一個分組中的第一個或最後一個值》:在PostgreSQL中使用數組的一個小技巧。
《SQL中常見的12個錯誤和易錯過的優化機會》:你在處理數據和編寫SQL時需要知道的一些SQL注意事項。
《Django聚合速查表頁面》:如何執行常見的聚合查詢。
英文原文:https://hakibenita.com/django-group-by-sql