はじめに
キャッシュを使う実装をしたことが無く、使ってみたい&パフォーマンスの比較をしてみたい、、
ということでbenchmark-ip
というGemを使ってベンチマークテストを行ってみたので、手順と結果を残しておく。
検証内容
Railsアプリケーションにおいて、100万件レコードを持つテーブルからSELECTする際のパフォーマンスについて、以下4つのケースを比較する。
キャッシュの使用/不使用と、ついでに検索対象のカラムの組み合わせでのインデックス有無によるパフォーマンスも比較してみる。
インデックスなし | インデックスあり | |
---|---|---|
キャッシュ不使用 | 1 | 2 |
キャッシュ使用 | 3 | 4 |
benchmark-ips
ベンチマークテストはbenchmark-ips
というGemを使用する。
benchmark-ips
は、Rubyのベンチマークテストを実行するためのライブラリで、"ips"は"iterations per second"の略で1秒間に何回の処理を実行できるかを示す。
通常のBenchmark
モジュールでは実行時間が計測されるが、benchmark-ips
では実行速度が計測されるため、より結果を比較しやすい。
手順
0. 前提
キャッシュの保存場所は、ローカル環境での検証につきメモリとする。
config/environments/development.rb
に下記の通り記述している。
config.cache_store = :memory_store
1. テストデータの準備
ベンチマークテストに用いるMyModel
テーブルに、100万件のレコードを挿入する。
seeds.rb
に下記の通り記述し、rails db:seed
コマンドを実行する。
1_000_000.times { MyModel.create!(name: Faker::Name.name, age: rand(20..99), city: Faker::Address.city) }
2. ベンチマークテストのスクリプト実装
キャッシュを使用する場合/使用しない場合について、ベンチマークテストのスクリプトを下記の通り実装する。
内容は、MyModel
テーブルの3つのカラム(name
、age
、city
)に対してランダムな条件で検索してSELECTするというもの。
このテストを、
- インデックスが無い状態
- 検索対象のカラムの組み合わせのインデックスがある状態
で実施し、結果を比較する。
※このファイルはプロジェクトディレクトリ直下に配置する。
require 'benchmark/ips'
require_relative 'config/environment'
def random_query
name = MyModel.order('RAND()').limit(1).pluck(:name).first
age = rand(20..99)
city = MyModel.order('RAND()').limit(1).pluck(:city).first
MyModel.where(name: name, age: age, city: city).to_a
end
Benchmark.ips do |x|
x.config(time: 10, warmup: 2)
x.report('キャッシュなし') { random_query }
x.report('キャッシュあり') { Rails.cache.fetch('my_model_cache', expires_in: 1.hour) { random_query } }
x.compare!
end
-
x.config
:測定時間(time)とウォームアップ時間(warmup)を設定する。
ウォームアップ時間は、JITコンパイルやガーベジコレクションなどの影響を受けずに測定を行うために使用するらしい。 -
x.report
:測定対象のコードブロックとそのラベルを指定する。 -
x.compare!
:測定したパフォーマンスを比較し、結果を表示する。
3. SQLの実行計画確認(インデックスが無い状態)
ベンチマークテスト実行前に、インデックスが無い状態でのSQLの実行計画を確認しておく。
mysql> EXPLAIN SELECT * FROM `my_models` WHERE `my_models`.`name` = 'Kevin Wolff' AND `my_models`.`age` = 53 AND `my_models`.`city` = 'South Viviana';
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | my_models | NULL | ALL | NULL | NULL | NULL | NULL | 995115 | 0.10 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
-
type
: ALL。フルスキャンが行われる。 -
possible_keys
: NULL。利用可能なインデックスがないことを示す。 -
key
: NULL。使用されるインデックスがない。 -
rows
: 995,115。これはクエリが実行される際にスキャンされる行数の見積もりで、インデックスがないためほぼ全ての行がスキャンされることを示す。
4. ベンチマークテストの実行①(インデックスが無い状態)
ruby benchmark_test.rb
コマンドを実行して、ベンチマークテストを実施する。
結果は下記の通り。
# ruby benchmark_test.rb // ベンチマークテスト実行
Warming up --------------------------------------
キャッシュなし 1.000 i/100ms
キャッシュあり 1.000 i/100ms
Calculating -------------------------------------
キャッシュなし 0.634 (± 0.0%) i/s - 7.000 in 11.048828s
キャッシュあり 9.441k (±13.9%) i/s - 80.881k in 8.822440s
Comparison:
キャッシュあり インデックスなし: 9441.1 i/s
キャッシュなし インデックスなし: 0.6 i/s - 14901.22x slower
- キャッシュなし: 1秒あたり約0.634回のクエリが実行できる。
- キャッシュあり: 1秒あたり約9,441回のクエリが実行できる。
キャッシュありの場合、キャッシュなしの場合に比べて約14,901倍
速いパフォーマンスが得られており、キャッシュを使用することで大幅にパフォーマンスが向上していることがわかる。
5. インデックスの追加
検索対象のカラムにインデックスを追加するため、下記の通りマイグレーションファイルを作成する。
class AddCompositeIndexToMyModel < ActiveRecord::Migration[7.0]
def change
add_index :my_models, %i[name age city]
end
end
rails db:migrate
コマンドを実行して、インデックスを追加する。
6. SQLの実行計画確認(インデックスがある状態)
インデックスを追加したところで、SQLの実行計画を確認しておく。
mysql> EXPLAIN SELECT * FROM `my_models` WHERE `my_models`.`name` = 'Kevin Wolff' AND `my_models`.`age` = 53 AND `my_models`.`city` = 'South Viviana';
+----+-------------+-----------+------------+------+------------------------------------------+------------------------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------------+------+------------------------------------------+------------------------------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | my_models | NULL | ref | index_my_models_on_name_and_age_and_city | index_my_models_on_name_and_age_and_city | 2051 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+-----------+------------+------+------------------------------------------+------------------------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
-
type
: ref。インデックスが使用されてフルスキャンが回避されている。 -
possible_keys
: index_my_models_on_name_and_age_and_city。利用可能なインデックスが表示されている。 -
key
: index_my_models_on_name_and_age_and_city。このインデックスがクエリ実行時に使用される。 -
rows
: 1。インデックスのおかげで、スキャンされる行数が大幅に減少している。
7. ベンチマークテストの実行②(インデックスがある状態)
# ruby benchmark_test.rb
Warming up --------------------------------------
キャッシュなし インデックスあり 1.000 i/100ms
キャッシュあり インデックスあり 1.000 i/100ms
Calculating -------------------------------------
キャッシュなし インデックスあり 0.969 (± 0.0%) i/s - 10.000 in 10.322245s
キャッシュあり インデックスあり 9.417k (±18.3%) i/s - 77.719k in 8.820943s
Comparison:
キャッシュあり インデックスあり: 9417.2 i/s
キャッシュなし インデックスあり: 1.0 i/s - 9720.09x slower
- キャッシュなし: 1秒あたり約0.969回のクエリが実行できる。
- キャッシュあり: 1秒あたり約9,417回のクエリが実行できる。
ベンチマークテストの結果まとめ
ベンチマークテストの結果、1秒あたりに実行できるクエリの回数は下記の通りとなった。
インデックスなし | インデックスあり | |
---|---|---|
キャッシュ不使用 | 0.634回 | 0.969回 |
キャッシュ使用 | 9,441回 | 9,417回 |
誤差はあるものの、この表から下記のような結果が読み取れる。
-
キャッシュの使用により、インデックスが無い場合で約14,901倍、インデックスがある場合でも9約9718倍パフォーマンスが向上する。
-
インデックスにより、約1.5倍パフォーマンスが向上する。
さいごに
ハードウェア面のスペックや、データ件数、キャッシュの有効時間や保管場所など、実際にはパフォーマンスに影響する要因が色々あるのはもちろんだが、総じてキャッシュの利用と複合インデックスの適用により大幅にパフォーマンスが向上するという結果が確認できた。
またbenchmark-ip
というGemは非常に簡単に使えて結果の比較もしやすかったので、今後もパフォーマンスが気になった時にぜひ使ってみたいと思う。
キャッシュの使用可否や有効時間は業務要件によるものであり、キャッシュの保管場所の確保やその費用など考慮すべきことはたくさんあるけれど、今回わかったキャッシュによりこんなにも大幅にパフォーマンスが向上するということを頭に置き、適切なアプリケーションの構成を考えられるようになりたい。