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


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

https://zenn.dev/kinzal/articles/c5745e7d9a950c

### `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` モデルのデータを一括で取得する

```python
players = SoccerPlayers.objects.select_related("position").prefetch_related("skills")
```

- この状態でポジション名を取り出すには、以下のように記載します。

  ```python
  position_name = players.first().position.name
  ```

- 保有スキルの一覧を取り出すには、以下のように記載します。

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

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

### 例2）`TrainingPlan` モデルを起点として、紐づいている `SoccerPlayer`・`Contract`・`Team`・`League` などをまとめて取得する

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

- `TrainingPlan` → `SoccerPlayer`：外部キー (`ForeignKey`)
- `TrainingPlan` → `Contract`：外部キー
- `Contract` → `Team`：外部キー
- `Team` → `League`：外部キー

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

```python
training_plans = TrainingPlan.objects.select_related(
    "soccer_player",          # ForeignKey想定
    "contract__team__league"  # 複数段階の関連をまとめてJOIN可能
)
```

- **特定の選手（`soccer_player=1`）に紐づく契約一覧を取得したい場合**  
  ```python
  contracts = training_plans.filter(soccer_player=1).contract.all()
  ```
  これにより、選手IDが1のトレーニング計画に関連づけられた契約のリストを取得できます。

- **特定のリーグ名（`"PremierLeague"`）に所属するチームでフィルタしたい場合**  
  ```python
  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` をつける
```python
query = SoccerPlayers.objects.filter(id__gt=10).select_related("team").query
print(query)
```
これにより、内部的に生成されるSQL文を確認できます。

### 2. Django Debug Toolbar を導入してブラウザで確認する
画面表示時に実行されるクエリ数やクエリ内容をブラウザで可視化できるため、N+1問題の発見に便利です。  
[公式ドキュメント](https://django-debug-toolbar.readthedocs.io/en/latest/)や[関連ブログ記事](https://self-methods.com/django-debug-toolbar-setting/)などを参考にしてください。



## 複数の関連モデルを取得してデータ構造が複雑になる場合
データ構造がさらに複雑になる場合は、Django Rest Framework（DRF）の導入を検討するのも一つの手です。

### DRFとは
DjangoでWeb APIを簡単に構築するためのフレームワークです。  
- **利点**: モデルのデータをJSONやXMLなどの形式で柔軟にシリアライズできるため、データ構造が複雑な場合でも管理しやすい。  
- **注意点**: SPA（Single Page Application）向けのAPI構築だけでなく、通常のWebアプリでも便利ですが、多少学習コストがかかります。

https://qiita.com/pythonista/items/1babe212609aa159af77

## コーディング例

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

```python
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)` を使うことで、スキル名の一覧だけを簡単にリスト化できます。



https://just-python.com/document/django/orm-query/select_related-prefetch_related