👩‍💻

#109 【Django】ORMとN+1問題についての備忘録

に公開

はじめに

今回はPythonのフレームワークであるDjangoのORMについての備忘録です。
速度改善のため、N+1問題についてや、select_related・prefetch_related・Qオブジェクトなどでパフォーマンスを改善できないか調べていました。

N+1問題

N+1問題とは

DBからN個のデータを取得するとき、関連するデータを取得するためにさらにN回のクエリが発生する問題です。
データが増えるほどクエリの発行が多くなり、パフォーマンスが低下する原因となってしまいます。

具体例

社員(Employee)と会社(Company)という2つのモデルを定義しました。

class Company(models.Model):
    name = models.CharField(max_length=50)

class Employee(models.Model):
    name = models.CharField(max_length=50)
    company = models.ForeignKey(Company, on_delete=models.CASCADE)

すべての社員の名前と所属する会社の名前を取得してみます。

employees = Employee.objects.all()
for employee in employees:
    print(employee.name, employee.company.name)

以下のクエリが発行されます。

/* 全社員の情報を取得 */
SELECT * FROM employee;

 /* 上記のクエリで取得した社員の数(N回)分、会社の情報を取得する */
SELECT * FROM company WHERE id = 社員が所属する会社のid;

全社員のデータを1回、各社員の会社データをN回クエリで取得するため、合計でN+1回のクエリが実行されてしまいます。

N+1問題の解決案

select_related は、外部キーを使った「多対1」や「1対1」のデータをJOINして一度に取得できます。

employees = Employee.objects.select_related('company').all()
for employee in employees:
    print(employee.name, employee.company.name)

関連する会社の情報がJOINされ、1回のクエリで全てのデータを取得できます。

SELECT "employee"."id", "employee"."name", "company"."id", "company"."name"
FROM "employee"
INNER JOIN "company" ON ("employee"."company" = "company"."id");

prefetch_related は、複数のクエリを発行してデータを取得しますが、Djangoがメモリ上で効率的に関連付けるため、パフォーマンスが向上します。
「多対多」や「逆方向の外部キーリレーション」の関係で使われます。

employees = Employee.objects.prefetch_related('company').all()
for employee in employees:
    print(employee.name, employee.company.name)

以下のように2つのクエリが発行されます。
DBへのアクセス回数が抑えられ、N+1問題を回避できます。

SELECT * FROM employee;
SELECT * FROM company WHERE id IN (1, 2, 3, ...N); /* 全社員のIDのリスト */

Qオブジェクト を使う

Qオブジェクト は、複数の条件を「AND」や「OR」で結合した複雑なフィルタリングが行うことができます。
select_related や prefetch_related と組み合わせて、N+1問題を回避して効率よくデータを取得することができます。

from django.db.models import Q

# 会社名が'会社 1'または'会社 2'の社員を取得
employees = Employee.objects.select_related('company').filter(
    Q(company__name='会社 1') | Q(company__name='会社 2')
)
for employee in employees:
    print(employee.name, employee.company.name)
SELECT "employee"."id", "employee"."name", "company"."id", "company"."name"
FROM "employee"
INNER JOIN "company" ON ("employee"."company" = "company"."id")
WHERE ("company"."name" = '会社 1' OR "company"."name" = '会社 2');

それでも解決しないとき

raw()で生のSQLを使う

複雑なクエリが必要でDjangoのORMが生成するクエリでは対応しきれない場合は raw() を使います。
raw() を使うと、生のSQLクエリを直接実行することができます。

employees = Employee.objects.raw(
'''
    SELECT employee.id, employee.name, company.name AS company_name
    FROM employee
    JOIN company ON employee.company = company.id
    WHERE company.name = %s
''', ['会社 1']
)

for employee in employees:
    print(employee.name, employee.company_name)
SELECT employee.id, employee.name, company.name AS company_name
FROM employee
JOIN company ON employee.company = company.id;

ORMを介さず効率的なクエリを発行できました。
ただ、SQLインジェクションのリスクがありますので注意してつかう必要があります。

最後に

Djangoは機能が多いフレームワークですが、使いこなせるよう理解を深めていきたいと思います。
ご覧いただきありがとうございました。

出典:
https://docs.djangoproject.com/ja/5.1/ref/models/querysets/#select-related
https://docs.djangoproject.com/ja/5.1/ref/models/querysets/#prefetch-related
https://docs.djangoproject.com/ja/5.1/ref/models/querysets/#django.db.models.Q
https://docs.djangoproject.com/ja/5.1/topics/db/sql/

Discussion