0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Django】select_relatedとprefetch_relatedを使ったクエリ最適化

Last updated at Posted at 2025-01-20

select_relatedprefetch_related

DjangoのORMで関連オブジェクトを効率的に取得し、N+1問題を防ぐためのメソッドです。

select_related

  • 主な用途
    外部キー(ForeignKey)や一対一(OneToOneField)などのリレーションを持つモデルに対して使用します。
  • 特徴
    1つのSQLで関連先のモデルを結合し、一括でデータを取得します(主にINNER JOINやLEFT OUTER JOINなどを自動生成)。
  • 具体例
    サッカープレイヤー(SoccerPlayers)の情報を取得するとき、関連するポジション(Position)や所属チーム(Team)の情報も同時に取得できます。

prefetch_related

  • 主な用途
    多対多(ManyToManyField)のリレーション、あるいはForeignKeyを逆参照する際に使用します。
  • 特徴
    関連先をまとめて取得するための追加クエリが発行されますが、一度に一括取得できるのでN+1問題を回避できます。
  • 具体例
    サッカープレイヤー(SoccerPlayers)が習得しているスキル(Skills)を一度にまとめて取得できます。

実装例

例1)SoccerPlayers モデルを起点として、紐づいている Skills モデル・Position モデルのデータを一括で取得する

players = SoccerPlayers.objects.select_related("position").prefetch_related("skills")
  • この状態でポジション名を取り出すには、以下のように記載します。

    position_name = players.first().position.name
    
  • 保有スキルの一覧を取り出すには、以下のように記載します。

    skills = players.first().skills.all()  # プレイヤーが持っているスキルをリスト形式で取得
    

補足:
このようにselect_relatedprefetch_relatedを使わないと、プレイヤーを取得するたびにチーム名やスキルなどを個別のクエリで取得するN+1問題が発生しやすくなるため、効率が悪くなります。

例2)TrainingPlan モデルを起点として、紐づいている SoccerPlayerContractTeamLeague などをまとめて取得する

サッカー選手のトレーニング計画を管理する例を考えてみましょう。TrainingPlan モデルが下記のような関連を持っているとします。

  • TrainingPlanSoccerPlayer:外部キー (ForeignKey)
  • TrainingPlanContract:外部キー
  • ContractTeam:外部キー
  • TeamLeague:外部キー

これらの関係を使って一度に関連データを取得したい場合、以下のように書くことができます。

training_plans = TrainingPlan.objects.select_related(
    "soccer_player",          # ForeignKey想定
    "contract__team__league"  # 複数段階の関連をまとめてJOIN可能
)
  • 特定の選手(soccer_player=1)に紐づく契約一覧を取得したい場合

    contracts = training_plans.filter(soccer_player=1).contract.all()
    

    これにより、選手IDが1のトレーニング計画に関連づけられた契約のリストを取得できます。

  • 特定のリーグ名("PremierLeague")に所属するチームでフィルタしたい場合

    filtered_plans = TrainingPlan.objects.filter(
        contract__team__league__name="PremierLeague"
    ).select_related("soccer_player", "contract", "team", "league")
    

    リーグ名が "PremierLeague" のチームを対象に、関連するトレーニング計画を一括でロードしてフィルタできます。

このように、select_related や多段階のリレーション指定をうまく組み合わせることで、深い階層のモデルを効率よくまとめて取得でき、N+1問題を回避できます。

(補足)Django ORMを通して発行されるクエリの確認方法

N+1問題を起こしていないか確認するために、実際に発行されるSQLクエリをチェックすることが重要です。

1. クエリセットの末尾に .query をつける

query = SoccerPlayers.objects.filter(id__gt=10).select_related("team").query
print(query)

これにより、内部的に生成されるSQL文を確認できます。

2. Django Debug Toolbar を導入してブラウザで確認する

画面表示時に実行されるクエリ数やクエリ内容をブラウザで可視化できるため、N+1問題の発見に便利です。
公式ドキュメント関連ブログ記事などを参考にしてください。

複数の関連モデルを取得してデータ構造が複雑になる場合

データ構造がさらに複雑になる場合は、Django Rest Framework(DRF)の導入を検討するのも一つの手です。

DRFとは

DjangoでWeb APIを簡単に構築するためのフレームワークです。

  • 利点: モデルのデータをJSONやXMLなどの形式で柔軟にシリアライズできるため、データ構造が複雑な場合でも管理しやすい。
  • 注意点: SPA(Single Page Application)向けのAPI構築だけでなく、通常のWebアプリでも便利ですが、多少学習コストがかかります。

コーディング例

以下は、サッカー選手情報を取得する関数の例です。select_relatedprefetch_related を用いて関連データをまとめて取得し、辞書に整理しています。

def get_player_data(player_id):
    player = SoccerPlayers.objects.select_related(
        'team',
        'position'
    ).prefetch_related('skills').get(id=player_id)

    player_info = {
        'id': player.id,
        'name': player.name,
        'team': getattr(player.team, 'name', None),
        'league': getattr(player.team.league, 'name', None) if player.team else None,
        'position': getattr(player.position, 'name', None),
        'skills': list(player.skills.values_list('name', flat=True)),
    }

    return player_info
  • team が存在しない場合にエラーを起こさないよう、getattr で安全に取り出しています。
  • skills.values_list('name', flat=True) を使うことで、スキル名の一覧だけを簡単にリスト化できます。

0
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?