📘

Django Rest FrameworkのPaginationクラスが発行するCOUNTが遅い時の対処

2020/10/10に公開

はじめに

フロントVue.js、バックエンドDjangoRestFrameworkで作っていますが、一部の検索が遅い…。
具体的に言うと、全件検索で15〜20秒以上掛かってしまいます。

他のAPIと比べても検索件数が多いわけでも無いですし、
実際にQuerysetから発行される生SQLをDB上で叩いても、5秒以内には終わります。

何が悪いのだろうとDjangoのログを見ていると、
検索前に以下のCOUNT(*)クエリが発行されていました。

 [DEBUG] utils 71 140228644845312 (22.130) SELECT COUNT(*) FROM (SELECT ...

その後実行されるSELECT文と比べると、durationの値(22.130と0.067)が全然違います。

 [DEBUG] utils 71 140228644845312 (0.067) SELECT ...

Countクエリを発行していたのは、Paginationクラスを指定していたから

該当のAPIを定義しているViewSetクラスは、HTMLでページングをしたい為、PageNumberPaginationを継承したクラスを指定していました。

こちらを、Noneと指定すると、事前にCOUNTクエリが発行されることはなくなり、Responseが返ってくる速度が早くなりました

    pagination_class = None

とはいえ、Paginationは使いたいです。

なので、COUNTクエリを発行しないように、レコードの件数を返すようにしてみます。

やったこと

Paginationクラスでいくつかのメソッドをオーバーライドしました。

from collections import OrderedDict

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class HogePagination(PageNumberPagination):
    # ページサイズ
    page_size = 500

    # QuerySetで算出されるItem数
    count_local = 0

    def __init__(self):
        super().__init__()

    def paginate_queryset(self, queryset, request, view=None):
        self.count_local = len(queryset)
        return super().paginate_queryset(queryset, request, view)

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.count_local),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))

paginate_querysetに渡ってくるquerysetのアイテム数をlen()で取得し、
それをget_paginated_responsecountキーの値として返すことをしています。

これでCOUNTクエリを発行せず、全体の件数を取得することができました。

そもそもCOUNTクエリでこんなに遅くなるのは、いろんなテーブルをINNER JOIN(Queryset的には、select_related)して、そもそものSELECT文自体が複雑になっている可能性があるので、
テーブル構成を見直したほうがいいんじゃない? という気もしてますが、とりあえずはこれで。

参考にしたもの

Discussion