Django ORMのGROUP BY入門
グルーピングと集計の方法
Django ORMのvalues()とannotate()を使って、SQLのGROUP BYに相当するグルーピングと集計を行う方法を解説します。
こんな人向けの記事です
- Django ORMでグループ化と集計を行いたい人
- values()とannotate()の組み合わせを学びたい人
- Sum、Avg、Max、Min、Countの使い方を知りたい人
Step 1グルーピングの基本
Django ORMでGROUP BYを行うには、values()でグループ化するフィールドを指定し、annotate()で集計関数を適用します。
Python
# models.py
from django.db import models
class Sale(models.Model):
product = models.CharField(max_length=100)
category = models.CharField(max_length=50)
amount = models.IntegerField()
quantity = models.IntegerField()
sold_at = models.DateField()
def __str__(self):
return f"{self.product} - {self.amount}円"Python
from django.db.models import Count, Sum
# カテゴリごとの件数を集計
# SQL: SELECT category, COUNT(*) FROM sale GROUP BY category
results = Sale.objects.values("category").annotate(
count=Count("id")
)
for r in results:
print(f"{r['category']}: {r['count']}件")
# カテゴリごとの売上合計
results = Sale.objects.values("category").annotate(
total=Sum("amount")
).order_by("-total")
for r in results:
print(f"{r['category']}: {r['total']}円")実行結果
食品: 120件
電子機器: 85件
衣料品: 65件
---
電子機器: 8,500,000円
食品: 3,600,000円
衣料品: 1,950,000円values()とannotate()の順序
values()をannotate()の前に書くとGROUP BYとして機能します。annotate()を先に書くと、各レコードに集計値が付加されます(GROUP BYなし)。順序が重要です。Step 2集計関数の種類
Python
from django.db.models import Count, Sum, Avg, Max, Min
# Count: 件数
results = Sale.objects.values("category").annotate(
count=Count("id")
)
# Sum: 合計
results = Sale.objects.values("category").annotate(
total_amount=Sum("amount"),
total_quantity=Sum("quantity")
)
# Avg: 平均
results = Sale.objects.values("category").annotate(
avg_amount=Avg("amount")
)
for r in results:
print(f"{r['category']}: 平均 {r['avg_amount']:.0f}円")
# Max / Min: 最大・最小
results = Sale.objects.values("category").annotate(
max_amount=Max("amount"),
min_amount=Min("amount")
)
for r in results:
print(f"{r['category']}: {r['min_amount']}〜{r['max_amount']}円")
# 複数の集計関数を同時に使う
results = Sale.objects.values("category").annotate(
count=Count("id"),
total=Sum("amount"),
average=Avg("amount"),
highest=Max("amount"),
lowest=Min("amount"),
).order_by("-total")実行結果
食品: 平均 30,000円
電子機器: 平均 100,000円
衣料品: 平均 30,000円
---
食品: 500〜120,000円
電子機器: 5,000〜500,000円
衣料品: 1,000〜80,000円Step 3複数フィールドでのグルーピング
Python
# カテゴリと商品名でグルーピング
results = Sale.objects.values("category", "product").annotate(
count=Count("id"),
total=Sum("amount")
).order_by("category", "-total")
for r in results:
print(f"{r['category']} | {r['product']}: {r['count']}件 ({r['total']}円)")
# 年月ごとの売上集計
from django.db.models.functions import TruncMonth
monthly = Sale.objects.annotate(
month=TruncMonth("sold_at")
).values("month").annotate(
count=Count("id"),
total=Sum("amount")
).order_by("month")
for m in monthly:
print(f"{m['month'].strftime('%Y-%m')}: {m['count']}件 / {m['total']}円")
# 年ごとの集計
from django.db.models.functions import TruncYear
yearly = Sale.objects.annotate(
year=TruncYear("sold_at")
).values("year").annotate(
total=Sum("amount")
).order_by("year")実行結果
食品 | りんご: 45件 (1,350,000円)
食品 | みかん: 38件 (1,140,000円)
電子機器 | ノートPC: 30件 (6,000,000円)
---
2024-10: 42件 / 2,100,000円
2024-11: 55件 / 3,200,000円
2024-12: 68件 / 4,500,000円Step 4条件付き集計
Python
from django.db.models import Q, Count, Sum, Case, When, IntegerField
# filter()で絞り込んでからグルーピング
results = Sale.objects.filter(
sold_at__year=2024
).values("category").annotate(
total=Sum("amount")
)
# annotate内のfilter引数で条件付きカウント
results = Sale.objects.values("category").annotate(
total_count=Count("id"),
high_value_count=Count("id", filter=Q(amount__gte=10000)),
low_value_count=Count("id", filter=Q(amount__lt=10000)),
)
for r in results:
print(f"{r['category']}: 全{r['total_count']}件 "
f"(高額{r['high_value_count']}件 / 少額{r['low_value_count']}件)")
# Case/Whenを使った条件付き集計
results = Sale.objects.values("category").annotate(
total=Sum("amount"),
high_amount=Sum(
Case(
When(amount__gte=10000, then="amount"),
default=0,
output_field=IntegerField()
)
)
)
# HAVING句相当(集計結果でフィルタ)
results = Sale.objects.values("category").annotate(
total=Sum("amount")
).filter(total__gte=1000000) # annotate後のfilter = HAVING
print("売上100万円以上のカテゴリ:")
for r in results:
print(f" {r['category']}: {r['total']}円")filter()の位置に注意
annotate()の前のfilter()はWHERE句(集計前の絞り込み)、annotate()の後のfilter()はHAVING句(集計後の絞り込み)として機能します。Step 5実践的な集計クエリ
Python
# views.py - ダッシュボード用の集計
from django.shortcuts import render
from django.db.models import Count, Sum, Avg
from django.db.models.functions import TruncMonth
from .models import Sale
def dashboard(request):
# カテゴリ別売上
category_sales = Sale.objects.values("category").annotate(
count=Count("id"),
total=Sum("amount"),
avg=Avg("amount"),
).order_by("-total")
# 月別推移
monthly_sales = Sale.objects.annotate(
month=TruncMonth("sold_at")
).values("month").annotate(
total=Sum("amount"),
count=Count("id"),
).order_by("month")
# 全体集計(aggregate)
overview = Sale.objects.aggregate(
total_sales=Sum("amount"),
total_count=Count("id"),
avg_sale=Avg("amount"),
)
context = {
"category_sales": category_sales,
"monthly_sales": monthly_sales,
"overview": overview,
}
return render(request, "dashboard.html", context)Python
# aggregateとannotateの違い
# aggregate(): QuerySet全体を1つの値に集計 → 辞書を返す
total = Sale.objects.aggregate(total=Sum("amount"))
print(total) # {"total": 14050000}
# annotate(): 各グループ/レコードに集計値を付加 → QuerySetを返す
results = Sale.objects.values("category").annotate(total=Sum("amount"))
# [{"category": "食品", "total": 3600000}, ...]まとめ
まとめ
values()+annotate()でSQLのGROUP BYに相当する集計ができる- 集計関数はCount、Sum、Avg、Max、Minが使える
- TruncMonth等で日付を丸めて月別・年別の集計が可能
annotate()前のfilter()はWHERE句、後のfilter()はHAVING句として機能するaggregate()は全体集計(辞書)、annotate()はグループ集計(QuerySet)を返す