0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N+1 の解決

Last updated at Posted at 2025-05-22

はしがき

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

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

テーブルレコード

3.png

4.png

rails から問題発生

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

6.png

発生しました!

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

解決方法

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

preload

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

8.png
9.png

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

eager_load

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

11.png
12.png

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

includes

15.png
14.png

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

追加的なこと

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

ロジックを修正してみる

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

17.png
18.png

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

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

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
21.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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?