ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Ruby on Railsのexists? / any? / present?

初めに

こんにちは。ユニファのバックエンドエンジニア、シュウです。

最近、社内の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