🟥

ActiveRecord::Relation におけるレコードの存在確認、個数確認を行うメソッドを比較する

2024/08/09に公開

Rails で使用される ActiveRecord::Relation にはレコードの存在確認や個数確認を行うためのメソッドが複数存在しています。
普段の実装で気にすることは少ないかもしれないですが、クエリの発行されるタイミングと内容が少しずつ異なるため違いをまとめてみました。

Rails のバージョンは 7.1.3.2 を前提としています。

比較対象のメソッド

存在確認メソッド

  • exists?
  • blank?
  • empty?

個数確認メソッド

  • count
  • length
  • size

メソッドの比較

User モデルに対して where で条件を指定した relation を用意します。

relation = User.where(id: 1)

その後、対象のメソッドそれぞれを relation に対して実行した場合と、 relation.load で Active Record オブジェクトをメモリに読み込んだ後実行した場合で発行されるクエリがどうなるかを見てみます。

存在確認メソッド

未 load 時

> relation.exists?
  User Exists? (2.6ms)  SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> true
> relation.blank?
  User Load (4.3ms)  SELECT `users`.`id`, `users`.`name`, `users`.`created_at`, `users`.`updated_at` FROM `users` WHERE `users`.`id` = 1
=> false
> relation.empty?
  User Exists? (2.7ms)  SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> false

load 後

> relation.exists?
  User Exists? (2.6ms)  SELECT 1 AS one FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> true
> relation.blank?
=> false
> relation.empty?
=> false

exists?SELECT 1 ... とあるように存在確認のみにフォーカスした軽量なクエリを発行しています。load 状態にかかわらずクエリを発行しています。
発行されるクエリは軽量ですが、load 済みの場合は不要なクエリを発行してしまうことにもなりそうです。

blank? は内部で load を実行し対象のオブジェクトを一度メモリに読み込みます。読み込み後はクエリを発行しません。
eachto_a などの load を前提としたメソッドと同様の動きになっていそうです。
別の処理ですでに load している場合はそれを使用するためクエリの発行を抑えられますが、load しておらず存在確認のみを必要とする場合 exists? と比べてメモリ効率は悪くなりそうです。

empty? は load 前に実行すると exists? と同様に軽量なクエリを発行しています。ただし load 後はクエリを発行しません。
exists?blank? のちょうど間をとったような動きをしてくれそうです。 empty? の実装は以下のようになっており load 済みであればそれを使用し、未 load 時であれば exists? の動きをすることがわかります。

https://github.com/rails/rails/blob/v7.1.3.2/activerecord/lib/active_record/relation.rb#L282-L291

ただ load 前に empty? のみを連続で実行しても load は一向にされずクエリを発行し続けるため、 load 前に連続で使用する場合は blank? のほうがパフォーマンスが良い場面もありそうです。

個数確認メソッド

未 load 時

> relation.count
  User Count (3.6ms)  SELECT COUNT(*) FROM `users` WHERE `users`.`id` = 1
=> 1
> relation.length
User Load (2.9ms)  SELECT `users`.`id`, `users`.`name`, `users`.`created_at`, `users`.`updated_at` FROM `users` WHERE `users`.`id` = 1
=> 1
> relation.size
User Count (4.5ms)  SELECT COUNT(*) FROM `users` WHERE `users`.`id` = 1
=> 1

load 後

> relation.count
  User Count (3.6ms)  SELECT COUNT(*) FROM `users` WHERE `users`.`id` = 1
=> 1
> relation.length
=> 1
> relation.size
=> 1

countSELECT COUNT(*) ... とあるように個数確認のみにフォーカスした軽量なクエリを発行しています。 load の状態にかかわらずクエリを発行しています。

length は内部で load を実行し対象のオブジェクトを一度メモリに読み込みます。読み込み後はクエリを発行しません。

size は load 前に実行すると count と同様に軽量なクエリを発行しています。ただし load 後はクエリを発行しません。

すでにお気付きかもしれませんが、count length size の特徴がそれぞれ、存在確認メソッドの exists? blank? empty? の特徴と同様のものになっています。

size の実装は以下のようになっており、load 済みであればそれを使用し、未 load 時であれば count の動きをすることがわかります。

https://github.com/rails/rails/blob/v7.1.3.2/activerecord/lib/active_record/relation.rb#L273-L280

まとめ

簡単な表にしました。

存在確認メソッド(存在時に true

メソッド 発行クエリ 発行タイミング
exists? SELECT 1 毎回
present? SELECT * 未 load 時
any? SELECT 1 未 load 時

存在確認メソッド(存在時に false

メソッド 発行クエリ 発行タイミング
!exists? SELECT 1 毎回
blank? SELECT * 未 load 時
empty? SELECT 1 未 load 時

個数確認メソッド

メソッド 発行クエリ 発行タイミング
count SELECT COUNT(*) 毎回
length SELECT * 未 load 時
size SELECT COUNT(*) 未 load 時
SocialPLUS Tech Blog

Discussion