Joifup Blog

Django ORM: select_relatedとprefetch_relatedの違いと実際の挙動

2023-06-03

DjangoN+1問題select_relatedprefetch_related

はじめに

DjangoはPythonで作られた素晴らしいフレームワークで、そのORM(Object-Relational Mapping)はデータベース操作を非常にシンプルにします。しかし、複数のモデルが関連付けられている場合、どのようにクエリを最適化するかが重要になります。ここで登場するのが、select_relatedprefetch_relatedです。

select_relatedについて

select_relatedはDjangoのクエリセットAPIの一部で、外部キーまたは一対一の関連を持つモデルのデータを単一のSQLクエリで取得します。

例えば、以下のようなモデルがあるとします:

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

そして、次のように書かれたコードがあるとします:

books = Book.objects.all()
for book in books:
    print(book.author.name)

select_relatedを使用せずにこのコードを実行すると、各ループで新たなSQLクエリが発行され、本の数だけデータベースへのアクセスが増えてしまいます。しかし、select_relatedを使用すると、次のように一度のクエリで必要な情報をすべて取得できます:

books = Book.objects.select_related('author').all()
for book in books:
    print(book.author.name)

この場合、単一のSQLクエリが実行され、必要なすべての情報が一度に取得されます。

prefetch_relatedについて

prefetch_relatedもまた、関連データを効率的に取得するためのDjangoのクエリセットAPIです。しかし、これは多対多関係や逆外部キー関係を持つモデルに対して最適化されています。

たとえば、以下のようなモデルがあるとします:

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

そして、次のようなコードがあるとします:

books = Book.objects.all()
for book in books:
    for author in book.authors.all():
        print(author.name)

上記のコードでは、各本のために新たなSQLクエリが発行されるため、パフォーマンスの低下が生じます。しかし、prefetch_relatedを使用すると、次のようになります:

books = Book.objects.prefetch_related('authors').all()
for book in books:
    for author in book.authors.all():
        print(author.name)

select_relatedとprefetch_relatedの比較

select_relatedprefetch_relatedはどちらも関連データを効率的に取得するためのものですが、それぞれ異なるシチュエーションで最適化されています。select_relatedは一対一または外部キー関係に対して最適化されており、prefetch_relatedは逆外部キーまたは多対多関係に対して最適化されています。

実際のユースケース

ブログサイトでは、多くの場合、各ブログポスト(記事)が著者と関連付けられ、また多数のコメントもそれに紐付けられています。ここではそのようなモデルがあると仮定しましょう。

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

class Comment(models.Model):
    post = models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE)
    content = models.TextField()

ここでは、PostモデルがAuthorモデルに対して外部キーを持ち、CommentモデルがPostモデルに対して外部キーを持っています。

  1. select_relatedの使用例

select_relatedを使って、ブログポストとその著者を一度のクエリで取得しましょう。これは以下のように行います。

posts = Post.objects.select_related('author').all()
for post in posts:
    print(f'Title: {post.title}, Author: {post.author.name}')

このコードは、ブログポストとその著者の情報を一度のデータベースクエリで取得します。これにより、各ポストの著者情報を取得するために新たなクエリを発行する必要がなくなり、パフォーマンスが向上します。

  1. prefetch_relatedの使用例

次に、prefetch_relatedを使って、ブログポストとそのコメントを効率的に取得する方法を見てみましょう。

posts = Post.objects.prefetch_related('comments').all()
for post in posts:
    print(f'Title: {post.title}, Number of comments: {len(post.comments.all())}')

上記のコードでは、まず全てのブログポストを取得し、次に各ポストに紐づくコメントを別のクエリで一括取得します。これにより、各ポストのコメントを取得するために新たなクエリを発行する必要がなくなり、パフォーマンスが向上します。

まとめ

Djangoのselect_relatedprefetch_relatedは、データベースのクエリを効率化する強力なツールです。それぞれの使用方法と適用場面を理解することで、アプリケーションのパフォーマンスを最適化することができます。

参考文献