🐍

Django の select_related と prefetch_related を SQL で理解しよう

2024/11/02に公開

概要

Django の ORM を SQL で理解しておきたいのでここに記載する。

対象者

SQL はちょっとわかるけど、Django の ORM はよくわからん!という方向け。

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 は(タイトル詐欺っぽいが)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 を全件取得のようなことをするので、これらのサイズが大きい場合は別案を考えた方がいいかも。

参考資料

https://docs.djangoproject.com/ja/5.1/ref/models/querysets/

Discussion