ActiveRecord::Relation におけるレコードの存在確認、個数確認を行うメソッドを比較する
Rails で使用される ActiveRecord::Relation にはレコードの存在確認や個数確認を行うためのメソッドが複数存在しています。
普段の実装で気にすることは少ないかもしれないですが、クエリの発行されるタイミングと内容が少しずつ異なるため違いをまとめてみました。
Rails のバージョンは 7.1.3.2 を前提としています。
比較対象のメソッド
存在確認メソッド
exists?blank?empty?
個数確認メソッド
countlengthsize
メソッドの比較
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 を実行し対象のオブジェクトを一度メモリに読み込みます。読み込み後はクエリを発行しません。
each や to_a などの load を前提としたメソッドと同様の動きになっていそうです。
別の処理ですでに load している場合はそれを使用するためクエリの発行を抑えられますが、load しておらず存在確認のみを必要とする場合 exists? と比べてメモリ効率は悪くなりそうです。
empty? は load 前に実行すると exists? と同様に軽量なクエリを発行しています。ただし load 後はクエリを発行しません。
exists? と blank? のちょうど間をとったような動きをしてくれそうです。 empty? の実装は以下のようになっており load 済みであればそれを使用し、未 load 時であれば exists? の動きをすることがわかります。
ただ 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
count は SELECT COUNT(*) ... とあるように個数確認のみにフォーカスした軽量なクエリを発行しています。 load の状態にかかわらずクエリを発行しています。
length は内部で load を実行し対象のオブジェクトを一度メモリに読み込みます。読み込み後はクエリを発行しません。
size は load 前に実行すると count と同様に軽量なクエリを発行しています。ただし load 後はクエリを発行しません。
すでにお気付きかもしれませんが、count length size の特徴がそれぞれ、存在確認メソッドの exists? blank? empty? の特徴と同様のものになっています。
size の実装は以下のようになっており、load 済みであればそれを使用し、未 load 時であれば count の動きをすることがわかります。
まとめ
簡単な表にしました。
存在確認メソッド(存在時に 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 時 |
Discussion