N+1 の解決

# はしがき

2023年度末ぐらい、「N+1の問題」がありました。その時は、なんとなく、解決しましたが、解決の内容や知識など、整理しておきたい気持ちなので、記事を作成します。

# N+1の問題 ってなに？

(ORM の)OOP や RDB のパラダイムから発生する問題
ORM から loop 処理途中、loop の毎回 SQL を発生、これでパフォーマンスが低下される問題

OOP の Object は他の Object と関係を持つことができます。
メモリの random access で、ある Object の関係になっている他の Object にリファレンスできます。

Random access: https://en.wikipedia.org/wiki/Random_access

RDB にもテーブルとかテーブルは関係を持つことができます。
しかし、単純に、あるテーブルから関係になっている他のテーブルのレコードを取得することではなく、追加的なクエリーが必要です。

つまり、OOP はリファレンス(参考しているアドレス)から、他のテーブルのデータの取得できます。
RDB は追加クエリーで、他のテーブルのデータの取得できます。

# 問題を発生してみよう

## 前提のテーブル

### テーブル関係

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/3a5f2b48-cc96-4988-adbe-234c78013dd3.png)

`brand:machine = 1:N` 関係です。

### テーブルレコード

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/9409494e-0ab6-4316-9f76-eff253018ce1.png)

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/218c5d6d-300f-4bad-bcbe-344b7bf7ad6e.png)

## rails から問題発生

```ruby
# 悪いこーど生成
Brand.all.each do |b|
  if b.machines.size >= 2
    b.name = "big " + b.name
    b.save!
  end
end
```

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/7990193c-59de-4b50-87b3-5ebe6c516062.png)

発生しました！

Brand のレコードのごとに SQL が実行されます。
これはパフォーマンスによくない影響があるはずです。

# 解決方法

Query の内容も確認したかったので、`log/development.log` から確認しました。

## preload

```ruby
Brand.preload(:machines).all.each do |b|
  if b.machines.size >= 2
    b.name = "big " + b.name
    b.save!
  end
end
```

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/df5d956d-6e8b-4da1-9b72-a25af76f7a2c.png)
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/5f9692ef-03c2-49eb-bc92-5b8ac4ea24e9.png)

指定した関係のテーブルの複数のクエリーで取得して、キャッシュします。
preload の場合、絞り込み条件(where など)には元のテーブルのカラムのみ使えます。

## eager_load

```ruby
Brand.eager_load(:machines).all.each do |b|
  if b.machines.size >= 2
    b.name = "big " + b.name
    b.save!
  end
end
```

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/773d4364-a35f-42d1-ba08-3a9c03f60e28.png)
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/05e49c44-38bd-4ce8-bf10-1e0847e17e1a.png)

指定した関係のテーブルの LEFT OUTER JOIN で取得して、キャッシュします。
LEFT OUTER JOIN なので、絞り込み条件(where など)に対応するテーブルのカラムが使えます。

## includes

![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/abcab006-ea8f-4500-b90e-9364477fd0fe.png)
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/4ea1b0f5-e975-487a-b4a7-ece4b18d2967.png)

preload と eager_load を柔軟に対応します。

## 追加的なこと

元々ロジックが悪いので、N+1の問題になっているかもしれませんね。
これのロジックを修正してみます。

### ロジックを修正してみる

```ruby
Brand.where(id: Machine.group(:brand_id)
                       .having('COUNT(*) >= 2')
                       .pluck(:brand_id))
     .update_all("name = CONCAT('big ', name)")
```

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/79cf01c5-67c2-488e-a250-11e3301922a1.png)
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/61ca369b-b7a7-4d03-8b1f-a3c1aa58c5e1.png)

ここは pluck を使いましたが、pluck は追加クエリーが生成されるので、select の方が良いかもしれません。

https://kossy-web-engineer.hatenablog.com/entry/2020/11/28/095127

### 変更したロジックを subquery を使ってみる

```SQL
UPDATE brands
SET name = CONCAT('big ', name)
WHERE id IN (
  SELECT m.brand_id
  FROM machines as m
  GROUP BY m.brand_id
  HAVING COUNT(*) >= 2
);
```

![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/c93ec82d-873d-45b6-8a97-052591a25a9c.png)
![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582489/19201921-0c1a-400a-a473-5c1d7387e202.png)

むしろ、クエリーをはっきりしていたら、クエリーで直接にレコードを取得する方法が良いかもしれません

# 参考&関連 URL

https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1
https://qiita.com/muroya2355/items/d4eecbe722a8ddb2568b
https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361
https://velog.io/@xogml951/JPA-N1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%B4%9D%EC%A0%95%EB%A6%AC
https://gmlwjd9405.github.io/2019/02/01/orm.html
https://stackoverflow.com/questions/22881974/how-to-understand-reference-in-object-oriented-programming
https://en.wikipedia.org/wiki/Reference_(computer_science)
https://nippondanji.blogspot.com/2015/06/rdb.html
https://coding-factory.tistory.com/870
https://programmer93.tistory.com/83
https://railsguides.jp/active_record_querying.html
https://qiita.com/k-o-u/items/31e4a2f9f5d2a3c7867f