初めに
こんにちは。ユニファのバックエンドエンジニア、シュウです。
最近、社内のRuby on Railsのソースコードを調査している中で、レコードの存在チェックによく使われている3つのメソッドを見ました。
- present?
- exists?
- any?
Ruby on Railsのエンジニアになってから、「存在チェックの際にはexists?
やany?
の方がパフォーマンスが優れている」という話をよく聞きますが、実際に調査してみると意外にも興味深い結果が得られました。この記事では、present?
、exists?
、any?
の3つのメソッドの挙動、パフォーマンス、および適切な使用場面についてまとめました。
結論から言うと
- 単純な存在チェックを行いたい場合は、
present?
よりexists?
とany?
を使う方が適しています。 - ブロックを使用したい場合は、
any?
を使います。 - 存在チェック後にレコードに対して何らかの操作を行いたい場合は、
present?
を使う方が良いです。
検証を始めよう
環境
- Ruby 3.1.4
- Ruby on Rails 7.0.6
- PostgreSQL 15.3
usersテーブルに20万件のレコードを作成しました。その中で、5万件のユーザの生年月日を2003/7/14
と設定しました。そして、2003/7/14
に生まれたユーザが存在するかどうかを確認したいと思います。
present?
よく見かけるコードを実行してみました。
User.where(birth_date: Date.new(2003, 7, 14)).present? User Load (59.7ms) SELECT "users".* FROM "users" WHERE "users"."birth_date" = $1 [["birth_date", "2003-07-14"]] => true
ふむ、59.7msかかりました。
exists?
User.where(birth_date: Date.new(2003, 7, 14)).exists? User Exists? (6.0ms) SELECT 1 AS one FROM "users" WHERE "users"."birth_date" = $1 LIMIT $2 [["birth_date", "2003-07-14"], ["LIMIT", 1]] => true
whereと同じ感覚の書き方もできます。実行されるSQLは同じです。
User.exists?(birth_date: Date.new(2003, 7, 14)) User Exists? (6.1ms) SELECT 1 AS one FROM "users" WHERE "users"."birth_date" = $1 LIMIT $2 [["birth_date", "2003-07-14"], ["LIMIT", 1]] => true
6.0msと6.1msはpresent?
よりもはるかに速いですね。その理由は、
present?
のSQLクエリには、条件に合うすべてのユーザを読み込んでから存在チェックを行います。exists?
のSQLクエリにはLIMIT 1
の条件が付いています。条件に合うユーザが1件でもあれば、trueを返します。
結論:単純な存在チェックを行いたい場合は、present?
よりexists?
を使う方が適しています。
any?
any?
メソッドには2つの利用方法があります。
ブロックなし
この場合、any?
はActiveRecord::Relation#any?
メソッドを指します。
User.where(birth_date: Date.new(2003, 7, 14)).method(:any?).inspect => "#<Method: User::ActiveRecord_Relation(ActiveRecord::Relation)#any?() /usr/local/bundle/gems/activerecord-7.0.6/lib/active_record/relation.rb:284>"
早速使ってみましょう。
User.where(birth_date: Date.new(2003, 7, 14)).any? User Exists? (6.0ms) SELECT 1 AS one FROM "users" WHERE "users"."birth_date" = $1 LIMIT $2 [["birth_date", "2003-07-14"], ["LIMIT", 1]] => true
発行されるSQLはexists?
と全く同じですね。パフォーマンスの差もほとんどありません。
(ActiveRecord 5.1以降、ブロックなしのany?
はexists?
と同じSQL文を生成するようになったようです。参考: Differences between `any?` and `exists?` in Ruby on Rails? - Stack Overflow)
結論:exists?
の代わりにany?
も使えそうです。
ブロックあり
ちなみに、any?
メソッドではブロックを渡すこともできます。実際にany?
メソッドのソースコードを見てみましょう。
rails/activerecord/lib/active_record/relation.rb at v7.0.6 · rails/rails · GitHub
# Returns true if there are any records. def any? return super if block_given? !empty? end
ソースコードにinclude Enumerable
しているため、ブロックが渡されると、superのEnumerable#any?
が実行されます。その場合、Rubyのコードが実行されます。
User.any?{ |user| user.birth_date == Date.new(2003, 7, 14) } User Load (118.5ms) SELECT "users".* FROM "users" => true
実行にかかる時間は118.5msですが、DBクエリが完了した後Rubyのコードが実行されるため、もう少し時間がかかります。これはpresent?
よりも遅いですね。
ただし、exists?
/ present?
と比較するとany?
の利点はブロックが使えることです。存在チェックのロジックが複雑になった場合、ブロックを使うと記述しやすく、読みやすくなることがあります。そのため、any?{ ... }
の使用は珍しくないです。
結論:ブロックを使用したい場合は、any?
を使います。
present?
はどの場面を使う?
ここまで読んでいただいた後で、「present?
は不要ではないか?」と思うかもしれませんが、実際には多くの場面で役立ちます。
存在チェックだけでなく、連続した存在チェックや、その後の各レコードを操作する必要がある場合にpresent?
を使います。以下の例を見てみてください。
present?
# 行末に「; nil」を追加するのはconsoleからのstdout出力とレコードのロードを抑制するためです。 irb(main):001:0> users = User.where(birth_date: Date.new(2003, 7, 14)); nil => nil irb(main):002:0> users.present? User Load (63.1ms) SELECT "users".* FROM "users" WHERE "users"."birth_date" = $1 [["birth_date", "2003-07-14"]] => true irb(main):003:0> users.present? => true irb(main):004:0> users.each{}; nil => nil
SQLは1回だけ発行されました。
exists?
次のexists?
を使うコードを実行したら、逆に3回のSQLが発行されます。(以下の例ではany?
の動作はexists?
と同じです。)
irb(main):001:0> users = User.where(birth_date: Date.new(2003, 7, 14)); nil => nil irb(main):002:0> users.exists? User Exists? (12.0ms) SELECT 1 AS one FROM "users" WHERE "users"."birth_date" = $1 LIMIT $2 [["birth_date", "2003-07-14"], ["LIMIT", 1]] => true irb(main):003:0> users.exists? User Exists? (6.0ms) SELECT 1 AS one FROM "users" WHERE "users"."birth_date" = $1 LIMIT $2 [["birth_date", "2003-07-14"], ["LIMIT", 1]] => true irb(main):004:0> users.each{}; nil User Load (80.5ms) SELECT "users".* FROM "users" WHERE "users"."birth_date" = $1 [["birth_date", "2003-07-14"]] => nil
結論:存在チェック後にレコードに対して何らかの操作を行いたい場合は、present?
を使う方が良いです。
まとめ
各メソッドの適切な使用場面をまとめました。
メソッド | 単純に一回の存在チェック | レコードに対する別の操作が必要 | ブロックを使いたい |
---|---|---|---|
present? |
○ | ||
exists? |
○ | ||
any? |
○ | ○ |
補足:any?
とexists?
は少し違いがあります。レコードがロード済みの場合、any?
は新しいクエリを投げません。でもexists?
は毎回クエリを投げます。そしてany?
とexists?
のクエリ結果はどちらでも記憶されないです。
ユニファでは、ともに保育の課題解決と向き合う仲間を募集しています。 unifa-e.com