見出し画像

Django_クエリの最適化(select_related, prefetch_related)#210日目

Djangoで多くのmodelをOneToOneFieldやForeignKey, ManyToManyで繋ぐようになると、データベースの読み込み速度が落ちていきます。

これは通常はフィールドを指定した分だけクエリが飛んでしまうからで、例えばfor文でループ処理などをすると、ループの数だけクエリが増えてしまいます。本番環境でこうなっていると処理時間が大幅に増加してし、サービスの運用に与える影響が大きくなります。

そこで、一度のクエリで関係するフィールドのデータを取得し、クエリの実行回数を減らすための方法が「select_related」と「prefetch_related」です。


以下のモデルを使ってそれぞれ整理してみたいと思います。

[models.py]

from django.contrib.auth import get_user_model

class Article(models.Model):
    title        = models.CharField(max_length=100)
    body         = models.TextField()
    is_published = models.BooleanField(default=False)
    created      = models.DateTimeField(auto_now_add=True)
 
class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
    body    = models.TextField()
    user    = models.ForeignKey(get_user_models(), on_delete=models.SET_NULL)
    created = models.DateTimeField(auto_now_add=True)


select_related

対一の関係にある外部キーを指定するクエリを最適化できます。上記のモデルだと、Comment経由でArticleのデータを抽出する場合です。クエリ数が増えてしまう悪い例と合わせてコード例を記載します。

select_relatedでは、引数の中に該当のフィールド名を指定することで、該当データを抽出することが可能です。

[悪い例]

from .models import Comment

# commentとarticleでクエリが2回発生してしまう
comment = Comment.objects.get()
article = comment.article
[良い例]

from .models import Comment

# commentのクエリでarticleまで引っ張ってきているためクエリは1回
comment = Comment.objects.select_related('article').get()
article = comment.article
 


prefetch_relatedとPrefetch

対多の関係にある外部キーを指定するクエリを最適化できます。上記のモデルだと、Article経由でCommentのデータを抽出する場合です。

Articleからは、Comment.articleのrelated_nameを指定することで、該当データを抽出できます。

[]

from .models import Article

article  = Article.objects.prefetch_related('comments').get()
comments = article.comments.filter(created__gte='2022-06-28').order_by('-created')
 

ちなみに「field名__gte='条件'」はgreater than equalの意味で、「以上」という条件指定です。同様に「__gt」で「より大きい(greater than)」、「__lt」で「より小さい(less than)」、「__lte」で「以下(less than equal)」です。


上記のprefetch_relatedでもクエリ数は減らせますが、例えばcommentsから更に別のForeignKeyであるuserをループ処理で抽出する場合、ループの数だけクエリが飛んでしまいます。

そこで活用するのがPrefetchという機能です。悪い例と合わせてコード例を記載します。

[悪い例]

from .models import Article

article  = Article.objects.prefetch_related('comments').get()
comments = article.comments.filter(created__gte='2022-06-28').order_by('-created')

# このままだとuser(別モデルのForeingKey)を参照する分だけクエリが発生する
for comment in comments:
    print(comment.user.username)
 
[良い例]

from django.db.models import Prefetch
from .models import Article, Comment  # Prefetcを使う場合はCommentまでインポート

article = Article.objects.prefetch_related(
    Prefetch(
        'comments',
        queryset=Comment.objects.select_related('user').filter(created__gte='2022-06-28').order_by('-created'),
        to_attr='article_comments')
    ).get()

comments = article.article_comments

# これで以下の処理では新たなクエリは飛ばない
for comment in comments:
    print(comment.user.username)
 


コードは短い方が良い、というのが原則ですが、処理速度に大きな影響がある場合はこのように工夫した方がよいとのことです。

ここまでお読みいただきありがとうございました!!


参考


この記事が気に入ったらサポートをしてみませんか?