1
1

知っておきたいDjango ORMのクエリ最適化術:N+1問題を一撃で解消

Posted at

概要

DjangoのORMとは

DjangoのORM(Object-Relational Mapping)は、Djangoフレームワークの一部であり、データベースとのやり取りをオブジェクト指向の方法で行うためのツールです。ORMを使用することで、SQLクエリを直接書かずにデータベース操作を行うことができます。Pythonのオブジェクトを通じてデータベースのテーブルとやり取りできるため、コードがより直感的かつ保守しやすくなります。

from django.db import models

class Post(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    title = models.CharField(max_length=100)
    content = models.TextField()

class Comment(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    post = models.ForeignKey(Post, related_name="comments", on_delete=models.CASCADE)
    content = models.TextField()

上記のように、Djangoのモデルクラスを定義するだけで、Djangoはそのモデルに対応するデータベースのテーブルを自動的に作成し、管理できる。

ORMのクエリ最適化の重要性

ORMを使用すると、コードの生産性やメンテナンス性が向上しますが、その反面で自動生成されるクエリが必ずしも最適ではない場合があります。特に、大規模なアプリケーションや大量のデータを扱う場合、非効率なクエリはパフォーマンスのボトルネックになることがあります。そのため、ORMを適切に最適化することが重要です。これにより、データベースへの負荷を減らし、アプリケーション全体の応答性を向上させることができます。

N+1問題とは

N+1問題は、データベースのクエリを行う際に頻繁に発生するパフォーマンスの問題です。これは、最初に1つのクエリを実行し、その後に関連する各オブジェクトに対して追加のクエリ(N個)を実行することから名付けられています。このようなクエリの発生は、特に大規模なデータセットに対して非常に非効率です。

具体例

ブログアプリを想定して、以下のモデルを用意しました。

class Author(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    name = models.CharField(verbose_name=_("name"), max_length=50, blank=False, null=False)
    email = models.EmailField(verbose_name=_("email"), blank=False, null=False)
    created_at = models.DateTimeField(verbose_name=_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updateded_at"), auto_now=True)


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


class Blog(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    title = models.CharField(verbose_name=_("title"), max_length=50, blank=False, null=False)
    body = models.TextField(verbose_name=_("body"), blank=True, null=True)
    author = models.ForeignKey(
        Author, verbose_name=_("author"), related_name="blogs", on_delete=models.CASCADE
    )
    tags = models.ManyToManyField(Tag, verbose_name=_("tags"), related_name="blogs")
    created_at = models.DateTimeField(verbose_name=_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updateded_at"), auto_now=True)


class Comment(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    body = models.TextField(verbose_name=_("body"), blank=False, null=False)
    blog = models.ForeignKey(
        Blog, verbose_name=_("blog"), related_name="comments", on_delete=models.CASCADE
    )
    created_at = models.DateTimeField(verbose_name=_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updateded_at"), auto_now=True)

ここで、Blog一覧からそれぞれのブログの著者を取得する際に以下の処理を実行するとします。

blogs = Blog.objects.all()
for blog in blogs:
    print(blog.author)

これを実行すると、どのようなクエリが実行されるのでしょう?
djangoのDEBUGログを確認してみます。

SELECT `app_blog`.`id`, `app_blog`.`title`, `app_blog`.`body`, `app_blog`.`author_id`, `app_blog`.`created_at`, `app_blog`.`updated_at` FROM `app_blog`; args=();
SELECT `app_author`.`id`, `app_author`.`name`, `app_author`.`email`, `app_author`.`created_at`, `app_author`.`updated_at` FROM `app_author` WHERE `app_author`.`id` = 526 LIMIT 21; args=(526,);
SELECT `app_author`.`id`, `app_author`.`name`, `app_author`.`email`, `app_author`.`created_at`, `app_author`.`updated_at` FROM `app_author` WHERE `app_author`.`id` = 526 LIMIT 21; args=(526,);
...

このようにprint(blog.author)を実行するたびにクエリが実行されてしまいました。
これがN+1問題です。

N+1問題の解決方法

Djangoでは、select_relatedprefetch_relatedといったメソッドがあります。

select_relatedは一対一および一対多のリレーションを最適化し、prefetch_relatedは多対多および逆方向のリレーションを最適化します。これにより、必要なクエリ数を減らし、パフォーマンスを向上させることができます。

select_relatedの使用例

例えば、先ほどのN+1問題の具体例の場合、select_relatedを使って以下のように書き換えられます。

blogs = Blog.objects.select_related("author").all()
for blog in blogs:
    print(blog.author)

このクエリは、Blogモデルと関連するAuthorモデルのデータを単一のクエリで取得します。これにより、各ブログの著者情報にアクセスする際、追加のクエリを実行する必要がなくなります。

クエリを確認してみましょう。

SELECT `app_blog`.`id`, `app_blog`.`title`, `app_blog`.`body`, `app_blog`.`author_id`, `app_blog`.`created_at`, `app_blog`.`updated_at`, `app_author`.`id`, `app_author`.`name`, `app_author`.`email`, `app_author`.`created_at`, `app_author`.`updated_at` FROM `app_blog` INNER JOIN `app_author` ON (`app_blog`.`author_id` = `app_author`.`id`); args=();

INNER JOINを使って、ひとつのクエリを実行するようになりました。

prefetch_relatedの使用例

prefetch_relatedは、多対多または逆方向の外部キーリレーションシップを持つモデルに対して使用します。このメソッドは、関連するモデルのデータを別々のクエリで取得し、Pythonのレベルでそれらを結合します。

例えば、以下のBlogとTagが多対多の関係にあるので、BlogからTagを取得する場合を想定します。

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


class Blog(models.Model):
    id = models.AutoField(verbose_name=_("id"), primary_key=True)
    title = models.CharField(verbose_name=_("title"), max_length=50, blank=False, null=False)
    body = models.TextField(verbose_name=_("body"), blank=True, null=True)
    author = models.ForeignKey(
        Author, verbose_name=_("author"), related_name="blogs", on_delete=models.CASCADE
    )
    tags = models.ManyToManyField(Tag, verbose_name=_("tags"), related_name="blogs")
    created_at = models.DateTimeField(verbose_name=_("created_at"), auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updateded_at"), auto_now=True)

prefetch_relatedを使用しない場合、以下のクエリが実行されます。

blogs = Blog.objects.all()
for blog in blogs:
    print(blog.tags.all())
SELECT `app_blog`.`id`, `app_blog`.`title`, `app_blog`.`body`, `app_blog`.`author_id`, `app_blog`.`created_at`, `app_blog`.`updated_at` FROM `app_blog`; args=();
SELECT `app_tag`.`id`, `app_tag`.`name` FROM `app_tag` INNER JOIN `app_blog_tags` ON (`app_tag`.`id` = `app_blog_tags`.`tag_id`) WHERE `app_blog_tags`.`blog_id` = 1 LIMIT 21; args=(1,);
SELECT `app_tag`.`id`, `app_tag`.`name` FROM `app_tag` INNER JOIN `app_blog_tags` ON (`app_tag`.`id` = `app_blog_tags`.`tag_id`) WHERE `app_blog_tags`.`blog_id` = 2 LIMIT 21; args=(2,);
...

一方、prefetch_relatedを使用すると、以下のように2回のクエリで取得できます。

blogs = Blog.objects.prefetch_related("tags").all()
for blog in blogs:
    print(blog.tags.all())
SELECT `app_blog`.`id`, `app_blog`.`title`, `app_blog`.`body`, `app_blog`.`author_id`, `app_blog`.`created_at`, `app_blog`.`updated_at` FROM `app_blog`; args=();
SELECT (`app_blog_tags`.`blog_id`) AS `_prefetch_related_val_blog_id`, `app_tag`.`id`, `app_tag`.`name` FROM `app_tag` INNER JOIN `app_blog_tags` ON (`app_tag`.`id` = `app_blog_tags`.`tag_id`) WHERE `app_blog_tags`.`blog_id` IN (1, 2, ...); args=(1, 2, ...);

その他のクエリ最適化テクニック

deferとonlyの使い方

deferonlyは、特定のフィールドのロードを制御するために使用されます。deferは指定したフィールドを除いて、すべてのフィールドをロードします。一方、onlyは指定したフィールドのみをロードします。これにより、必要なフィールドのみを取得することで、パフォーマンスの向上が期待できます。

annotateとaggregateの使用例

annotateaggregateは、データベースの集計操作を行うためのメソッドです。annotateは各オブジェクトに集計結果を追加します。一方、aggregateは全体の集計結果を返します。これらを使用することで、データベースレベルでの集計操作により、パフォーマンスの向上が期待できます。

インデックスの利用

データベースのインデックスは、特定のフィールドに対するクエリの速度を向上させます。しかし、インデックスはデータの追加、更新、削除の速度を低下させる可能性があるため、適切なフィールドに対して使用することが重要です。

最後に

DjangoのORMは非常に強力で、複雑なデータベース操作を容易に行うことができます。しかし、その便利さにより、生成されるSQLが最適化されていないこともあります。そのため、適切な方法でクエリの最適化を行うことが重要です。

また、全ての最適化テクニックが全ての状況で有効なわけではありません。アプリケーションの要件、使用するデータベースの種類、データの量と種類により、最適なテクニックは変わる可能性があります。

さらに深く学びたい方は、以下のリソースを参照してください。

適切なメソッドを選択し、効率的なクエリを実行することで、アプリケーションのパフォーマンスを最適化することができます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1