ORM

Django ORMのグルーピング|GROUP BYとannotate

Django ORM

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)を返す