こんにちは、ryohei515です。
Ruby on Rails で、ActiveRecord::QueryMethods
の select
メソッドがありますが、使ってますか?
私はあまり使ってませんでした。
というのも、大抵のケースで pluck
を使うほうがパフォーマンス的にメリットが大きいと思い、そちらを多く使っていたからです。
しかし、最近 select
を使ったほうがメリットがあるケースもあることを知ったので、ご紹介します。
select
の基本的な使い方
例えば、本を検索するには
books = Book.where('title like ?', "%#{query}%") => #<ActiveRecord::Relation [#<Book id: 1, title: "hoge", created_at: ...>, #<Book id: 2, title: "fuga", created_at: ...>]
みたいに取得できます。
しかし、業務では以下のパフォーマンスの懸念より、この書き方を避けることができないかを考えることが多いです。
- ActiveRecord のインスタンスの生成コスト
- 取得したデータのメモリ使用量
- 不要な項目のDBからの取得(SQL実行コスト)
そこで、必要な項目だけを2次元配列で取得できる pluck
を使うことを考えます。
これなら上記の懸念全てに対し効果があり、必要最低限の処理にできるためです。
books = Book.where('title like ?', "%#{query}%").pluck(:id, :title) => [[1, 'hoge'], [2, 'fuga']]
ただ、Book
のインスタンスメソッドを使いたいといったことがあったりします。
pluck
だと Array
となってしまいそれが満たせないので、代わりに select
を利用することを考えます。
select
を使えば、ActiveRecord のインスタンスの生成コストはあるものの、SQL の段階で不要な項目を DB から取ってこないようにできるので、全体的なコストはselect
なしのときより落とせます。
books = Book.where('title like ?', "%#{query}%").select(:id, :title) => #<ActiveRecord::Relation [#<Book id: 1, title: "hoge">, #<Book id: 2, title: "fuga">]
この程度の認識しかなかったため、パフォーマンス優位であろう pluck
を積極的に使い、select
を使うことはあまりありませんでした。
しかし、select
のほうがパフォーマンス的にも優位になる場合があることを知ったので、以下に記載します。
where
の対象を select
で設定する
例えば図書館のアプリとして、以下のようなテーブル構成があったとします。
※ returned_date
が返却日を示しており、1冊の本がレンタルされた履歴は全て rentals
に保存されます。
ここで、「過去に1度だけレンタルがされたことがある本」を取得したいとなったとします。
SQL における HAVING
で集計結果を抽出条件とすることができるため、これを使いつつ ActiveRecord で取得しようとすると、以下のような書き方をしていました。
books = Book.where(id: Rental.group(:book_id).having("count(*) = 1").pluck(:book_id))
該当の book_id
の一覧を pluck
で配列として取得し、それを抽出条件に使う形です。
しかし、SQL的には book_id
の一覧取得と books
の取得により、2回実行する結果になってしまいます。
SELECT "rentals"."book_id" FROM "rentals" GROUP BY "rentals"."book_id" HAVING COUNT(*) = 1 ; -- 取得結果が [1, 2, 3] だったすると SELECT "books".* FROM "books" WHERE "books"."id" IN (1, 2, 3) ;
SQL で書くなら1度の SQL で以下のように書けるのにと思っていましたが、
SELECT "books".* FROM "books" WHERE "books"."id" IN ( SELECT "rentals"."book_id" FROM "rentals" GROUP BY "rentals"."book_id" HAVING COUNT(*) = 1 ) ;
pluck
を select
に変えるだけで、上記の SQL が実行され、1度のみ実行を満たすことができます!
book_ids = Book.where(id: Rental.having("count(*) = 1").group(:book_id).select(:book_id))
ActiveRecord::Relation
のオブジェクトは実際に中身が必要になるまでは DB アクセスしない(遅延評価)ため、pluck
だと Array
で取得する必要があるから即時に実行されるが、select
ならばその時点での実行を待ってくれてこのような SQL を組み立ててくれるのだと思います。
終わりに
where
に ActiveRecord::Relation
を渡すことができることを知らなかったです。
ただ複雑な SQL だったりすると、1度にまとめることが必ずしもパフォーマンス向上につながらないこともあるので、どちらが有効かを比較して活かしていきたいなと思いました。
インゲージではエンジニアを募集中です!