🍼
【夢】ActiveRecordの遅延評価について
遅延評価について改めて確認する機会があったので、メモしておきます。
遅延評価とは??
実際にデータが必要になるタイミングで SQL クエリが生成され、データが取得されることを遅延評価(Lazy Evaluation)
といいます。
実際に見てみる
どのタイミングでSQL発行されているのか見てみます。
実験
Rails.logger.debug("いち")
users = User.where(created_at: Time.zone.parse("2023-10-01 00:00:00")..Time.zone.parse("2025-5-22 23:59:59"))
Rails.logger.debug("に")
Rails.logger.debug("users: #{users}")
Rails.logger.debug("さん")
Rails.logger.debug(users.first)
Rails.logger.debug("よん")
log
いち
に
users: #<User::ActiveRecord_Relation:0x0000ffff99d75980>
さん
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."created_at" BETWEEN $1 AND $2 ORDER BY "users"."id" ASC LIMIT $3 [["created_at", "2023-10-01 07:00:00"], ["created_at", "2025-05-23 06:59:59"], ["LIMIT", 1]]
#<User id: 1, email: [FILTERED], created_at: "2025-04-21 20:46:17.078294000 -0700", updated_at: "2025-04-21 20:46:17.078294000 -0700">
よん
この結果から分かる通り、、、
- 実際に具体的データが必要になった
users.first
のタイミングでSQL発行される - whereしたら、実際には
ActiveRecord::Relation
が返る- なんとなくSQL発行しそうですが、実際にはしていない!
これが遅延評価です。素敵な機能ですね。
SQLクエリ発行のタイミングとは??
実際にデータが必要になったタイミングとは、、、
ActiveRecordの中身を参照しようとした時!(データないと困るからね、、)
users.first
user.name
-
user.to_a
(配列化) users.each do |user| ....
User.exists?(name: "cinnamon")
-
User.count
などなど、、、
ActiveRecord::Relation
どういうSQLを組み立てるのかという情報を持っており、データが必要になったタイミングでいい感じにデータ取得してくれるもの。。と思っています。。
自分でうまく言語化できなかったので、ChatGPTに聞きました。
まだ実行されていないSQLの設計図(クエリのレシピ)
実際のデータではなく、「どんなデータを取りに行くか」の準備状態のオブジェクト
そして、そのRelationオブジェクトは…
✅ 必要になったときに、実際にSQLを発行してデータを取ってくれる!
(ChatGPTって絵文字使いがちですよね、、✅ )
N+1問題...
某有名はN+1問題。
ループ処理内で関連オブジェクトにアクセスしようとすると、、そこでSQL発行されてしまい、ループの数だけSQL発行されてしまい、パフォーマンス悪化につながってしまいます。
実験
Rails.logger.debug("いち")
users = User.where(created_at: Time.zone.parse("2025-5-01 00:00:00")..Time.zone.parse("2025-5-22 23:59:59"))
Rails.logger.debug("に")
users.each do |user| <- ここでuserが必要だ!となって、SQL発行される
Rails.logger.debug("さん")
Rails.logger.debug("user.daily_access: #{user.daily_accesses.first}") <- ここでdaily_accessが必要だ!となって、SQL発行される ✕ n回、、
Rails.logger.debug("よん")
end
log
いち
に
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."created_at" BETWEEN $1 AND $2 [["created_at", "2025-05-01 07:00:00"], ["created_at", "2025-05-23 06:59:59"]]
さん
UserDailyAccess Load (0.2ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 22], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 27], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 28], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 29], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 30], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 32], ["LIMIT", 1]]
user.daily_access:
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 31], ["LIMIT", 1]]
user.daily_access: #<UserDailyAccess:0x0000ffff86582790>
よん
さん
UserDailyAccess Load (0.1ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" = $1 ORDER BY "user_daily_accesses"."id" ASC LIMIT $2 [["user_id", 33], ["LIMIT", 1]]
user.daily_access:
よん
includes
をつけることで、関連データも一括で取得されるよう最適化されます!
実験
Rails.logger.debug("いち")
users = User.includes(:daily_accesses).where(created_at: Time.zone.parse("2025-5-01 00:00:00")..Time.zone.parse("2025-5-22 23:59:59"))
Rails.logger.debug("に")
users.each do |user| <- ここでuser取得のSQL発行されるのは変わりないですが、関連データも一括で取得する
Rails.logger.debug("さん")
Rails.logger.debug("user.daily_access: #{user.daily_accesses.first}")
Rails.logger.debug("よん")
end
log
いち
に
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."created_at" BETWEEN $1 AND $2 [["created_at", "2025-05-01 07:00:00"], ["created_at", "2025-05-23 06:59:59"]]
UserDailyAccess Load (0.4ms) SELECT "user_daily_accesses".* FROM "user_daily_accesses" WHERE "user_daily_accesses"."user_id" IN ($1, $2, $3, $4, $5, $6, $7, $8) [["user_id", 22], ["user_id", 27], ["user_id", 28], ["user_id", 29], ["user_id", 30], ["user_id", 32], ["user_id", 31], ["user_id", 33]]
さん
user.daily_access:
よん
さん
user.daily_access:
よん
さん
user.daily_access:
よん
さん
user.daily_access:
よん
さん
user.daily_access:
よん
さん
user.daily_access:
よん
さん
user.daily_access: #<UserDailyAccess:0x0000ffff6bf5c290>
よん
さん
user.daily_access:
よん
まとめ
- あまり意識してないけどありがたい機能ですね〜
- 意識しなさすぎて意図しないところで、データ取得されないようにしなくては
Discussion