社内でDjangoプロジェクト増えてきて、イケてないクエリも増えてきそうなので簡単にメモを残す。
ORMを使って何も考えずにforeign keyの逆参照を取ってきたりMany-to-Manyの参照先を取得する際に、ループの度にクエリを飛ばして悲惨なことになるので、prefetch_relatedを使っとけという話
普通のForeign Keyならselect_relatedで必要な階層分引っ張ることが出来る。
普通に使う
とりあえずこんなモデル
from django.db import models
class Campaign(models.Model):
name = models.CharField(max_length=255)
created_at = models.DateTimeField()
class Creative(models.Model):
name = models.CharField(max_length=255, default="")
campaign = models.ForeignKey(Campaign)
created_at = models.DateTimeField()
CreativeからCampaignを引く場合はselect_relatedを使ってあげればループの度にCampaignをselectせずにjoinしてまとめて取ってきてくれる。
for creative in Creative.objects.all().select_related():
print(creative.campaign)
CampaignからCreativeの一覧を引っ張る場合はprefetch_relatedを使うと良い
for campaign in Campaign.objects.all().prefetch_related("creative_set"):
print(campaign.creative_set.all())
joinではなく一度全部Campaignを引っ張ってきて、campaign.idのリストをwhere inして引っ張ってきている様子。まぁ毎回飛ぶより良いし大抵の場合において問題なさそう。
ちなみにcreativeでcampaignを絞るのは普通にクエリでかける
Campaign.object.filter(creative_set__name="aaa")
Prefetchオブジェクトを使う
単純にprefetch_relatedを使うだけだと、XXXX_set.all()の物しか取得できない。creative_setに対して絞込等を掛けるとループの度にqueryが飛んでとても悲しいことになる。
for campaign in Campaign.objects.all().prefetch_related("creative_set"):
print(campaign.creative_set.filter(name__startswith="hoge"))
そこで、逆参照やMany-to-Manyの先をfilter使って絞り込みたい場合や、並び順を指定したい場合は、Djangoの1.7から入ったPrefetchオブジェクトを利用する。こいつのお陰でだいぶDjangoのORMもマシになった気がする。
from django.db import models
from django.db.models import Prefetch
for campaign in Campaign.objects.all().prefetch_related(Prefetch("creative_set", queryset=Creative.objects.filter(name__startswith="hoge").order_by("-created_at"), to_attr="creatives")):
if len(campaign.creatives) > 0:
print(campaign.id, creatives[0].id)
これだとCampaignが何個あってもクエリが2回しか飛ばなくてお得。
クエリ見る
クエリのチューニングをする際は、Django Shellを使ってクエリの確認をすることになると思うので、shellでクエリをぶっ叩いた時にSQL文が表示されるようにloggerをいじると良い。普通にやっているとログの設定をいじっているとは思うが、クエリの調査の時だけはdjango.db.backendsのloglevelをDEBUGにしておくと幸せになる。
LOGGING = {
'version': 1,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
}
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
}
}
view単位のチューニングの場合は、django-debug-toolbarを使ってクエリの確認をするのが基本