ActiveRecordの裏側でどのようなSQLが発行されるのかを確認する方法がわからなかったので、備忘録としてまとめます。
バージョン
- Ruby 3.0.3
- Rails 6.1.7.7
記事の信頼性
- ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
- 実務ではVue.jsとRailsを毎日書いています。
- 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
問題
仕事でスロークエリ改善のタスクを任されていたため、ActiveRecordの裏でどのようなSQLが発行されているかを調べたかったです。
しかしどのようにやれば良いのかが分かりませんでした、、。
もちろんログを愚直に追えば確認はできるんでしょうが、それだと効率が悪いので、「何か便利なメソッドがないかな」と探していました。
解決方法
RailsのActiveRecord::Relation
クラスにはto_sql
というメソッドが存在しています。
これを用いることで、ActiveRecordの裏側で発行されるSQLを個別に確認することが可能です。
たとえば、以下の記述でどのようなSQLが発行されるか、知りたかったとします。
# task_ids はTaskモデルのIDの配列
Task.where(id: task_ids).eager_load(:category, :user)
その場合、Railsコンソール上で次のように書いて実行してみれば、発行されるSQLを確認できます。
# task_idsの位置には適当な配列(1..5)を入れています
[1] pry(main)> Task.where(id: (1..5).to_a).eager_load(:category, :user).to_sql
"SELECT `tasks`.`id` AS t0_r0, `tasks`.`category_id` AS t0_r1, `tasks`.`user_id` AS t0_r2, `tasks`.`title` AS t0_r3, `tasks`.`description` AS t0_r4, `tasks`.`deadline` AS t0_5, `tasks`.`status` AS t0_r6, `tasks`.`created_at` AS t0_r7, `tasks`.`updated_at` AS t0_r8, `task_categories`.`id` AS t1_r0, `task_categories`.`name` AS t1_r1, `task_categories`.`created_at` AS t1_r2, `task_categories`.`updated_at` AS t1_r3, `users`.`id` AS t2_r0, `users`.`company_id` AS t2_r1, `users`.`role` AS t2_r2, users`.`status` AS t2_r3, `users`.`created_at` AS t2_r4, `users`.`updated_at` AS t2_r5, FROM `tasks` LEFT OUTER JOIN `users` ON `users`.`id` = `tasks`.`user_id` LEFT OUTER JOIN `task_categories` ON `task_categories`.`id` = `tasks`.`category_id` WHERE `tasks`.`id` IN (1, 2, 3, 4, 5)"
`tasks`
の``
が見づらく感じる場合は、gsub
コマンドを併用すれば消せます。
# gsub コマンドで ` を空文字に置き換える
[2] pry(main)> Task.where(id: (1..5).to_a).eager_load(:category, :user).to_sql.gsub('`', '')
=> "SELECT tasks.id AS t0_r0, tasks.category_id AS t0_r1, tasks.user_id AS t0_r2, tasks.title AS t0_r3, tasks.description AS t0_r4, tasks.deadline AS t0_5, tasks.status AS t0_r6, tasks.created_at AS t0_r7, tasks.updated_at AS t0_r8, task_categories.id AS t1_r0, task_categories.name AS t1_r1, task_categories.created_at AS t1_r2, task_categories.updated_at AS t1_r3, users.id AS t2_r0, users.company_id AS t2_r1, users.role AS t2_r2, users.status AS t2_r3, users.created_at AS t2_r4, users.updated_at AS t2_r5, FROM tasks LEFT OUTER JOIN users ON users.id = tasks.user_id LEFT OUTER JOIN task_categories ON task_categories.id = tasks.category_id WHERE tasks.id IN (1, 2, 3, 4, 5)"
注意点
このto_sql
メソッドはとても便利なんですが、ActiveRecordのオブジェクトなら何にでも使える訳ではありません。
例として、次の実行結果を見てください。
[1] pry(main)> User.all.to_sql
=> "SELECT `users`.* FROM `users`"
[2] pry(main)> User.last.to_sql
User Load (4.4ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
NoMethodError: undefined method `to_sql' for #<User::IndividualUser id: 59, created_at: "2024-04-04 11:54:26.000000000 +0900", updated_at: "2024-04-04 11:54:26.000000000 +0900", role: "employee", identity_verified_flag: false, postpayment_flag: 0, registered_flag: false, nda_flag: false, chat_status: "", config: "", partnership_agreement_agreed_flag: false, company_id: nil, _note: "", user_auth_id: 59, inactivated_flag: false, resigned_at: nil, type: "User::IndividualUser", corporation_id: nil>
Did you mean? to_s
from /usr/local/bundle/gems/activemodel-6.1.7.7/lib/active_model/attribute_methods.rb:469:in `method_missing'
User.all
に対してはto_sql
メソッドの実行が成功しているのに対し、User.last
に対しては失敗しています(この場合でも発行されるSQLは見えますが)
これはUser.all
が返すオブジェクトのクラスとUser.last
が返すオブジェクトのクラスが異なることが原因です。
[3] pry(main)> User.all.class.name
=> "ActiveRecord::Relation"
[4] pry(main)> User.last.class.name
User Load (8.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
=> "User"
User.all
がActiveRecord::Relation
クラスのオブジェクトを返す一方で、User.last
はUser
クラスのオブジェクトを返します。
to_sql
メソッドはActiveRecord::Relation
クラスに定義されているため、User.last
に使っても失敗してしまうんです。
この点に注意しておきましょう。
おわりに
この記事ではActiveRecordの裏側で発行されるSQLの調べ方を解説しました。
パフォーマンス改善のタスクをしているときにto_sql
メソッドはとても役立ちます。しっかりマスターしておきましょう。
また、以下の記事ではワンランク上のRailsエンジニアになりたいと考えている方向けにおすすめの技術書を紹介しています。
こちらの記事もぜひ読んでみてください。
コメント