ORM

Django ORMのannotate入門|計算フィールドを追加して取得

Django ORMでデータ取得時に計算したフィールドを追加する方法を解説します。annotate()extra()を使うと、データベース側で計算を行い、結果を仮想的なフィールドとして取得できます。

基本的な使い方

annotate()Fオブジェクトを使って、既存フィールドから計算フィールドを追加します。

views.py
from django.db.models import F, FloatField
from django.db.models.functions import Cast
from .models import Sale

def index(request):
    # amountの1.1倍を税込価格として追加
    sales = Sale.objects.annotate(
        tax_included=F('amount') * 1.1
    ).values('amount', 'sales_date', 'tax_included')
    print(sales)
実行結果
<QuerySet [
    {'amount': 10000, 'sales_date': datetime.date(2025, 1, 15), 'tax_included': 11000.0},
    {'amount': 25000, 'sales_date': datetime.date(2025, 1, 20), 'tax_included': 27500.0}
]>

annotate()で追加したフィールドは、通常のフィールドと同様にアクセスできます。データベース側で計算されるため、Python側で処理するより効率的です。

様々な計算例

Fオブジェクトを使った算術演算の例です。

views.py
from django.db.models import F

# 割引後の金額を計算
sales = Sale.objects.annotate(
    net_amount=F('amount') - F('discount')
)

# 割引率を計算
sales = Sale.objects.annotate(
    discount_rate=F('discount') * 100.0 / F('amount')
)

# 数量 × 単価 の合計金額
orders = Order.objects.annotate(
    total_price=F('price') * F('quantity')
)

データベース関数の活用

Djangoには多くのデータベース関数が用意されています。

views.py
from django.db.models import Value, CharField
from django.db.models.functions import (
    Concat, Upper, Round, Coalesce,
    ExtractYear, ExtractMonth
)

# 文字列結合
users = User.objects.annotate(
    full_name=Concat('last_name', Value(' '), 'first_name')
)

# 四捨五入
sales = Sale.objects.annotate(
    rounded_tax=Round(F('amount') * 1.1, 0)
)

# NULL値のデフォルト値を設定
sales = Sale.objects.annotate(
    actual_discount=Coalesce('discount', Value(0))
)

# 日付から年・月を抽出
sales = Sale.objects.annotate(
    sale_year=ExtractYear('sales_date'),
    sale_month=ExtractMonth('sales_date')
)

条件分岐(Case/When)

SQLのCASE文に相当する条件分岐もannotateで表現できます。

views.py
from django.db.models import Case, When, Value, CharField

# 金額に応じたランク付け
sales = Sale.objects.annotate(
    scale=Case(
        When(amount__lt=5000, then=Value("小口")),
        When(amount__lt=10000, then=Value("中口")),
        default=Value("大口"),
        output_field=CharField()
    )
)
実行結果
<QuerySet [
    <Sale: 商品A - scale=小口>,
    <Sale: 商品B - scale=中口>,
    <Sale: 商品C - scale=大口>
]>

annotateの結果でfilterやorder

追加した計算フィールドを使って、フィルタリングや並べ替えもできます。

views.py
from django.db.models import F

# 税込10000円以上のデータをフィルタ
high_sales = Sale.objects.annotate(
    tax_included=F('amount') * 1.1
).filter(tax_included__gte=10000)

# 計算フィールドで並べ替え
sorted_sales = Sale.objects.annotate(
    total_price=F('price') * F('quantity')
).order_by('-total_price')

集計関数との組み合わせ

annotate()で集計関数を使うと、グループごとの集計値を各レコードに追加できます。

views.py
from django.db.models import Sum, Count

# カテゴリごとの売上合計と税込合計
category_stats = Sale.objects.values('product_category').annotate(
    total_amount=Sum('amount'),
    tax_included_total=Sum(F('amount') * 1.1),
    sale_count=Count('id')
).order_by('-total_amount')

実践的な使用例

売上一覧画面で計算フィールドを活用する実践例です。

views.py
from django.shortcuts import render
from django.db.models import F, Case, When, Value, CharField
from .models import Sale

def sale_list(request):
    sales = Sale.objects.annotate(
        tax_included=F('amount') * 1.1,
        scale=Case(
            When(amount__lt=5000, then=Value("小")),
            When(amount__lt=10000, then=Value("中")),
            default=Value("大"),
            output_field=CharField()
        )
    ).order_by('-sales_date')

    return render(request, 'sales/list.html', {
        'sales': sales
    })

テンプレートでは通常のフィールドと同様にアクセスできます。

templates/sales/list.html
{% for sale in sales %}
<tr>
    <td>{{ sale.product_name }}</td>
    <td>{{ sale.amount }}円</td>
    <td>{{ sale.tax_included }}円</td>
    <td>{{ sale.scale }}</td>
</tr>
{% endfor %}
ポイント

annotate()で追加した計算フィールドはデータベース側で処理されるため、Pythonのループで1件ずつ計算するよりはるかに効率的です。大量データを扱う場合は積極的に活用しましょう。

注意

annotate()で追加したフィールドはデータベースには保存されません。毎回クエリ実行時に計算されます。頻繁に使う計算値は、モデルにフィールドを追加するか@propertyデコレータの使用を検討してください。

まとめ

  • annotate()F()で既存フィールドから計算フィールドを追加できる
  • ConcatRoundCoalesceなどのデータベース関数が利用できる
  • Case/Whenで条件分岐の計算フィールドを作成できる
  • 追加した計算フィールドでfilter()order_by()もできる
  • 計算はデータベース側で実行されるためPythonで処理するより効率的