こんにちは。KIYONOエンジニアです。
Djangoを使っていて「あれ、なんかレスポンス遅いな…?」感じたことはないでしょうか。そんな時に原因調査するのに便利なdjango-debug-toolbarというパッケージとそれを使って、実際にパフォーマンス改善するためのselect_related/prefetch_relatedをご紹介します。
本記事はDjangoを用いてWebアプリケーション開発経験したことがある方を対象とし、あくまでツールの使い方とパフォーマンスの改善方法のイメージを掴んでいただくことを目的とします。したがってパッケージのインストール、初期セットアップ方法は割愛しますのでご注意いただければと思います。
さて、本編に入って参ります。
django-debug-toolbarって一体何ができるのか?
実際に画面を見た方がイメージしやすいので、画像を使いながら説明します。
右側に表示されている黒色のサイドバーがdjango-debug-toolbarのメニューバーとなっていて、上図のように履歴として、いつ、どのパスに対して、どのようなメソッドでリクエストされたかを確認できます。
一番よく使うであろうどのようなSQLが実行されているかを確認してみましょう。
赤枠のSwitchと書かれている部分を押していただくと、そのリクエストにフォーカスされます。
今回は/api/project/というリクエストにフォーカスしており、その状態でSQLと書かれた部分を押してみると以下の画面になります。
上図をみると色々なリクエストが走っていることがわかるかと思います。
実際にプラスボタンを押すと、よりみやすくかつ詳細にSQL情報を確認することもできます。
今回はどのようなSQLが実行されているかという部分にフォーカスしていきます。その他にヘッダーがどうなっているか、またシグナルやキャッシュ等の情報も確認できるので気になる方は実際に使ってみてください。
Djangoを使ってよく発生する問題
Djangoではテーブル間にリレーションがある状態で該当のテーブル情報を呼び出すと、なかなかレスポンスが返ってこない事象が起こることがあります。
例えば上図のようなテーブル構造の場合に、案件に企業や売上まで紐付けた形で情報返却するようなリクエストをする場合、パフォーマンスチューニングされていないとかなり重くなります。
なぜ重くなるかというと、案件のレコード数分、企業情報を検索するためのクエリが走ります。実際にSQLパネルから見た方がイメージつきやすいと思うので以下をご確認ください。
パフォーマンスチューニングなし | パフォーマンスチューニングあり |
![]() |
![]() |
上図の通りパフォーマンスチューニングがないと似たようなクエリが案件のレコード数分実行されてしまいます。(DjangoではこれをN+1問題という言い方をしたりもします。)
後半で実際にどのように改善していくかを説明します。
パフォーマンス改善する方法
select_related/prefetch_relatedを使ってクエリの本数を減らそう
前提
Djangoで以下のモデル前提とします。
from django.db import models
class Project(models.Model):
name = models.CharField(max_length=255)
client = models.ForeignKey(Client, related_name="projects", on_delete=models.SET_NULL, null=True, blank=True)
revenue = models.ForeignKey(Revenue, related_name="projects", on_delete=models.SET_NULL, null=True, blank=True)
created_by = models.CharField(max_length=255)
updated_by = models.CharField(max_length=255)
def __str__(self):
return self.name
class Client(models.Model):
name = models.CharField(max_length=255)
zip_code = models.CharField(max_length=20)
address1 = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Revenue(models.Model):
amount = models.DecimalField(max_digits=12, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Revenue {self.id} - {self.amount}"
class RevenueDetail(models.Model):
revenue = models.ForeignKey(Revenue, related_name="details", on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=12, decimal_places=2)
item = models.CharField(max_length=255)
accounting_month = models.DateField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.item} - {self.amount}"
select_relatedとは
まずはselect_relatedから説明します。リレーション先のオブジェクトを取得する際にJOINしてから情報取得する方法になります。先ほどのテーブルの例ですと、案件テーブルに対して、企業テーブルをJOINして案件情報を取得するイメージになります。
>>> Projects.objects.select_related("client")
上記のようにクエリ処理を記述いただければ、リレーション先のテーブルをJOINした状態でクエリ発行することができます。
SELECT * FROM project LEFT OUTER JOIN client ON project.client_id = clinet.id
まとめるとselect_relatedの使い方としては多 対 一、または一 対 一のリレーションの状態で、JOINを用いてリレーション先の情報含めて情報取得する際に使います。
prefetch_relatedとは
prefetch_relatedは、一 対 多のケースで使います。どのようなクエリが発行されているかというとWHERE…IN…になります。
>>> Projects.objects.select_related("revenue").prefetch_related('revenue__details')
実際に発行されるクエリは以下の通りです。
SELECT * FROM project LEFT OUTER JOIN revenue ON project.revenue_id = revenue.id
SELECT * FROM revenuedetail WHERE "revenuedetail"."revenue_id" IN (1,2,3...)
最初に案件情報を取得するクエリが実行され、その中に含まれるrevenue_idをもとに2行目のクエリが実行されます。WHERE … IN (1,2,3…)の部分は1行目で取得したrevenue_idとなります。
まとめるとprefetch_relatedの使い方としては一 対 多のリレーションの状態 で、WHERE…IN(…)を用いてリレーション先の情報含めて情報取得する際に使います。
最後に
いかがだったでしょうか。
本日はdjango-debug-toolbarやそこで発行されるクエリを見ながら、select_relatedやprefetch_relatedを使ってパフォーマンス改善する方法を紹介しました。
ぜひパフォーマンス問題で困ってる方は試してみてください。
コメント