2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Ruby on RailsAdvent Calendar 2021

Day 9

Railsでincludes(eager_load)した値を、where句を使用せずに絞り込む

Last updated at Posted at 2021-12-08

調べたけど意外に出てこなかったので書いてみます。
ActiveRecord の join, includes, preload, eager_load の違いについては、この記事では言及しません。

ユースケース

N + 1 問題回避のために includes を使用したいけれど、絞り込みには where 句を使いたくない。
例えば includes で取得した値に対して、同じ処理内で異なった条件での絞り込みをしたいとき。

結論

ActiveRecord::QueryMethods#where じゃなくて、 Array#select を使用して絞り込む。

[追記]
別のやり方として、association を応用する方法を教えていただきました。詳細はコメント参照

サンプルコード

本題

例えば以下のようなテーブル構造に対して、

image.png

次のようなレコードが用意されているとする。

author_one = Author.find_or_create_by!(name: 'author_one')
author_two = Author.find_or_create_by!(name: 'author_two')

book_one = Book.find_or_create_by!(name: 'book_one', author_id: author_one.id, book_type: 'magazine')
book_two = Book.find_or_create_by!(name: 'book_two', author_id: author_one.id, book_type: 'comic')
book_three = Book.find_or_create_by!(name: 'book_three', author_id: author_two.id, book_type: 'magazine')
book_four = Book.find_or_create_by!(name: 'book_four', author_id: author_two.id, book_type: 'comic')

eager_load(includes)などを使わない場合(N + 1 問題が発生する場合)

eager_load(includes)などを使わずに、下記のコードを実行すると N + 1 問題が発生する。

authors = Author.all
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--book_#{index}--", book
  end
end

# 出力結果
"--book_0--"
#<Book:0x00007f83b6adf9a8
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--book_1--"
#<Book:0x00007f83ba996fc0
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--book_0--"
#<Book:0x00007f83b7544628
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--book_1--"
#<Book:0x00007f83b7544538
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">

実行ログを見て発行される SQL を確認すると、以下のように N + 1 問題が発生している。

  Author Load (0.6ms)  SELECT "authors".* FROM "authors"
  Book Load (0.5ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]
  Book Load (0.1ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 2]]

eager_load(includes) + where を使って絞り込みを行う

eager_load(includes) + where を使って絞り込みを行うと、N + 1 問題を回避した上で次のように book_type が magazine のものだけが取得できる。

authors = Author.all.eager_load(:books).where(books: { book_type: 'magazine' })
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--magazine_#{index}--", book
  end
end

# 出力結果
"--magazine_0--"
#<Book:0x00007f83b74b9a78
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83ba97d2f0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">

SQL を確認すると、以下のように N + 1 問題が回避されたものとなる。

  SQL (0.1ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."book_type" = ?  [["book_type", "magazine"]]

しかし、追加で同じ処理内で異なる book_type のものを取得しようした場合、

authors = Author.all.eager_load(:books).where(books: { book_type: 'magazine' })
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--magazine_#{index}--", book
  end
end


+ authors.each do |author|
+   author.books.where(book_type: 'comic').each_with_index do |book, index|
+     pp "--comic_#{index}--", book
+   end
+ end

# 出力結果
"--magazine_0--"
#<Book:0x00007f83b74b9a78
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83ba97d2f0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--comic_0--"
#<Book:0x00007f83b74f4010
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--comic_0--"
#<Book:0x00007f83b74e7bd0
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">

追加で SQL が発行されてしまう。

  SQL (0.8ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."book_type" = ?  [["book_type", "magazine"]]
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ? AND "books"."book_type" = ?  [["author_id", 1], ["book_type", "comic"]]
  Book Load (0.1ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ? AND "books"."book_type" = ?  [["author_id", 2], ["book_type", "comic"]]

eager_load(includes) + select を使って絞り込みを行う

eager_load(includes) + select を使って絞り込みを行うと、同じ処理内で異なる book_type のものを取得しようした場合でも、N + 1 問題を回避することができる。

authors = Author.all.eager_load(:books)
authors.each do |author|
  magazines = author.books.select{ |book| book.book_type == 'magazine' }
  magazines.each_with_index do |magazine, index|
    pp "--magazine_#{index}--", magazine
  end
end

authors.each do |author|
  comics = author.books.select{ |book| book.book_type == 'comic' }
  comics.each_with_index do |comic, index|
    pp "--comic_#{index}--", comic
  end
end

# 実行結果
"--magazine_0--"
#<Book:0x00007f83b74dd428
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83b74f49c0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--comic_0--"
#<Book:0x00007f83b74f50a0
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--comic_0--"
#<Book:0x00007f83b74f4c40
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">

発行される SQL は一つだけとなっている。

  SQL (0.2ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id"

おまけ

N + 1 問題を回避するために includes などを使うと LEFT OUTER JOIN が発生するので、 JOIN するテーブルの規模がとってもとっても大きかったりすると、実は N + 1 を発生させたままにしておいた方が早いってことがあったりするらしい。

実際に色々試して計測して、一番早くなる方法を採用するのが良いですね。おわり。

2
2
2

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?