ORM

Django ORMのselect_related入門|テーブル結合でデータ取得

Django ORMで紐づいた別のモデルのフィールドを結合して取得する方法を解説します。select_related()を使うとSQLのJOINを発行し、関連モデルのデータを1回のクエリで効率的に取得できます。

基本的な使い方

ForeignKeyで紐づいたモデルのデータを結合して取得するにはselect_related()を使います。

models.py
class Employee(models.Model):
    name = models.CharField(max_length=100)
    department = models.ForeignKey('Department', on_delete=models.CASCADE)

class Sale(models.Model):
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    amount = models.IntegerField()
    sales_date = models.DateField()
views.py
from .models import Sale

def index(request):
    # SaleとEmployeeを結合して取得
    sales = Sale.objects.select_related('employee')

    for sale in sales:
        # 追加クエリなしでemployeeのフィールドにアクセスできる
        print(f"{sale.employee.name}: {sale.amount}円")
実行結果
山田太郎: 15000円
山田太郎: 22000円
佐藤花子: 18000円

select_related()を使わない場合、ループ内でsale.employeeにアクセスするたびに追加のSQLクエリが発行されてしまいます(N+1問題)。

select_relatedなしの場合(N+1問題)

なぜselect_related()が重要なのかを理解するため、使わない場合を見てみましょう。

views.py
# N+1問題が発生するコード
sales = Sale.objects.all()  # 1回目のクエリ

for sale in sales:
    print(sale.employee.name)  # ループごとに追加クエリが発行される!
    # Sale 100件 → 合計101回のクエリ
発行されるSQL(N+1問題あり)
SELECT * FROM sale;                          -- 1回目
SELECT * FROM employee WHERE id = 1;         -- 2回目
SELECT * FROM employee WHERE id = 2;         -- 3回目
...                                          -- N回繰り返し
select_relatedを使った場合のSQL
SELECT sale.*, employee.*
FROM sale
INNER JOIN employee ON sale.employee_id = employee.id;  -- 1回だけ!

複数のモデルを結合

2つ以上のForeignKeyを結合することもできます。

views.py
# 複数のForeignKeyを同時に結合
sales = Sale.objects.select_related('employee', 'product')

# ネストした関連の結合(EmployeeのDepartmentまで結合)
sales = Sale.objects.select_related('employee__department')

for sale in sales:
    print(f"{sale.employee.name} ({sale.employee.department.name}): {sale.amount}円")

employee__departmentのようにダブルアンダースコアで深い階層のリレーションも結合できます。

valuesと組み合わせてフィールドを選択

結合したモデルの特定フィールドだけを取得するにはvalues()を組み合わせます。

views.py
# 関連モデルのフィールドをvaluesで取得
sales = Sale.objects.values(
    'employee__name',
    'amount',
    'sales_date'
)
# [{'employee__name': '山田太郎', 'amount': 15000, 'sales_date': ...}, ...]

# annotateで別名を付ける
from django.db.models import F
sales = Sale.objects.annotate(
    employee_name=F('employee__name')
).values('employee_name', 'amount', 'sales_date')

prefetch_relatedとの使い分け

select_related()prefetch_related()は用途が異なります。

views.py
# select_related: ForeignKey、OneToOneField(JOINを使う)
sales = Sale.objects.select_related('employee')

# prefetch_related: ManyToMany、逆参照(別クエリで取得してPythonで結合)
employees = Employee.objects.prefetch_related('sale_set')
ポイント

select_relatedはSQL JOINで1回のクエリで取得します。ForeignKeyとOneToOneFieldに対して使います。prefetch_relatedは別クエリで取得しPython側で結合します。ManyToManyや逆参照(_set)に対して使います。

条件付き結合

結合したモデルのフィールドでフィルタリングも可能です。

views.py
# 営業部の社員の売上のみ取得
sales = Sale.objects.select_related('employee__department').filter(
    employee__department__name="営業部"
)

# 金額範囲で絞り込み
sales = Sale.objects.select_related('employee').filter(
    amount__gte=10000,
    employee__name__contains="山田"
)

実践的な使用例

売上レポート画面を構築する実践例です。

views.py
from django.shortcuts import render
from django.db.models import Sum, Count
from .models import Employee, Sale

def performance_report(request):
    # 各社員の売上実績を取得(annotateと組み合わせ)
    top_sellers = Employee.objects.annotate(
        total_sales=Sum('sale__amount'),
        sales_count=Count('sale')
    ).filter(
        total_sales__isnull=False
    ).order_by('-total_sales')[:10]

    # 売上のない社員も含めたレポート
    all_employees = Employee.objects.select_related(
        'department'
    ).annotate(
        total_sales=Sum('sale__amount'),
        sales_count=Count('sale')
    ).order_by('department__name', 'name')

    return render(request, 'reports/performance.html', {
        'top_sellers': top_sellers,
        'all_employees': all_employees,
    })
注意

select_related()はINNER JOINではなくLEFT OUTER JOINを使用します。そのため、関連データがないレコードも取得されます。関連データが必ず存在するレコードだけに絞りたい場合は、.filter(employee__isnull=False)を追加してください。

まとめ

  • select_related()でForeignKey先のデータをJOINで1回のクエリで取得できる
  • N+1問題(ループ内で追加クエリが発生する問題)を防げる
  • ダブルアンダースコアで深い階層のリレーションも結合可能
  • ManyToManyや逆参照にはprefetch_related()を使う
  • values()annotate()と組み合わせて柔軟にデータを取得できる