🐍
Django の select_related と prefetch_related を SQL で理解しよう
概要
Django の ORM を SQL で理解しておきたいのでここに記載する。
対象者
SQL はちょっとわかるけど、Django の ORM はよくわからん!という方向け。
select_related
select_related
はすなわち、INNER JOIN
である。
Model 定義は以下であったとする。
from django.db import models
class City(models.Model):
# ...
pass
class Person(models.Model):
# ...
hometown = models.ForeignKey(
City,
on_delete=models.SET_NULL,
blank=True,
null=True,
),
class BookCategory(models.Model):
# ...
category = models.CharField(max_length=10)
class Book(models.Model):
# ...
author = models.ForeignKey(Person, on_delete=models.CASCADE)
category = models.ForeignKey(BookCategory, on_delete=models.PROTECT)
この時、シンプルな select_related は以下である。
> qs = Book.objects.select_related('author')
> print(qs.shell)
SELECT *
FROM myapp.book
INNER JOIN myapp.person
ON myapp.book.author_id = myapp.person.id
続いて、nested な select_related は 以下。
> qs = Book.objects.select_related('author__hometown')
> print(qs.shell)
SELECT *
FROM myapp.book
INNER JOIN myapp.person
ON myapp.book.author_id = myapp.person.id
INNER JOIN myapp.city
ON myapp.person.hometown_id = myapp.city.id
また、複数設定できる。
> qs = Book.objects.select_related('author__hometown', 'category')
> print(qs.shell)
SELECT *
FROM myapp.book
INNER JOIN myapp.person
ON myapp.book.author_id = myapp.person.id
INNER JOIN myapp.city
ON myapp.person.hometown_id = myapp.city.id
INNER JOIN myapp.book_category
ON myapp.book.category_id = myapp.book_category.id
こう見ると、ただのINNER JOINだね、とわかる。
ForeignKey または OneToOneField の id 同士でJOINすることを想定している。
クソでかいテーブルを JOIN する場合は気をつけよう。
JOIN を解除したい場合は queryset を上書きする。
without_relations = queryset.select_related(None)
prefetch_related
prefetch_related
は(タイトル詐欺っぽいが)SQL だけでは理解できないので若干面倒。
model 定義が以下であったとする。
from django.db import models
class Topping(models.Model):
name = models.CharField(max_length=30)
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
def __str__(self):
return "%s (%s)" % (
self.name,
", ".join(topping.name for topping in self.toppings.all()),
)
この時、通常はこうなる
>> qs = Pizza.objects.all()
>> for pizza in qs
>> toppings = [topping.name for topping in pizza.toppings.all()]
SELECT *
FROM myapp.pizza;
SELECT *
FROM myapp.topping
WHERE myapp.topping.pizza_set.id IN (1);
SELECT *
FROM myapp.topping
WHERE myapp.topping.pizza_set.id IN (2);
... (以下N個)
iteration が回るたびにクエリを発行するため、いわゆる N+1 回 叩かれるコードになる。
prefetch_related を使用するとこうなる。
>> qs = Pizza.objects.prefetch_related("toppings")
>> for pizza in qs
>> toppings = [topping.name for topping in pizza.toppings.all()]
SELECT *
FROM myapp.pizza;
SELECT *
FROM myapp.topping
WHERE myapp.topping.pizza_set.id IN (1,2,3...) # Pizza objects の id 全て
内部的に pizza に関連する topping を全て取ってきているので2回ですむ。
こちらも select_related
と同様に nested をサポートしている。
ただ、実質 pizza と toppings を全件取得のようなことをするので、これらのサイズが大きい場合は別案を考えた方がいいかも。
参考資料
Discussion