初学者が見逃しがちなN+1とは【Rails、Django】

# はじめに

みなさん、こんにちは。Happiness Chainメンターのryoです。

今回は、RailsやDjangoの課題を取り組んでいる受講生のコードレビューをする際に、よく指摘するN + 1について、詳細と解決策をお話します。

# N+1問題とは

RailsやDjango等に付帯するORMで、1つのテーブルから関連するテーブルのデータも取得する際に、発生する問題です。
例えばXのツイート機能で、ツイート一覧を表示する画面にて説明すると、
ツイートには、ユーザーが紐づいており、ユーザー画像を表示しています。

![tweet_list.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/106f06e5-bd60-0919-5864-b7740dd49f2b.jpeg)

Railsに以下の様なcontroller, viewを記述しているとします。

```rb:(Rails)
class TweetsController < ApplicationController
  def index
    @tweets = Tweet.all
  end
end
```

```erb:(Rails)
<% @tweets.each do |tweet| %>
  <div>
    <div>
      <p><%= tweet.content %></p>
    </div>
  </div>
<% end %>
```

Djangoには以下の様なview、templateを記述しているとします。

```py:(Django)
class TweetListView(Listview):
    template_name = 'tweet_list.html'
    query_set = Tweet.objects.all()
```

```html:(Django)
{% for tweet in object_list %}
    <div>
        <div>
            <p>{{ tweet.content }}</p>
        </div>
    </div>
{% endfor %}
```

上記の場合に、裏側ではSQLが叩かれてデータを取得するかと思いますが、以下のようなSQLが叩かれます。

```sql
SELECT `tweets`.`id`, `content`, `user_id` FROM `tweets`;
```

上記の場合、特に問題ないのですが、以下のようにユーザー画像を表示するとします。

```diff:(Rails)
<% @tweets.each do |tweet| %>
    <div>
+      <img src={{ tweet.user.img }}>
       <div>
         <p><%= tweet.content %></p>
       </div>
    </div>
<% end %>
```

```diff:(Django)
{% for tweet in object_list %}
    <div>
+       <img src={{ tweet.user.img }}>
        <div>
            <p>{{ tweet.content }}</p>
        </div>
    </div>
{% endfor %}
```

上記の場合は、以下のようなSQLが叩かれます。
まず、ツイート一覧を取得したあとに、ユーザーを1つ1つSQLを分けて取得しています。

```sql
SELECT tweets.id, content, user_id FROM tweets;
SELECT users.id, img FROM users WHERE users.id = 1;
SELECT users.id, img FROM users WHERE users.id = 2;
SELECT users.id, img FROM users WHERE users.id = 3;
SELECT users.id, img FROM users WHERE users.id = 4;
...
```

これは何が起こっているのかというと、ツイート一覧を取得する段階では、以下のSQLのみですが、

```sql
SELECT tweets.id, content, user_id FROM tweets;
```

`for tweet in object_list`、`@tweets.each do |tweet|`のループ処理内で、`tweet.user.img`と記述しているので、tweetのuser_idから検索をかけて、userを一つ取得するため、tweetの数だけ、sqlが叩かれることになります。
tweetが10件の場合は、10回userを取得するSQLが叩かれます。レコードの数 + 1件SQLが叩かれるので、**N+1問題**と呼ばれています。

# 解決方法
Rails、DjangoのORM共に、一覧データ取得時に結合、もしくはあとで一括取得する（`where id = (1, 2, 3, 4...)`のような形でfilterする）メソッドがあります。
Rails、Djangoにはそれぞれいくつかメソッドがあるのですが、関連の種類によってSQLのパフォーマンスが変動します。

### 多対1の関係の場合

tweetに紐づくuserを取得するような親レコードを取得する場合、1つのSQLで、join句を使用し結合元のデータと一緒に取得する、`eager_load`、`select_related`がよいです。

- Railsの場合: `eager_load`

```rb:(Rails)
Tweet.eager_load(:user)
```

- Djangoの場合: `select_related`

```py:(Django)
Tweet.objects.select_related('user')
```

実際に発行されるSQL

```sql
SELECT tweets.id, tweets.content, tweets.user_id, users.id, users.img FROM tweets INNER JOIN users ON tweets.user_id = users.id;
```

### 1対多の関係の場合

userに紐づくtweetを複数取得する場合、user一覧を取得したあと、そのuserのid一覧にて、tweetを絞り込む`preload`、`prefetch_related`が良いです。

- Railsの場合: `preload`

```rb:(Rails)
User.preload(:tweets)
```

- Djangoの場合: `prefetch_related`

```py:(Django)
User.objects.prefetch_related('tweets')
```

実際に発行されるSQL

```sql
SELECT users.* FROM users;
SELECT tweets.* FROM tweets WHERE tweets.user_id IN (1, 2, 3, ...);
```

またN+1に関しては、検知できるライブラリがあるので、是非入れましょう。

- Railsの場合: `bullet`
- Djangoの場合: `django-debug-toolbar`

# 終わりに

いかがだったでしょうか？
現場では、取り扱うデータ数が多いので、N + 1を対応しておかないと、パフォーマンスは目に見えて落ちますので、是非おさえておきましょう。