ActiveRecordはいつSQLを発行する?遅延評価をログで確認してみる

アイキャッチ画像

はじめに

RailsでActiveRecordを使っていると、普段はSQLを直接書かなくてもデータベースを操作できます。

例えば、次のようなコードです。

users = User.where(active: true)

SQLにすると、次のような処理をイメージすると思います。

SELECT *
FROM users
WHERE active = true;

では、User.where(active: true) が実行された瞬間に、このSQLもデータベースへ送信されているのでしょうか。

実は、多くの場合そうではありません。

ActiveRecordには遅延評価という仕組みがあり、where などで検索条件を組み立てただけではSQLを実行せず、実際にデータが必要になったタイミングでSQLを発行します。

今回はRailsのログを見ながら、

「ActiveRecordはいつSQLを発行するのか?」

を確認していきます。


whereを書いただけではSQLは発行されない

まずは次のコードを考えます。

users = User.where(active: true)

where を見ると、すぐにSELECT文が実行されそうに見えます。

しかし、この時点で作られているのは基本的に

ActiveRecord::Relation

というオブジェクトです。

つまり、

active = true のUserを取得する

という検索条件を持った状態が作られているだけです。

まだ検索結果そのものを取得する必要がなければ、データベースへ問い合わせる必要はありません。

例えば、

users = User.where(active: true)
users = users.order(created_at: :desc)
users = users.limit(10)

と書いても、それぞれの行でSQLが実行されるわけではありません。

ActiveRecordは条件を組み合わせ、

SELECT "users".*
FROM "users"
WHERE "users"."active" = TRUE
ORDER BY "users"."created_at" DESC
LIMIT 10

のようなSQLを実行する準備をします。

where、order、limit などをつなげられるのも、途中で結果を取得するのではなく、ActiveRecord::Relation として条件を組み立てているためです。


eachした瞬間にSQLが発行される

では、いつSQLが実行されるのでしょうか。

例えば次のコードです。

users = User.where(active: true)

users.each do |user|
  puts user.name
end

where の時点では結果を使っていません。

しかし、

users.each

では、実際のUserデータが必要になります。

そのため、このタイミングでSQLが発行されます。

Railsのログを見ると、次のようなSQLを確認できます。

User Load (1.2ms)
SELECT "users".*
FROM "users"
WHERE "users"."active" = TRUE

つまり処理の流れは、

User.where
↓
検索条件を作る
↓
ActiveRecord::Relation
↓
まだSQLは実行しない
↓
each
↓
実際のデータが必要になる
↓
SELECTを実行

というイメージです。


to_aでもSQLが発行される

to_a も分かりやすい例です。

users = User.where(active: true)
users.to_a

to_a はRelationの検索結果を配列として扱うため、実際のレコードが必要になります。

そのため、

users.to_a

のタイミングでSELECTが実行されます。

ActiveRecordでは、

where
order
limit
select
joins

などでRelationを組み立て、

to_a
each
map

などで実際の結果を利用するときにSQLが発行される、と考えると理解しやすいです。


firstやfind_byはその場でSQLを発行する

すべてのActiveRecordメソッドがRelationを返すわけではありません。

例えば、

user = User.first

では、最初の1件という実際のレコードが必要です。

そのため、その場でSQLが発行されます。

SELECT "users".*
FROM "users"
ORDER BY "users"."id" ASC
LIMIT 1

find_by も同様です。

user = User.find_by(email: "test@example.com")

これはRelationではなくUserオブジェクト、または nil を返すため、検索をその場で実行します。

大まかに整理すると、

where
order
limit
joins
↓
Relationを返す
↓
SQLはまだ実行しない

first
find
find_by
take
↓
実際のレコードを返す
↓
SQLを実行する

という違いがあります。


pluckやexists?もSQLを発行する

少し注意したいのが、レコードそのものを取得しないメソッドです。

例えば、

User.where(active: true).pluck(:id)

では、

SELECT "users"."id"
FROM "users"
WHERE "users"."active" = TRUE

のようなSQLがその場で実行されます。

pluck はRelationをさらに組み立てるメソッドではなく、必要な値を取得して配列として返すためです。

同じように、

User.where(active: true).exists?

では、条件に一致するレコードが存在するか確認するためのSQLが発行されます。

User.where(active: true).count

も件数を取得する必要があるため、通常は COUNT のSQLが実行されます。

つまり、

「SELECT * を実行するかどうか」ではなく、「その時点でデータベースの結果が必要かどうか」

で考えると分かりやすくなります。


sizeとlengthでも挙動が違う

ActiveRecordでは、似ているメソッドでもSQLの発行方法が違うことがあります。

例えば、

users = User.where(active: true)

users.size

Relationがまだロードされていなければ、size は件数を取得するSQLを利用します。

SELECT COUNT(*)
FROM "users"
WHERE "users"."active" = TRUE

一方、

users.length

ではレコード自体をロードして、その配列の長さを取得します。

大量データを扱う場合、この違いによって処理時間やメモリ使用量が大きく変わることがあります。

単純にRubyコードだけを見るのではなく、

最終的にどのSQLが発行されているかを見ることが重要です。


Railsコンソールで確認するときは注意

遅延評価をRailsコンソールで確認するときには注意点があります。

例えば、

User.where(active: true)

と入力すると、SQLがすぐ実行されたように見える場合があります。

これはRailsコンソールが結果を画面に表示するためにRelationを inspect し、その結果としてレコードがロードされることがあるためです。

遅延評価を確認したい場合は、例えば次のようにします。

users = User.where(active: true); nil

これならRelationの中身を画面へ表示しません。

続いて、

users.loaded?

を確認します。

false であれば、まだレコードはロードされていません。

その後、

users.to_a

を実行すると、

User Load
SELECT "users".*
FROM "users"
WHERE "users"."active" = TRUE

とログが出て、

users.loaded?

も true になります。

遅延評価を確認するには、この方法が分かりやすいです。


to_sqlを使うと実行前のSQLを確認できる

SQLを実行せず、どのようなSQLになるのかだけ確認したい場合は to_sql が便利です。

users = User
  .where(active: true)
  .order(created_at: :desc)
  .limit(10)

puts users.to_sql

例えば次のようなSQLを確認できます。

SELECT "users".*
FROM "users"
WHERE "users"."active" = TRUE
ORDER BY "users"."created_at" DESC
LIMIT 10

to_sql はSQL文字列を確認するためのメソッドなので、このSELECT文自体をデータベースへ実行するわけではありません。

クエリを調査するときは、

to_sql
↓
どんなSQLになるか確認

development.log
↓
実際にいつSQLが実行されたか確認

と使い分けると便利です。


まとめ

ActiveRecordでは、コードを書いた瞬間にすべてのSQLが実行されるわけではありません。

User.where(active: true)

のようなコードでは、まず ActiveRecord::Relation として検索条件が組み立てられます。

そして、

each
to_a
map
first
find_by
pluck
exists?
count

など、実際にデータベースの結果が必要になったタイミングでSQLが発行されます。

この遅延評価を理解しておくと、

なぜここでSQLが発行されたのか
なぜ同じようなコードなのにSQLが違うのか
どの処理がデータベースアクセスを発生させているのか

を判断しやすくなります。

Railsのコードだけを見ていると、データベースアクセスが隠れて見えることがあります。

パフォーマンスを調べるときは、ActiveRecordのコードだけではなく、development.logに出ている実際のSQLまで確認することが重要です。