# 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 クエリ問題。
2. select_related と prefetch_related
解決策は 関連を事前にまとめて取得する。
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_relatedやprefetch_relatedを使った基本形をすぐに出してくれる。 -
既存コードを提示して修正を依頼
「この ViewSet を N+1 対策して」と、自分のコードをそのまま渡すと、
適切な関連取得や Serializer の最適化を提案してくれるケースが多い。 -
過剰最適化に注意
AI は「とにかく全部 prefetch_related する」ようなコードを出すこともある。
実際には一覧と詳細で取得データを分けるなど、責務を整理しないと逆に重くなる。
→ 最適化の方向性は人間が設計し、AIには具体的な修正を任せる のが安全。 -
キャッシュやページネーションも依頼できる
「このAPIをキャッシュ対応して」「無限スクロール向けにして」と依頼すれば、
低レベルキャッシュや CursorPagination のコードを補完してくれる。
つまり、AI は 雛形生成とリファクタ提案 に強いが、
「どこを軽くするか」「どこはあえて重くてもよいか」という設計判断は人間側の役割。
まとめ
パフォーマンス改善のポイントは次の通り。
-
N+1 クエリ対策:
select_related/prefetch_relatedを徹底 - Serializer 側の最適化: 無駄なクエリを発行しない
- ページネーション: 大量データを小分けに返す
- キャッシュ: 重い処理はキャッシュでレスポンス高速化
- API の責務分離: 一覧・詳細・集計を分けて設計
これらを組み合わせることで、業務システムでも 安定して高速な API を提供できる。
次回は レスポンス設計のベストプラクティス を紹介する予定。
Discussion