LoginSignup
0

More than 3 years have passed since last update.

なぜany?はpresent?より高速と言われるのか

Posted at

はじめに

「present?メソッドとany?メソッドだったらany?メソッドが高速なのか・・・なぜだろう?」
この疑問を解決すべく、present?メソッドとany?メソッドの処理の違いについて調べてみました。

環境

macOS Catalina Version 10.15.4
Ruby: 2.7.0
Ruby on Rails: 6.0.2.2

present?メソッド、any?メソッドの違い

どちらもテーブルにデータが存在するかを確認するメソッドです。
けれども内部処理はどちらも異なっています。

侍エンジニアブログさんの、any?メソッド記事の一部を抜粋します。

present?メソッドとany?メソッドとの違いについて紹介します。
結論からいうとany?メソッドの方が高速です。
present? → 全てのデータを取得する
any? → 1件のみデータを取得する
Railsが実行する
SQLを比較して違いを確認してみましょう。

> Sample.where(name:"侍1").present?
Sample Load (0.4ms)  SELECT `samples`.* FROM `samples` WHERE `samples`.`name` = '侍1'
=> true
> Sample.where(name:"侍1").any?
Sample Exists (0.4ms)  SELECT  1 AS one FROM `samples` WHERE `samples`.`name` = '侍1' LIMIT 1
=> true

このように、SQLの最後にLIMIT 1 が付与されています。

any?メソッドがpresent?よりも優れているように見えます。
ではなぜこのような処理になっているのか、Ruby on Railsのコードを追ってみました。

なお、modelは侍エンジニアブログさんと同様のSampleモデル(stringのname属性のみ)を作成しています。

present?メソッドの内部処理

present?メソッド実行時のコードの場所はblank.rbとなっています。
このコードを追ってみます。

>Sample.where(name: "侍1").method(:present?).source_location
=> ["/Users/xxxxx/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/activesupport-6.0.2.2/lib/active_support/core_ext/object/blank.rb", 25]
blank.rb
def present?
  !blank?
end
relation.rb
alias :loaded? :loaded

def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
  @klass  = klass
  @table  = table
  @values = values
  @offsets = {}
  @loaded = false
  @predicate_builder = predicate_builder
  @delegate_to_klass = false
end

def records # :nodoc:
  load
  @records
end

def load(&block)
  unless loaded?
    @records = exec_queries(&block)
    @loaded = true
  end

  self
end

def blank?
  records.blank?
end

よってpresent?メソッドを呼び出した場合、クエリ未発行ならば、exec_queryメソッドでクエリを発行します。
このクエリは、指定されたテーブルの全レコード検索となります。
その後、結果に応じてblank?を行います。
もしクエリ発行済であれば、クエリを発行せずにblank?を行います。

any?の内部処理

any?メソッド実行時のコードの場所はrelation.rbとなっています。
このコードを追ってみます。

>Sample.where(name: "侍1").method(:any?).source_location
=> ["/Users/xxxxx/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/activerecord-6.0.2.2/lib/active_record/relation.rb", 277]
relation.rb
def empty?
  return @records.empty? if loaded?
  !exists?
end

def any?
  return super if block_given?
  !empty?
end

def load(&block)
  unless loaded?
    @records = exec_queries(&block)
    @loaded = true
  end

  self
end
finder_method.rb
def exists?(conditions = :none)
  if Base === conditions
    raise ArgumentError, <<-MSG.squish
      You are passing an instance of ActiveRecord::Base to `exists?`.
      Please pass the id of the object by calling `.id`.
    MSG
  end

  return false if !conditions || limit_value == 0

  if eager_loading?
    relation = apply_join_dependency(eager_loading: false)
    return relation.exists?(conditions)
  end

  relation = construct_relation_for_exists(conditions)

  skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists?") } ? true : false
end

よってany?メソッドを呼び出した場合、毎回exists?メソッドにて、テーブルにレコードが1件あるかどうかクエリを発行します。
その後、結果を確認しています。
もしload変数がtrueになった場合は、クエリ発行せずに結果を確認するのみとなります。

まとめ

最初に記載した疑問の答えです。

any?メソッドがpresent?よりも優れているように見えます。
ではなぜこのような処理になっているのか、Ruby on Railsのコードを追ってみました。

present?メソッドの場合、初回のみ全レコードを取得するクエリを発行して判定します。
2回目以降の呼び出しはクエリを発行せずに判定します。

any?メソッドの場合、基本的に毎回、テーブルから1件レコードを取得するクエリを発行して判定します。

このため、全レコードを取り出さないany?メソッドが高速と言われているだと思われます。

ご指摘等あればコメントにご記載をお願い致します。

参考記事

【Rails入門】any?メソッドの便利な使い方を紹介
RailsのActiveRecord::FinderMethodsのSQLクエリ発行の有無について調べる
ActiveRecord::QueryMethodsのselectメソッドについて深掘りしてみた
週刊Railsウォッチ(20191216前編)Rails 6.0.2がリリース、平成Ruby会議01開催、古いRailsのfindメソッド置き換えほか
RailsのArelを調査してみた

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