こんにちは。サーバーエンジニアの佐藤太一(@teach_kaiju)です。
本記事では、クエリキャッシュのメモリ使用量と有効/無効の切り替え方法について紹介します。
クエリキャッシュとは
Active Recordのクエリキャッシュは、1つのリクエストまたはジョブの実行中に同じSQLクエリが複数回実行された場合、2回目以降のクエリの実行を省略し、最初の結果をメモリ上にキャッシュして再利用する機能です。
# 1回目のクエリ実行時 Book.first # Book Load (2.9ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # 2回目のクエリ実行時 Book.first # CACHE Book Load (0.1ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
※ 本記事で用いるモデル名(Book)は執筆にあたって差し替えたものであり、実際に使用したモデル名とは異なります。
キャッシュが使用される場合には発行されたクエリのログの先頭にCACHE
と付いています。
2024/11 時点で、Sidekiq および SolidQueue ではクエリキャッシュがデフォルトで有効になっていること、 Rails コンソールでは無効になっていることを確認しました。
クエリキャッシュのメモリ消費量
クエリキャッシュはその性質上、大量のレコードを扱うジョブではメモリ使用量が膨大になりえます。
では、クエリキャッシュで実際どの程度メモリが圧迫されるのでしょうか? memory_profiler を用いて計測しました。 ※ モデル名は実際のものから差し替えています。
計測用コード
# 渡されたブロック内のメモリ消費および時間を出力 def report(&block) start_time = Time.current result = MemoryProfiler.report(&block) elapsed_time = Time.current - start_time puts "\n\n===== Profiler Report =====" puts "Total allocated: #{bytes_to_mb(result.total_allocated_memsize)} MB (#{result.total_allocated} objects)" puts "Total retained: #{bytes_to_mb(result.total_retained_memsize)} MB (#{result.total_retained} objects)" puts "Elapsed time: #{elapsed_time.round(2)} sec" puts "Query cache: #{Book.connection.query_cache.size} queries" end # bytes to MB 小数点第二位まで def bytes_to_mb(bytes) (bytes / 1024.0 / 1024.0).round(2) end
結果
データ数: 50 万
取得カラム: 数値と日付、合計 6 つ
実装: batch_size 1 万で上記のデータを取得する
※ find_each 等クエリキャッシュをスキップするメソッドは使用しません(後述)
クエリキャッシュ無効 | クエリキャッシュ有効 | |
---|---|---|
Total allocated | 281.03 MB (3510676 objects) | 272.02 MB (3512618 objects) |
Total retained | 3.83 MB (71 objects) | 99.24 MB (1500823 objects) Query cache: 56 queries |
Elapsed time | 17.46 sec | 20.83 sec |
考察
allocated の差は誤差です。クエリキャッシュの有効/無効でアロケーション数はそんなに変わらないでしょう。
retained (使用中のメモリ) はキャッシュ分大幅に増加しています。
状況によって大きく差が出るため参考程度ですが、 50 万のデータでおよそ 100MB 程度のメモリを確保することがわかりました。
時間は誤差かもしれませんが、クエリキャッシュが無効なほうが少し高速なようです。
もし batch_size が 1 万ではなく 1000 であれば実行するクエリの数は 500 を超えます。Rails 7.1 以上であればクエリキャッシュの数の制限 (default 100) を超えるため、その分 retained は大幅に減少するでしょう。
find_each 等ではクエリキャッシュが無効になる
find_each
、find_in_batches
そしてin_batches
ではクエリキャッシュが無効になります。
def batch_on_unloaded_relation(relation:, start:, finish:, load:, cursor:, order:, use_ranges:, remaining:, batch_limit:) ... relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
したがって、バッチ処理で上記メソッドを使用する分にはクエリキャッシュを気にする必要はあまりありません。 最適化のために上記メソッドを使わずにバッチ処理を行うときに気をつける必要があります。
クエリキャッシュを無効化する方法
ActiveRecordのモデル.uncached
を使うのがおすすめです。ドキュメント
Model.uncached do # この中ではクエリキャッシュが無効になる end
ActiveRecordのモデル.uncached
を使うと、リードレプリカ等の別DBを参照した場合でもクエリキャッシュを無効化することができます。
切り替え検証
※ Rails コンソールではキャッシュがデフォルト無効なため、無効 -> 有効の切り替えを行なっています
# 通常 (Rails コンソールのためキャッシュがデフォルト無効) Book.first Book.first # Book Load (2.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # Book Load (0.4ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # キャッシュを有効化 ActiveRecord::Base.cache do Book.first Book.first end # Book Load (2.9ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # CACHE Book Load (0.1ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # 別DBに接続 (キャッシュが有効にならない) ApplicationRecord.connected_to(role: :primary_replica) do ActiveRecord::Base.cache do Book.first Book.first end end # Book Load (3.9ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # Book Load (1.9ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # 別DBでキャッシュを有効化する ApplicationRecord.connected_to(role: :primary_replica) do Book.cache do Book.first Book.first end end # Book Load (2.8ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 # CACHE Book Load (0.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
本検証ではログにCACHE
と付くかどうかを見ていますが、以下でも確認可能です
Book.connection.query_cache_enabled
おわりに
本記事では ActiveRecord のクエリキャッシュについて紹介しました。メモリ使用量が気になる方は、ぜひクエリキャッシュの無効化を検討してみてください。その際に本記事の内容が参考になれば幸いです。
参考文献
Sidekiq: Problems and Troubleshooting
ShakaCode: Rails 7.1 makes ActiveRecord query cache an LRU
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら medpeer.co.jp
■エンジニア紹介ページはこちら engineer.medpeer.co.jp
■メドピア公式YouTube www.youtube.com
■メドピア公式note
style.medpeer.co.jp