Django ORMで関連モデルを結合し、グループ化して集計する方法を解説します。annotate()とvalues()を組み合わせると、SQLのJOIN + GROUP BYに相当する複雑なクエリもPythonコードで簡潔に書けます。
基本的な使い方
社員ごとの日別売上合計を取得する例です。
views.py
from django.db.models import Sum
from .models import Employee
def index(request):
# 社員ごと・日付ごとの売上合計を取得
employees_with_sales = Employee.objects.values(
'id', 'name', 'sale__sales_date'
).annotate(
total_sales=Sum('sale__amount')
).order_by('id', 'sale__sales_date')
for row in employees_with_sales:
print(f"{row['name']} ({row['sale__sales_date']}): {row['total_sales']}円")実行結果
山田太郎 (2025-01-01): 15000円
山田太郎 (2025-01-02): 25000円
山田太郎 (2025-01-03): 18000円
佐藤花子 (2025-01-01): 22000円
佐藤花子 (2025-01-02): 19500円発行されるSQL
SELECT employee.id, employee.name, sale.sales_date,
SUM(sale.amount) AS total_sales
FROM employee
INNER JOIN sale ON employee.id = sale.employee_id
GROUP BY employee.id, employee.name, sale.sales_date
ORDER BY employee.id, sale.sales_dateクエリの構造を理解する
この複雑なクエリを分解して理解しましょう。
views.py
# ステップ1: values()でGROUP BYの対象を指定
Employee.objects.values('id', 'name', 'sale__sales_date')
# → GROUP BY employee.id, employee.name, sale.sales_date
# ステップ2: annotate()で集計関数を適用
.annotate(total_sales=Sum('sale__amount'))
# → SELECT ... SUM(sale.amount) AS total_sales
# ステップ3: order_by()で並べ替え
.order_by('id', 'sale__sales_date')
# → ORDER BY employee.id, sale.sales_datevalues()をannotate()の前に書くと、values()で指定したフィールドがGROUP BYの対象になります。これがDjango ORMでGROUP BYを表現する方法です。
売上のない社員も含める
INNER JOINでは関連データがないレコードは除外されます。全社員を含めるには工夫が必要です。
views.py
from django.db.models import Sum, Count, Value
from django.db.models.functions import Coalesce
# annotate()を直接使う(LEFT OUTER JOINになる)
all_employees = Employee.objects.annotate(
total_sales=Coalesce(Sum('sale__amount'), Value(0)),
sales_count=Count('sale')
).order_by('name')
for emp in all_employees:
print(f"{emp.name}: {emp.total_sales}円 ({emp.sales_count}件)")実行結果
山田太郎: 58000円 (3件)
佐藤花子: 41500円 (2件)
田中一郎: 0円 (0件)Coalesce()を使ってNULL値を0に変換しています。売上がない田中一郎も結果に含まれます。
複数の集計関数を同時に使う
1つのクエリで複数の集計を同時に行えます。
views.py
from django.db.models import Sum, Count, Avg, Max, Min
# 社員ごとの詳細な売上統計
employee_stats = Employee.objects.annotate(
total_sales=Sum('sale__amount'),
sales_count=Count('sale'),
avg_sale=Avg('sale__amount'),
max_sale=Max('sale__amount'),
min_sale=Min('sale__amount')
).filter(
sales_count__gt=0
).order_by('-total_sales')期間を指定した集計
filter()で条件を絞り込んでから集計を行います。
views.py
from django.db.models import Sum, Count
from datetime import date
# 2025年1月の社員ごとの売上集計
monthly_stats = Employee.objects.filter(
sale__sales_date__year=2025,
sale__sales_date__month=1
).values('id', 'name').annotate(
total_sales=Sum('sale__amount'),
sales_count=Count('sale')
).order_by('-total_sales')部署情報も含めた集計
さらに別のモデル(部署)も結合して集計できます。
views.py
from django.db.models import Sum, Count
# 部署ごとの売上集計
department_stats = Employee.objects.values(
'department__name'
).annotate(
employee_count=Count('id', distinct=True),
total_sales=Sum('sale__amount')
).order_by('-total_sales')
for dept in department_stats:
print(f"{dept['department__name']}: "
f"{dept['employee_count']}人, "
f"{dept['total_sales']}円")実行結果
営業部: 5人, 850000円
技術部: 3人, 320000円
人事部: 2人, 150000円実践的な使用例
日次売上レポートを構築する実践例です。
views.py
from django.shortcuts import render
from django.db.models import Sum, Count, Avg
from django.db.models.functions import Coalesce
from datetime import date, timedelta
from .models import Employee
def daily_sales_report(request):
# 日付範囲の処理
start = request.GET.get('start')
end = request.GET.get('end')
start_date = date.fromisoformat(start) if start else date.today() - timedelta(days=30)
end_date = date.fromisoformat(end) if end else date.today()
# 社員ごと・日付ごとの売上集計
daily_sales = Employee.objects.filter(
sale__sales_date__range=(start_date, end_date)
).values(
'id', 'name', 'sale__sales_date'
).annotate(
total_sales=Sum('sale__amount'),
transaction_count=Count('sale')
).order_by('name', 'sale__sales_date')
# 社員ごとの合計
employee_totals = Employee.objects.filter(
sale__sales_date__range=(start_date, end_date)
).annotate(
total_sales=Coalesce(Sum('sale__amount'), 0),
avg_daily=Avg('sale__amount')
).order_by('-total_sales')
return render(request, 'reports/daily_sales.html', {
'daily_sales': daily_sales,
'employee_totals': employee_totals,
'start_date': start_date,
'end_date': end_date,
})ポイント
values().annotate()の順序が重要です。values()を先に書くとGROUP BYとして機能し、annotate()を先に書くと各レコードに集計値が追加されます。目的に応じて使い分けましょう。
注意
Count()で重複を除外したい場合はCount('id', distinct=True)を使います。JOINにより同じレコードが複数回カウントされることがあるため、必要に応じてdistinct=Trueを付けてください。
まとめ
values().annotate()でGROUP BY + 集計(SUM, COUNT等)を実現できるvalues()をannotate()の前に書くとGROUP BYの対象になるCoalesce()でNULL値のデフォルト値を設定できる- 関連モデルのフィールドはダブルアンダースコアでアクセスする
distinct=TrueでJOINによる重複カウントを防げる