61:ORMのN+1問題を回避しよう

プログラミング迷子: N+1問題を回避するORMの書き方は?

  • 後輩W:ログを出して発行されるSQLを確認するのはわかったんですが、件数に比例してSELECTがたくさん発行されてしまうのは、どうやって直せば良いんでしょうか?

  • 先輩T:N+1問題は、Djangoの場合、 select_relatedprefetch_related を使えば解決できるよ。

  • 後輩W:それじゃあ、常にそれを使うようにコードを書けば解決するんじゃないですか?

  • 先輩T:いやいや、常に使ってしまうと関連テーブルのデータを全く必要としないときにもデータを取得してデータベースに負荷をかけてしまうことになるよ。

具体的な失敗

プログラムのループ処理で、複数のIDそれぞれについてデータベースにSQLを発行すると、件数に比例してクエリ実行回数が増加して、パフォーマンスに影響が出ます。 たとえば以下のようなコードです。

def process_tasks(ids):
    for pk in my_ids:
        task = Task.objects.get(pk=id)
        ...

my_ids = [1, 2, 3, 4, 5]
process_tasks(my_ids)

このようなコードは、コードレビューなどで指摘されて、すぐに修正されるでしょう。 では、以下のコードではどうでしょうか。

def process_tasks(mail: Mail):
    for attach in mail.mailattach_set.all():
        task = attach.task
        ...

mail = Mail.objects.first()
process_tasks(mail)

このコードに登場する、mail, attach, taskがどんなオブジェクトなのかは、このコードだけではわかりません。 注意深くレビューする人であれば、変数それぞれが何のオブジェクトなのかを調べることで、問題に気づけるかもしれません。

メールに添付された複数のファイルそれぞれからタスク化して業務を進めるシステムの例を考えてみましょう。

cover

(中略)詳細は書籍 自走プログラマー をご参照ください

ベストプラクティス

ログを出力して、発行されているSQLを理解しましょう。 前述の例のように、ログ出力されていれば、どのようなSQLが発行されているかは簡単にわかります。 そのSQLを読み解いて、それがパフォーマンスに影響を及ぼすSQLだと理解する必要があります。

発行されているSQLを読み解いた後は、Django ORMの知識も必要となります。 N+1問題を回避する方法は、Djangoの公式ドキュメントのQuerySet API reference 1prefetch_related に記載されています。

ここでは、 prefetch_related を使って前述のコードを修正する例を紹介します。

1

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

cover

(中略)詳細は書籍 自走プログラマー をご参照ください

関連