この記事は クラウドワークス グループ Advent Calendar 2025 シリーズ1の12日目の記事です。
はじめに
こんにちは、クラウドソーシングサービス「クラウドワークス(crowdworks.jp)」でエンジニアをしている原です。
先日業務でRailsコンソールを利用してクエリのチューニングを行っていた際、ActiveRecord::Relation#explainメソッドの挙動に関する認識不足によりサーバーのリソースを枯渇させてしまったということがありました。
「実行計画(EXPLAIN)を確認するだけなら実際のデータ取得は行われない」と考えている方も多いかと思いますが、Railsの特定の条件下では通常のSELECT文が発行される場合があります。
障害発生時の状況
本番環境の管理用サーバーにて、重いクエリの調査のためにRailsコンソールを使用していました。
その際、クエリの実行計画を確認する目的でActiveRecord::Relation#explainを実行したところ、サーバーのメモリ使用量が急増し、一時的に接続できない状態となりました。
実行したコードのイメージ
実際にはより複雑な条件でしたが、実行したのは以下のようなコードでした。多数のpreloadを含むクエリにexplainをつけたものです。
# 実際はさらに多くのpreloadやjoinが含まれていました
# ※説明のため、テーブル名などは一般的な例に置き換えています
User
.preload(:orders, :reviews, :shipping_addresses)
.preload(:wishlists, :point_histories)
.active
.where(role: 'premium_member')
.explain # <--- 実行計画を確認するつもりで実行
影響
結果、当該管理用サーバーのメモリ使用率が高騰し、SSH接続やコンソール操作が応答しない状態となりました。
幸い、サービス提供用のサーバーとは分離された管理用インスタンスであったことから、ユーザーへの影響はありませんでした。
原因:explain実行時の内部挙動
原因は、ActiveRecord::Relation#explainメソッドを実行した際、対象となるテーブルへのSELECT文(実データの取得)が先行して実行されていたことにあります。
対象のクエリがusersテーブルをフルスキャンする重いものであり、かつpreloadによって大量の関連データも読み込まれる設定だったため、それらがすべてメモリ上に展開され、リソース枯渇を招きました。
対象のクエリは普段からバッチで定期実行されているもので、重いとはいえ特別スロークエリとなっているわけではありませんでした。しかし、管理用サーバーのメモリがサービス提供用のサーバーに比べて少なかったため、負荷に耐えきれなかったようです。
なぜSELECTが実行されたのか?
一般的なSQLのEXPLAIN文はクエリの解析のみを行いますが、Railsのコンソール上でexplainを実行した場合、以下のプロセスを経てSELECTが実行される仕様となっています。
1. IRBの表示仕様(Inspect Mode)
IRBはデフォルトで、コマンドの実行結果を画面に表示するために、戻り値のオブジェクトに対してinspectメソッドを呼び出すようになっています。
2. ExplainProxyのinspect
ActiveRecord::Relation#explainメソッドの戻り値は文字列ではなく、ActiveRecord内部のExplainProxyオブジェクトです。
このオブジェクトのinspectメソッドが呼び出されると、クエリを実行して実行計画を収集・整形する処理がトリガーされます。
3. クエリの先行実行
ExplainProxyオブジェクトのinspectメソッドは、EXPLAINコマンドを発行する前に通常のクエリを実行する実装になっています。これは、実際のクエリを使用することで正確な実行計画を取得するためと考えられます。
つまり、今回の件は以下の流れで起きました。
- コンソールで
User...explainを実行 - IRBが結果を表示しようとして
inspectを呼ぶ -
inspect内で実データのSELECTが走る - 大量のデータがメモリに乗る → メモリ枯渇
より安全に実行計画を取得する方法
explainメソッドを使わなくても同じように実行計画を取得することはできたので、一応やり方を書いておきます。
-
ActiveRecord::Relation#to_sqlを使用してSQLを生成するか、もしくはローカル環境で実際にクエリを一度実行する - 生成したSQLの先頭にEXPLAINをつける
- 2を
ActiveRecord::Base.connection.select_allの引数としてRailsコンソールで実行する- SQLが実行できる安全な分析環境などがある場合は、2をそちらで実行するとよいと思います
sql = User.preload(...).where(...).to_sql
puts ActiveRecord::Base.connection.select_all("EXPLAIN #{sql}").to_a
自分が試した限りでは、この方法だとSELECT文は発行されず、EXPLAIN文と実行計画のみが返ってきました。
注意点として、to_sqlで生成されるのはメインのクエリのみです。preloadによって発行される関連テーブルへのクエリの実行計画は含まれませんが、「まずは重いメインクエリを解析したい」という場合には使えると思います。
※2025/12/18 追記:@t0yohei さんより、to_sqlの代わりにテストのログを活用する方法を教えていただきました。テスト実行時のログを標準出力に表示させることで、実際にアプリケーションから発行されたSQLを取得することができます。
context 'hoge' do
it do
ActiveRecord::Base.logger = Logger.new(STDOUT) # ログを標準出力に表示させる
subject
expect(fuga).to have_received(:piyo)
end
end
まとめ
- Railsの
explainメソッドは、コンソールでの表示時に実データをSELECTする場合がある。 - そのため大量データを扱うクエリをメモリの少ないサーバー等で
explainすると障害につながるおそれがある。