ORM

Django ORMでサブクエリ|Subqueryを使ったフィルタリング

DjangoのORMで、あるモデルのクエリ結果を別のクエリの条件として使うには、Subqueryを使用します。SQLのサブクエリに相当する機能で、複雑なデータ取得を実現できます。

基本的な使い方

views.py
from django.db.models import Subquery, OuterRef

last_sales_date = Sales.objects.filter(
    name=OuterRef('pk')
   ).order_by('-date').values('date')[:1]

model = Person.objects.annotate(
    last_sale_date=Subquery(last_sales_date))

print(model.values('name', 'last_sale_date'))

説明

Step 1SubqueryとOuterRefの基本

Djangoでは、SubqueryとOuterRefを組み合わせることで、自身を呼び出しているデータを使用してフィルタリングし、そのデータをannotateで追加することができます。

from django.db.models import Subquery, OuterRef

重要な注意点: Subqueryは複数データがある状態が返ってくるとエラーになるので、返ってくるデータが1つになるようにする必要があります。

Step 2基本的な使用例

例えば、各Personに対して最終売上日を取得する場合:

from django.db.models import Subquery, OuterRef

# 各人の最終売上日を取得するサブクエリ
last_sales_date = Sales.objects.filter(
    person_id=OuterRef('pk')  # 外部参照で現在処理中のPersonのpkを参照
).order_by('-date').values('date')[:1]  # 日付の降順で並べて最初の1件だけ取得

# Personに最終売上日を追加
persons = Person.objects.annotate(
    last_sale_date=Subquery(last_sales_date)
)

上の例は、OuterRefを使用して、自身を呼び出しているモデル(Person)のpkを使用してフィルタリングしています。last_sales_dateはperson_idフィールドが自身を呼び出しているモデルのpkのデータを取得し、それをdateで降順にし、最初の1件目を取得しています。

Subqueryはannotateに他のモデルのデータを埋め込むのに使用され、ここでは、変数last_sales_dateで処理された結果がlast_sale_dateフィールドに代入されています。

Step 3複雑な例:フィールドの取得と条件付きサブクエリ

特定のフィールドの値や、条件付きのデータを取得する例:

# 各人の最終売上額を取得
last_sale_amount = Sales.objects.filter(
    person_id=OuterRef('pk')
).order_by('-date').values('amount')[:1]

# 各人の今年の最高売上額を取得
from django.db.models import Max
from datetime import datetime

current_year = datetime.now().year
max_sale_this_year = Sales.objects.filter(
    person_id=OuterRef('pk'),
    date__year=current_year
).values('person_id').annotate(
    max_amount=Max('amount')
).values('max_amount')

# Personに最終売上額と今年の最高売上額を追加
persons = Person.objects.annotate(
    last_sale_amount=Subquery(last_sale_amount),
    max_sale_this_year=Subquery(max_sale_this_year)
)

Step 4複数のモデルにまたがるサブクエリ

複数のモデルを通じた関連データの取得:

# 各会社の最終売上の担当者名を取得
latest_sale_person = Sales.objects.filter(
    company_id=OuterRef('pk')
).order_by('-date').values('person__name')[:1]

# 各会社の売上合計が最も多い部門名を取得
top_department = Sales.objects.filter(
    company_id=OuterRef('pk')
).values('department').annotate(
    total=Sum('amount')
).order_by('-total').values('department')[:1]

# Companyに最終売上担当者と売上トップ部門を追加
companies = Company.objects.annotate(
    latest_sale_by=Subquery(latest_sale_person),
    top_department=Subquery(top_department)
)

Step 5条件分岐を含むサブクエリ

Case式とSubqueryを組み合わせて、条件分岐を含む集計を行う例:

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

# 各人の売上状況に基づくステータスを計算
has_sales = Sales.objects.filter(
    person_id=OuterRef('pk')
).values('person_id').annotate(
    count=Count('id')
).values('count')[:1]

# Personにステータスを追加
persons = Person.objects.annotate(
    sales_status=Case(
        When(Subquery(has_sales) > 0, then=Value('有効')),
        default=Value('未売上'),
        output_field=CharField()
    )
)

Step 6実践的な使用例

views.pyでのSubqueryとOuterRefの使用例:

from django.shortcuts import render
from django.db.models import Subquery, OuterRef, Sum, Count, F, ExpressionWrapper, FloatField
from .models import Person, Sales, Company

def sales_dashboard(request):
    # 各人の最終売上情報
    last_sale_date = Sales.objects.filter(
        person_id=OuterRef('pk')
    ).order_by('-date').values('date')[:1]
    
    last_sale_amount = Sales.objects.filter(
        person_id=OuterRef('pk')
    ).order_by('-date').values('amount')[:1]
    
    # 各人の売上合計と平均
    total_sales = Sales.objects.filter(
        person_id=OuterRef('pk')
    ).values('person_id').annotate(
        total=Sum('amount')
    ).values('total')[:1]
    
    sales_count = Sales.objects.filter(
        person_id=OuterRef('pk')
    ).values('person_id').annotate(
        count=Count('id')
    ).values('count')[:1]
    
    # 平均売上額を計算(total_salesとsales_countから)
    persons = Person.objects.annotate(
        last_sale_date=Subquery(last_sale_date),
        last_sale_amount=Subquery(last_sale_amount),
        total_sales=Subquery(total_sales),
        sales_count=Subquery(sales_count),
        avg_sale=ExpressionWrapper(
            F('total_sales') / F('sales_count'),
            output_field=FloatField()
        )
    ).order_by('-total_sales')
    
    return render(request, 'persons/dashboard.html', {
        'persons': persons
    })

テンプレートでの使用例(dashboard.html):

<h1>営業担当者別ダッシュボード</h1>

<table>
    <tr>
        <th>担当者名</th>
        <th>最終売上日</th>
        <th>最終売上額</th>
        <th>売上合計</th>
        <th>売上件数</th>
        <th>平均売上額</th>
    </tr>
    {% for person in persons %}
        <tr>
            <td>{{ person.name }}</td>
            <td>{{ person.last_sale_date|default:"未売上" }}</td>
            <td>{{ person.last_sale_amount|default:"-"|floatformat:0 }}円</td>
            <td>{{ person.total_sales|default:0|floatformat:0 }}円</td>
            <td>{{ person.sales_count|default:0 }}件</td>
            <td>{{ person.avg_sale|default:"-"|floatformat:0 }}円</td>
        </tr>
    {% endfor %}
</table>
重要ポイント:
  • SubqueryとOuterRefを使うことで、関連モデルの特定データを効率的に取得できます。
  • Subqueryが返すのは必ず1件のデータにする必要があります([:1]や集計関数を使用)。
  • OuterRefは現在処理中のレコードのフィールド値を参照するために使用します。
  • 複数のSubqueryをannotateに追加することで、一度のクエリで複数の関連情報を取得できます。
  • パフォーマンスの観点から、通常のリレーション参照よりもSubqueryを使う方が複雑なケースで効率的なことがあります。

まとめ

  • Subqueryで別のクエリの結果をサブクエリとして使用できる
  • OuterRef()で外側のクエリのフィールドをサブクエリ内から参照できる
  • annotate()と組み合わせてサブクエリの結果をフィールドとして追加できる
  • Existsでサブクエリの存在確認ができる
  • 複雑な条件のフィルタリングをSQL1文で効率的に実行できる
  • パフォーマンスに注意し、必要に応じてインデックスを追加する