📑

# 3.5 パフォーマンス改善と N+1 対策(Django REST Framework 編)

に公開

パフォーマンス改善と N+1 対策(Django REST Framework 編)

前回の記事ではフィルタリングと検索についてまとめた。
今回は API 設計で避けて通れない パフォーマンス改善、特に N+1 クエリ問題 を取り上げる。


1. N+1 クエリ問題とは

典型的な例: 注文と注文アイテムをネストして返す場合。

class OrderDetailSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True)

    class Meta:
        model = Order
        fields = ["id", "customer_name", "total_amount", "items"]

このまま Order.objects.all() を直列化すると…

  • 注文一覧を取得 → 1 クエリ
  • 各注文のアイテム取得 → 注文数 × 1 クエリ

結果、注文が 100 件なら 101 クエリ が発行される。
これが N+1 クエリ問題。


解決策は 関連を事前にまとめて取得する

class OrderViewSet(viewsets.ModelViewSet):
    queryset = Order.objects.all().prefetch_related("items")
    serializer_class = OrderDetailSerializer
  • select_related: 外部キー(1対1 / 多対1)の関連を JOIN で取得
  • prefetch_related: 1対多 / 多対多の関連をまとめて取得

これで注文 100 件でも「注文取得 1 クエリ + アイテム取得 1 クエリ」の合計 2 クエリで済む。

補足: 使い分けのイメージ

  • select_related

    • JOIN で一度に取得
    • 「親を取るときに子が1つだけ紐づく」ケース(多対1 / 1対1)に最適
    • 例: 注文 → 顧客(1つの注文は必ず1人の顧客に紐づく)
  • prefetch_related

    • 最初に親をまとめて取得 → 別クエリで子を一気に取得 → Python側でマッピング
    • 「親に子が複数紐づく」ケース(1対多 / 多対多)に最適
    • 例: 注文 → 注文アイテム(1つの注文に複数のアイテムがある)

👉 簡単に言うと

  • 「子が1つだけなら select_related
  • 「子が複数なら prefetch_related

と覚えると実務で迷いにくい。


3. Serializer での最適化

Serializer 側で無駄なクエリを発行しない工夫も必要。

悪い例

class OrderSerializer(serializers.ModelSerializer):
    item_count = serializers.SerializerMethodField()

    def get_item_count(self, obj):
        return obj.items.count()  # 各注文ごとにクエリ発行

良い例

class OrderViewSet(viewsets.ModelViewSet):
    queryset = Order.objects.all().prefetch_related("items")
class OrderSerializer(serializers.ModelSerializer):
    item_count = serializers.IntegerField(source="items.count", read_only=True)

関連を事前取得しておけば .count はキャッシュを利用できる。


4. ページネーションでレスポンスを軽量化

大量データを一度に返すと処理が重くなる。
DRF のページネーションを組み合わせることで、返すデータを制御できる。

  • PageNumberPagination: ページ番号ベース
  • LimitOffsetPagination: limit / offset 指定
  • CursorPagination: 大量データや無限スクロール向け
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
    "PAGE_SIZE": 100
}

5. キャッシュの活用

集計系 API ではキャッシュも有効。

例: Django の低レベルキャッシュ

from django.core.cache import cache

def get_sales_summary():
    data = cache.get("sales_summary")
    if not data:
        data = calc_sales_summary()  # 重い処理
        cache.set("sales_summary", data, 60 * 5)  # 5分キャッシュ
    return data

API のレスポンスをキャッシュしておくことで、
同じリクエストに対しては即座に返せる。


6. 業務システムでの工夫

  • 一覧は軽量に、詳細で重く
    → 一覧 API では最小限の情報だけ、詳細 API で関連データを展開する
  • 集計は専用 API を用意する
    → 一覧 API に集計処理を混ぜない
  • バッチ処理との住み分け
    → 即時計算が重すぎるものはバッチで事前計算し、API では結果だけ返す

AI活用の観点

N+1 対策やパフォーマンス改善も、AI にコードを生成させるときに工夫できるポイントがある。

  • 最初はおまかせで依頼
    「Django REST Framework で N+1 を避ける実装例を教えて」といえば、
    select_relatedprefetch_related を使った基本形をすぐに出してくれる。

  • 既存コードを提示して修正を依頼
    「この ViewSet を N+1 対策して」と、自分のコードをそのまま渡すと、
    適切な関連取得や Serializer の最適化を提案してくれるケースが多い。

  • 過剰最適化に注意
    AI は「とにかく全部 prefetch_related する」ようなコードを出すこともある。
    実際には一覧と詳細で取得データを分けるなど、責務を整理しないと逆に重くなる。
    最適化の方向性は人間が設計し、AIには具体的な修正を任せる のが安全。

  • キャッシュやページネーションも依頼できる
    「このAPIをキャッシュ対応して」「無限スクロール向けにして」と依頼すれば、
    低レベルキャッシュや CursorPagination のコードを補完してくれる。

つまり、AI は 雛形生成とリファクタ提案 に強いが、
「どこを軽くするか」「どこはあえて重くてもよいか」という設計判断は人間側の役割。


まとめ

パフォーマンス改善のポイントは次の通り。

  • N+1 クエリ対策: select_related / prefetch_related を徹底
  • Serializer 側の最適化: 無駄なクエリを発行しない
  • ページネーション: 大量データを小分けに返す
  • キャッシュ: 重い処理はキャッシュでレスポンス高速化
  • API の責務分離: 一覧・詳細・集計を分けて設計

これらを組み合わせることで、業務システムでも 安定して高速な API を提供できる。


次回は レスポンス設計のベストプラクティス を紹介する予定。

Discussion