SQLite on Railsシリーズ(09)SQLiteのパフォーマンス測定結果(翻訳)
Ruby on Railsアプリケーションで SQLiteを使うのであれば、以下の過去記事で見てきたようなさまざまな微調整が不可欠です。
SQLiteは本来高速ですが、デフォルト設定のままではWebアプリ向けに調整されていません(訳注: Rails 8では既にSQLiteのチューニングは完了しています)。本記事では、いくつかのベンチマークを調べ、SQLiteデータベースを微調整する価値がどれほど大きいかを掘り下げたいと思います。
データベースやORM(Object/Relationマッピング)のベンチマークには、さまざまな形やサイズがあります。
Transaction Processing Performance CouncilのTPC-Eのような企業レベルの評議会が作成した標準もあれば、Telecom Application Transaction Processing Benchmarkのように学術研究でよく使われる大規模なベンチマークもあります。また、個人が通常使うようなアドホックなカスタムベンチマークもあります。
私としては独自のベンチマークスイートを書きたくなかったのですが、SQLiteやRailsとシームレスに連携するベンチマークスイートも必要でした。ありがたいことに、Ruby世界の真の宝石の1人であるJeremy Evansが公開しているベンチマークスイートを使えば、さまざまなRuby ORMを各種データベースに対してベンチマークできます。
これはRubyで書かれており、Ruby ORMを対象としていて、さまざまな操作がうまく組み合わされています。Jeremyが一流のプログラマーであることはわかっているので、何も言わなくても基本的に信頼しています。このベンチマーク操作スイートが、私のベンチマークの基盤となっています。
そういうわけで、私はコードを書き直しました。Jeremyのコードから複数のORMやデータベースエンジンをサポートするのに必要な間接参照を削除し、ベンチマーク結果を自分の興味に合わせて調整するためです。本記事で利用したベンチマークのコードは、このGistで参照できます。
目標は、上述の過去記事で解説した微調整オプションが、さまざまな操作・負荷・コンテキストにわたってActive Recordのパフォーマンスにどのように影響するかについての洞察を提供することです。
すべてのベンチマークはMacBook Pro 2021で実行しました。
- Apple M1 Maxチップ
- 32GB RAM
- macOS Monterey(12.5.1)
- 16インチ
また、各ベンチマーク実行には、sqlite3-ruby
gemのバージョン1.6.6を使っています。
個別のシナリオについて、設定されているすべてのコンパイル時オプションと、関連する1すべての PRAGMA
の値を報告します。
ここで重要なのは、SQLiteがどのような形でベンチマーク用に設定されているかという全体像を示すことです。Purohith、Mohan、およびChidambaramによる論文「The Dangers and Complexities of SQLite Benchmarking」で詳述されているように、このコンテキストが欠けているベンチマーク結果は、結果を再現することも、より大きなベンチマークコンテキストに配置することもできません。
これは特にSQLiteに当てはまり、設定を1箇所変更するだけでパフォーマンスが10倍以上向上する可能性があります。ここでは、さまざまな設定がパフォーマンスにどのように影響するかに関心があるので、このコンテキストは不可欠です。
ここでは、以下の4つのシナリオでSQLiteをベンチマークします:
- 「デフォルトのコンパイルオプションを用いた
sqlite3-ruby
」「PRAGMA
微調整なし」 - 「デフォルトのコンパイルオプションを用いた
sqlite3-ruby
」「PRAGMA
微調整あり」 - 「推奨のコンパイルオプションを用いた
sqlite3-ruby
」「PRAGMA
微調整なし」 - 「推奨のコンパイルオプションを用いた
sqlite3-ruby
」「PRAGMA
微調整あり」
結果を詳しく見てみましょう
🔗 1: デフォルトコンパイルのSQLite、ランタイム微調整なし
このベンチマークがベースラインになります。
sqlite3-ruby
1.6.6 をインストールし、機能強化なしでベンチマークスクリプトを実行します。
gem install sqlite3 -v 1.6.6
irb
require_relative 'sqlite-activerecord-benchmark'
run_benchmark!(enhance: false, log: true)
run_benchmark!
関数は、ベンチマーク実行の平均合計時間(完全なスイートを10回実行)と、個別のベンチマーク操作の平均時間のタプルを返します。私のコンピュータでベンチマークを実行したところ、平均合計時間は以下のようになりました。
13.5945s
▶ベンチマークの操作別内訳(クリックで展開)
Operation | Duration |
---|---|
heavy_threading | 3.4941s |
model_object_destruction | 2.4036s |
model_object_and_associated_object_creation | 1.5116s |
eager_loading_single_query_with_1_to_n_to_n_records | 1.2999s |
model_object_select_and_save | 1.2889s |
eager_loading_single_query_with_1_to_n_to_n_records (txn) | 1.2686s |
light_threading | 0.4362s |
model_object_update_json | 0.3391s |
model_object_select_and_save (txn) | 0.2782s |
model_object_update_json_nested | 0.2304s |
lazy_loading_with_1_to_1_records | 0.1659s |
lazy_loading_with_1_to_1_records (txn) | 0.1627s |
model_object_update_json (txn) | 0.0804s |
eager_loading_query_per_association_with_1_to_n_to_n_records (txn) | 0.0549s |
eager_loading_query_per_association_with_1_to_n_to_n_records | 0.0545s |
eager_loading_single_query_with_1_to_n_records | 0.0431s |
model_object_select_json_nested | 0.0414s |
eager_loading_single_query_with_1_to_n_records (txn) | 0.0412s |
model_object_update_json_nested (txn) | 0.0409s |
model_object_select_json_nested (txn) | 0.038s |
lazy_loading_with_1_to_n_records | 0.0374s |
lazy_loading_with_1_to_n_records (txn) | 0.0356s |
eager_loading_query_per_association_with_1_to_n_records | 0.0316s |
eager_loading_query_per_association_with_1_to_n_records (txn) | 0.0306s |
eager_loading_single_query_with_1_to_1_records | 0.026s |
eager_loading_single_query_with_1_to_1_records (txn) | 0.0258s |
model_object_select_by_attr | 0.0255s |
eager_loading_query_per_association_with_1_to_1_records (txn) | 0.0241s |
eager_loading_query_per_association_with_1_to_1_records | 0.0239s |
model_object_select_by_attr (txn) | 0.0231s |
model_object_select_by_pk | 0.0183s |
model_object_select_by_pk (txn) | 0.0161s |
model_object_creation | 0.0013s |
model_object_creation (txn) | 0.001s |
model_object_and_associated_object_creation (txn) | 0.0003s |
model_object_destruction (txn) | 0.0003s |
model_object_select (txn) | 0.0001s |
model_object_select | 0.0s |
▶環境情報(クリックで展開)
{
"sqlite3-ruby version" => "1.6.6",
"sqlite3 version" => "3.43.1",
"sqlcipher?" => false,
"threadsafe?" => true,
"compile_options" => [
"ATOMIC_INTRINSICS=1",
"COMPILER=clang-10.0.0",
"DEFAULT_AUTOVACUUM",
"DEFAULT_CACHE_SIZE=-2000",
"DEFAULT_FILE_FORMAT=4",
"DEFAULT_JOURNAL_SIZE_LIMIT=-1",
"DEFAULT_MMAP_SIZE=0",
"DEFAULT_PAGE_SIZE=4096",
"DEFAULT_PCACHE_INITSZ=20",
"DEFAULT_RECURSIVE_TRIGGERS",
"DEFAULT_SECTOR_SIZE=4096",
"DEFAULT_SYNCHRONOUS=2",
"DEFAULT_WAL_AUTOCHECKPOINT=1000",
"DEFAULT_WAL_SYNCHRONOUS=2",
"DEFAULT_WORKER_THREADS=0",
"ENABLE_FTS3",
"ENABLE_FTS4",
"ENABLE_FTS5",
"ENABLE_GEOPOLY",
"ENABLE_MATH_FUNCTIONS",
"ENABLE_RTREE",
"MALLOC_SOFT_LIMIT=1024",
"MAX_ATTACHED=10",
"MAX_COLUMN=2000",
"MAX_COMPOUND_SELECT=500",
"MAX_DEFAULT_PAGE_SIZE=8192",
"MAX_EXPR_DEPTH=1000",
"MAX_FUNCTION_ARG=127",
"MAX_LENGTH=1000000000",
"MAX_LIKE_PATTERN_LENGTH=50000",
"MAX_MMAP_SIZE=0x7fff0000",
"MAX_PAGE_COUNT=1073741823",
"MAX_PAGE_SIZE=65536",
"MAX_SQL_LENGTH=1000000000",
"MAX_TRIGGER_DEPTH=1000",
"MAX_VARIABLE_NUMBER=32766",
"MAX_VDBE_OP=250000000",
"MAX_WORKER_THREADS=8",
"MUTEX_PTHREADS",
"SYSTEM_MALLOC",
"TEMP_STORE=1",
"THREADSAFE=1"
],
"pragmas" => {
"analysis_limit" => 0,
"application_id" => 0,
"auto_vacuum" => 0,
"automatic_index" => 1,
"timeout" => 0,
"cache_size" => -2000,
"cache_spill" => 483,
"cell_size_check" => 0,
"checkpoint_fullfsync" => 0,
"data_version" => 1,
"defer_foreign_keys" => 0,
"encoding" => "UTF-8",
"foreign_keys" => 1,
"freelist_count" => 0,
"fullfsync" => 0,
"hard_heap_limit" => 0,
"ignore_check_constraints" => 0,
"integrity_check" => "ok",
"journal_mode" => "delete",
"journal_size_limit" => -1,
"legacy_alter_table" => 0,
"locking_mode" => "normal",
"max_page_count" => 1073741823,
"mmap_size" => 0,
"page_count" => 7,
"page_size" => 4096,
"query_only" => 0,
"quick_check" => "ok",
"read_uncommitted" => 0,
"recursive_triggers" => 0,
"reverse_unordered_selects" => 0,
"secure_delete" => 0,
"soft_heap_limit" => 0,
"synchronous" => 2,
"temp_store" => 0,
"threads" => 0,
"trusted_schema" => 1,
"user_version" => 0,
"wal_autocheckpoint" => 1000
}
}
🔗 2: デフォルトコンパイルのSQLite、ランタイムPRAGMA
微調整あり
引き続きsqlite3-ruby
v1.6.6)のデフォルトインストールを使いますが、この実行では過去記事のPRAGMA
による機能強化を適用します。
require_relative 'sqlite-activerecord-benchmark'
run_benchmark!(enhance: true, log: true)
私のコンピュータでベンチマークを実行するたびに、平均合計時間は2倍短縮されています。
6.7886s
この結果は、前述のPurohith、Mohan、ChidambaramによるSQLiteベンチマーク研究論文の調査結果と一致しています。
調査の結果、ジャーナルモードのみを変更するとパフォーマンスが11.8倍向上し、同期モードのみを変更すると1.5倍向上し、ジャーナルサイズのみを変更すると5倍向上することが示された。
SQLiteデータベースのパフォーマンスを強化したければ、ランタイム設定をこのように微調整する必要があるということです。
▶ベンチマークの操作別内訳(クリックで展開)
Operation | Duration |
---|---|
eager_loading_single_query_with_1_to_n_to_n_records | 1.317s |
eager_loading_single_query_with_1_to_n_to_n_records (txn) | 1.2892s |
heavy_threading | 1.1621s |
model_object_destruction | 0.5061s |
model_object_and_associated_object_creation | 0.4827s |
model_object_select_and_save | 0.3851s |
model_object_select_and_save (txn) | 0.2797s |
lazy_loading_with_1_to_1_records | 0.165s |
lazy_loading_with_1_to_1_records (txn) | 0.1644s |
light_threading | 0.142s |
model_object_update_json | 0.1388s |
model_object_update_json (txn) | 0.0819s |
eager_loading_query_per_association_with_1_to_n_to_n_records (txn) | 0.0553s |
eager_loading_query_per_association_with_1_to_n_to_n_records | 0.0539s |
model_object_update_json_nested | 0.0487s |
model_object_update_json_nested (txn) | 0.0413s |
eager_loading_single_query_with_1_to_n_records (txn) | 0.04s |
eager_loading_single_query_with_1_to_n_records | 0.0398s |
model_object_select_json_nested | 0.0386s |
lazy_loading_with_1_to_n_records | 0.0382s |
model_object_select_json_nested (txn) | 0.0376s |
lazy_loading_with_1_to_n_records (txn) | 0.0367s |
eager_loading_query_per_association_with_1_to_n_records | 0.0305s |
eager_loading_query_per_association_with_1_to_n_records (txn) | 0.0304s |
eager_loading_single_query_with_1_to_1_records | 0.0265s |
eager_loading_single_query_with_1_to_1_records (txn) | 0.0264s |
eager_loading_query_per_association_with_1_to_1_records | 0.0246s |
eager_loading_query_per_association_with_1_to_1_records (txn) | 0.0245s |
model_object_select_by_attr | 0.0243s |
model_object_select_by_attr (txn) | 0.0239s |
model_object_select_by_pk | 0.0165s |
model_object_select_by_pk (txn) | 0.0159s |
model_object_creation | 0.0005s |
model_object_creation (txn) | 0.0004s |
model_object_destruction (txn) | 0.0002s |
model_object_and_associated_object_creation (txn) | 0.0002s |
model_object_select (txn) | 0.0s |
model_object_select | 0.0s |
▶環境情報(クリックで展開)
{
"sqlite3-ruby version"=>"1.6.6",
"sqlite3 version"=>"3.43.1",
"sqlcipher?"=>false,
"threadsafe?"=>true,
"compile_options"=>[
"ATOMIC_INTRINSICS=1",
"COMPILER=clang-10.0.0",
"DEFAULT_AUTOVACUUM",
"DEFAULT_CACHE_SIZE=-2000",
"DEFAULT_FILE_FORMAT=4",
"DEFAULT_JOURNAL_SIZE_LIMIT=-1",
"DEFAULT_MMAP_SIZE=0",
"DEFAULT_PAGE_SIZE=4096",
"DEFAULT_PCACHE_INITSZ=20",
"DEFAULT_RECURSIVE_TRIGGERS",
"DEFAULT_SECTOR_SIZE=4096",
"DEFAULT_SYNCHRONOUS=2",
"DEFAULT_WAL_AUTOCHECKPOINT=1000",
"DEFAULT_WAL_SYNCHRONOUS=2",
"DEFAULT_WORKER_THREADS=0",
"ENABLE_FTS3",
"ENABLE_FTS4",
"ENABLE_FTS5",
"ENABLE_GEOPOLY",
"ENABLE_MATH_FUNCTIONS",
"ENABLE_RTREE",
"MALLOC_SOFT_LIMIT=1024",
"MAX_ATTACHED=10",
"MAX_COLUMN=2000",
"MAX_COMPOUND_SELECT=500",
"MAX_DEFAULT_PAGE_SIZE=8192",
"MAX_EXPR_DEPTH=1000",
"MAX_FUNCTION_ARG=127",
"MAX_LENGTH=1000000000",
"MAX_LIKE_PATTERN_LENGTH=50000",
"MAX_MMAP_SIZE=0x7fff0000",
"MAX_PAGE_COUNT=1073741823",
"MAX_PAGE_SIZE=65536",
"MAX_SQL_LENGTH=1000000000",
"MAX_TRIGGER_DEPTH=1000",
"MAX_VARIABLE_NUMBER=32766",
"MAX_VDBE_OP=250000000",
"MAX_WORKER_THREADS=8",
"MUTEX_PTHREADS",
"SYSTEM_MALLOC",
"TEMP_STORE=1",
"THREADSAFE=1"
],
"pragmas" => {
"analysis_limit"=>0,
"application_id"=>0,
"auto_vacuum"=>0,
"automatic_index"=>1,
"timeout"=>0,
"cache_size"=>2000,
"cache_spill"=>2000,
"cell_size_check"=>0,
"checkpoint_fullfsync"=>0,
"data_version"=>2,
"defer_foreign_keys"=>0,
"encoding"=>"UTF-8",
"foreign_keys"=>1,
"freelist_count"=>0,
"fullfsync"=>0,
"hard_heap_limit"=>0,
"ignore_check_constraints"=>0,
"integrity_check"=>"ok",
"journal_mode"=>"wal",
"journal_size_limit"=>67108864,
"legacy_alter_table"=>0,
"locking_mode"=>"normal",
"max_page_count"=>1073741823,
"mmap_size"=>134217728,
"page_count"=>7,
"page_size"=>4096,
"query_only"=>0,
"quick_check"=>"ok",
"read_uncommitted"=>0,
"recursive_triggers"=>0,
"reverse_unordered_selects"=>0,
"secure_delete"=>0,
"soft_heap_limit"=>0,
"synchronous"=>1,
"temp_store"=>0,
"threads"=>0,
"trusted_schema"=>1,
"user_version"=>0,
"wal_autocheckpoint"=>1000
}
}
🔗 3: コンパイル調整済みSQLite、ランタイム微調整なし
データベースのPRAGMA
を調整するとパフォーマンスが最も著しく向上しますが、コンパイル時のフラグも微調整すれば、さらに数サイクルを節約できます。
テスト環境を準備するには、以下のようにsqlite3-ruby
gem v1.6.6
をアンインストールし、コンパイルフラグを指定して再インストールする必要があります。
gem uninstall sqlite3 -v 1.6.6
gem install sqlite3 -v 1.6.6 --platform=ruby -- \
--with-sqlite-cflags="-DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_FTS5"
インストールが完了したら、irb
コンソールでベンチマークを実行します。
require_relative 'sqlite-activerecord-benchmark'
run_benchmark!(enhance: false, log: true)
SQLiteドキュメントには、推奨されるコンパイルフラグ セットをすべて使っても5%程度しか増加しないと記載されているため、予想どおり、ベースラインと比較するとわずかな改善(約 3%)しか見られません。
13.2174s
▶ベンチマークの操作別内訳(クリックで展開)
| Operation | Duration |
| heavy_threading | 3.4161s |
| model_object_destruction | 2.3001s |
| model_object_and_associated_object_creation | 1.4006s |
| model_object_select_and_save | 1.3145s |
| eager_loading_single_query_with_1_to_n_to_n_records | 1.2383s |
| eager_loading_single_query_with_1_to_n_to_n_records (txn) | 1.233s |
| light_threading | 0.423s |
| model_object_update_json | 0.328s |
| model_object_select_and_save (txn) | 0.2785s |
| model_object_update_json_nested | 0.2281s |
| lazy_loading_with_1_to_1_records | 0.1765s |
| lazy_loading_with_1_to_1_records (txn) | 0.1688s |
| model_object_update_json (txn) | 0.0802s |
| eager_loading_query_per_association_with_1_to_n_to_n_records (txn) | 0.0555s |
| eager_loading_query_per_association_with_1_to_n_to_n_records | 0.0541s |
| model_object_update_json_nested (txn) | 0.0405s |
| model_object_select_json_nested | 0.0405s |
| eager_loading_single_query_with_1_to_n_records (txn) | 0.04s |
| eager_loading_single_query_with_1_to_n_records | 0.0387s |
| model_object_select_json_nested (txn) | 0.0374s |
| lazy_loading_with_1_to_n_records | 0.037s |
| lazy_loading_with_1_to_n_records (txn) | 0.0365s |
| eager_loading_query_per_association_with_1_to_n_records (txn) | 0.0313s |
| eager_loading_query_per_association_with_1_to_n_records | 0.031s |
| model_object_select_by_attr | 0.0272s |
| eager_loading_single_query_with_1_to_1_records | 0.0262s |
| eager_loading_single_query_with_1_to_1_records (txn) | 0.026s |
| model_object_select_by_attr (txn) | 0.0251s |
| eager_loading_query_per_association_with_1_to_1_records | 0.0247s |
| eager_loading_query_per_association_with_1_to_1_records (txn) | 0.0243s |
| model_object_select_by_pk | 0.0176s |
| model_object_select_by_pk (txn) | 0.0155s |
| model_object_creation | 0.0012s |
| model_object_creation (txn) | 0.001s |
| model_object_and_associated_object_creation (txn) | 0.0003s |
| model_object_destruction (txn) | 0.0003s |
| model_object_select (txn) | 0.0001s |
| model_object_select | 0.0s |
▶環境情報(クリックで展開)
{
"sqlite3-ruby version" => "1.6.6",
"sqlite3 version" => "3.43.1",
"sqlcipher?" => false,
"threadsafe?" => false,
"compile_options" => [
"ATOMIC_INTRINSICS=1",
"COMPILER=clang-14.0.0",
"DEFAULT_AUTOVACUUM",
"DEFAULT_CACHE_SIZE=-2000",
"DEFAULT_FILE_FORMAT=4",
"DEFAULT_JOURNAL_SIZE_LIMIT=-1",
"DEFAULT_MEMSTATUS=0",
"DEFAULT_MMAP_SIZE=0",
"DEFAULT_PAGE_SIZE=4096",
"DEFAULT_PCACHE_INITSZ=20",
"DEFAULT_RECURSIVE_TRIGGERS",
"DEFAULT_SECTOR_SIZE=4096",
"DEFAULT_SYNCHRONOUS=2",
"DEFAULT_WAL_AUTOCHECKPOINT=1000",
"DEFAULT_WAL_SYNCHRONOUS=2",
"DEFAULT_WORKER_THREADS=0",
"DQS=0",
"ENABLE_FTS3",
"ENABLE_FTS4",
"ENABLE_FTS5",
"ENABLE_GEOPOLY",
"ENABLE_MATH_FUNCTIONS",
"ENABLE_RTREE",
"LIKE_DOESNT_MATCH_BLOBS",
"MALLOC_SOFT_LIMIT=1024",
"MAX_ATTACHED=10",
"MAX_COLUMN=2000",
"MAX_COMPOUND_SELECT=500",
"MAX_DEFAULT_PAGE_SIZE=8192",
"MAX_EXPR_DEPTH=0",
"MAX_FUNCTION_ARG=127",
"MAX_LENGTH=1000000000",
"MAX_LIKE_PATTERN_LENGTH=50000",
"MAX_MMAP_SIZE=0x7fff0000",
"MAX_PAGE_COUNT=1073741823",
"MAX_PAGE_SIZE=65536",
"MAX_SQL_LENGTH=1000000000",
"MAX_TRIGGER_DEPTH=1000",
"MAX_VARIABLE_NUMBER=32766",
"MAX_VDBE_OP=250000000",
"MAX_WORKER_THREADS=0",
"MUTEX_OMIT",
"OMIT_PROGRESS_CALLBACK",
"OMIT_SHARED_CACHE",
"SYSTEM_MALLOC",
"TEMP_STORE=1",
"THREADSAFE=0",
"USE_ALLOCA"
],
"pragmas" => {
"analysis_limit" => 0,
"application_id" => 0,
"auto_vacuum" => 0,
"automatic_index" => 1,
"timeout" => 0,
"cache_size" => -2000,
"cache_spill" => 483,
"cell_size_check" => 0,
"checkpoint_fullfsync" => 0,
"data_version" => 1,
"defer_foreign_keys" => 0,
"encoding" => "UTF-8",
"foreign_keys" => 1,
"freelist_count" => 0,
"fullfsync" => 0,
"hard_heap_limit" => 0,
"ignore_check_constraints" => 0,
"integrity_check" => "ok",
"journal_mode" => "delete",
"journal_size_limit" => -1,
"legacy_alter_table" => 0,
"locking_mode" => "normal",
"max_page_count" => 1073741823,
"mmap_size" => 0,
"page_count" => 7,
"page_size" => 4096,
"query_only" => 0,
"quick_check" => "ok",
"read_uncommitted" => 0,
"recursive_triggers" => 0,
"reverse_unordered_selects" => 0,
"secure_delete" => 0,
"soft_heap_limit" => 0,
"synchronous" => 2,
"temp_store" => 0,
"threads" => 0,
"trusted_schema" => 1,
"user_version" => 0,
"wal_autocheckpoint" => 1000
}
}
🔗 4: コンパイル調整済みSQLite、ランタイム微調整あり
最後に、完全調整済みのSQLiteセットアップを調べます。もちろん、コンパイルチューニングによるパフォーマンス向上がわずかであることは既にわかっていますが、とにかく実行して正確な結果を確認してみましょう。
2番目のシナリオと比較すると、さらに約3%もの改善が見られます。
6.5462s
▶ベンチマークの操作別内訳(クリックで展開)
| Operation | Duration |
| eager_loading_single_query_with_1_to_n_to_n_records | 1.2298s |
| eager_loading_single_query_with_1_to_n_to_n_records (txn) | 1.1829s |
| heavy_threading | 1.1489s |
| model_object_destruction | 0.4958s |
| model_object_and_associated_object_creation | 0.477s |
| model_object_select_and_save | 0.3783s |
| model_object_select_and_save (txn) | 0.2746s |
| lazy_loading_with_1_to_1_records | 0.171s |
| lazy_loading_with_1_to_1_records (txn) | 0.1697s |
| light_threading | 0.1403s |
| model_object_update_json | 0.1313s |
| model_object_update_json (txn) | 0.0792s |
| eager_loading_query_per_association_with_1_to_n_to_n_records (txn) | 0.0541s |
| eager_loading_query_per_association_with_1_to_n_to_n_records | 0.0532s |
| model_object_update_json_nested | 0.0461s |
| eager_loading_single_query_with_1_to_n_records | 0.0404s |
| eager_loading_single_query_with_1_to_n_records (txn) | 0.0402s |
| model_object_select_json_nested | 0.0395s |
| model_object_update_json_nested (txn) | 0.0394s |
| model_object_select_json_nested (txn) | 0.0379s |
| lazy_loading_with_1_to_n_records | 0.0375s |
| lazy_loading_with_1_to_n_records (txn) | 0.0354s |
| eager_loading_query_per_association_with_1_to_n_records | 0.0309s |
| eager_loading_query_per_association_with_1_to_n_records (txn) | 0.0304s |
| eager_loading_single_query_with_1_to_1_records | 0.026s |
| eager_loading_single_query_with_1_to_1_records (txn) | 0.0256s |
| model_object_select_by_attr | 0.025s |
| model_object_select_by_attr (txn) | 0.0244s |
| eager_loading_query_per_association_with_1_to_1_records | 0.0236s |
| eager_loading_query_per_association_with_1_to_1_records (txn) | 0.0236s |
| model_object_select_by_pk | 0.0167s |
| model_object_select_by_pk (txn) | 0.0164s |
| model_object_creation | 0.0005s |
| model_object_creation (txn) | 0.0005s |
| model_object_and_associated_object_creation (txn) | 0.0002s |
| model_object_destruction (txn) | 0.0002s |
| model_object_select | 0.0s |
| model_object_select (txn) | 0.0s |
▶環境情報(クリックで展開)
{
"sqlite3-ruby version" => "1.6.6",
"sqlite3 version" => "3.43.1",
"sqlcipher?" => false,
"threadsafe?" => false,
"compile_options" => [
"ATOMIC_INTRINSICS=1",
"COMPILER=clang-14.0.0",
"DEFAULT_AUTOVACUUM",
"DEFAULT_CACHE_SIZE=-2000",
"DEFAULT_FILE_FORMAT=4",
"DEFAULT_JOURNAL_SIZE_LIMIT=-1",
"DEFAULT_MEMSTATUS=0",
"DEFAULT_MMAP_SIZE=0",
"DEFAULT_PAGE_SIZE=4096",
"DEFAULT_PCACHE_INITSZ=20",
"DEFAULT_RECURSIVE_TRIGGERS",
"DEFAULT_SECTOR_SIZE=4096",
"DEFAULT_SYNCHRONOUS=2",
"DEFAULT_WAL_AUTOCHECKPOINT=1000",
"DEFAULT_WAL_SYNCHRONOUS=2",
"DEFAULT_WORKER_THREADS=0",
"DQS=0",
"ENABLE_FTS3",
"ENABLE_FTS4",
"ENABLE_FTS5",
"ENABLE_GEOPOLY",
"ENABLE_MATH_FUNCTIONS",
"ENABLE_RTREE",
"LIKE_DOESNT_MATCH_BLOBS",
"MALLOC_SOFT_LIMIT=1024",
"MAX_ATTACHED=10",
"MAX_COLUMN=2000",
"MAX_COMPOUND_SELECT=500",
"MAX_DEFAULT_PAGE_SIZE=8192",
"MAX_EXPR_DEPTH=0",
"MAX_FUNCTION_ARG=127",
"MAX_LENGTH=1000000000",
"MAX_LIKE_PATTERN_LENGTH=50000",
"MAX_MMAP_SIZE=0x7fff0000",
"MAX_PAGE_COUNT=1073741823",
"MAX_PAGE_SIZE=65536",
"MAX_SQL_LENGTH=1000000000",
"MAX_TRIGGER_DEPTH=1000",
"MAX_VARIABLE_NUMBER=32766",
"MAX_VDBE_OP=250000000",
"MAX_WORKER_THREADS=0",
"MUTEX_OMIT",
"OMIT_PROGRESS_CALLBACK",
"OMIT_SHARED_CACHE",
"SYSTEM_MALLOC",
"TEMP_STORE=1",
"THREADSAFE=0",
"USE_ALLOCA"
],
"pragmas" => {
"analysis_limit" => 0,
"application_id" => 0,
"auto_vacuum" => 0,
"automatic_index" => 1,
"timeout" => 0,
"cache_size" => 2000,
"cache_spill" => 2000,
"cell_size_check" => 0,
"checkpoint_fullfsync" => 0,
"data_version" => 2,
"defer_foreign_keys" => 0,
"encoding" => "UTF-8",
"foreign_keys" => 1,
"freelist_count" => 0,
"fullfsync" => 0,
"hard_heap_limit" => 0,
"ignore_check_constraints" => 0,
"integrity_check" => "ok",
"journal_mode" => "wal",
"journal_size_limit" => 67108864,
"legacy_alter_table" => 0,
"locking_mode" => "normal",
"max_page_count" => 1073741823,
"mmap_size" => 134217728,
"page_count" => 7,
"page_size" => 4096,
"query_only" => 0,
"quick_check" => "ok",
"read_uncommitted" => 0,
"recursive_triggers" => 0,
"reverse_unordered_selects" => 0,
"secure_delete" => 0,
"soft_heap_limit" => 0,
"synchronous" => 1,
"temp_store" => 0,
"threads" => 0,
"trusted_schema" => 1,
"user_version" => 0,
"wal_autocheckpoint" => 1000
}
}
🔗 まとめ
以上のすべてを踏まえると、どのような結論を導き出せるでしょうか。
PRAGMA
のチューニング、特にjournal_mode
、synchronization
、journal_size_limit
プラグマの設定が必要不可欠であることは明らかです。また、コンパイルチューニングは、プラグマチューニングのような2倍の改善は得られませんが、今では非常に簡単にできるので、3~5%を無駄にするのはもったいないですよね。
せっかくなので、これらのさまざまなSQLite 設定が、ノートPC上の同じベンチマークでPostgreSQLをローカル実行する場合とどの程度パフォーマンスが違ってくるかも見てみたいと思いました。
pg gemの微調整方法についてはよく知らないので、Macbook Proにデフォルトでインストールされたpg(gemバージョン1.5.4、PostgreSQLバージョン14.0.9)でベンチマークを1回だけ実行しました。
結果は次のとおりです:2
11.4623s
▶ベンチマークの操作別内訳(クリックで展開)
| Operation | Duration |
| heavy_threading | 2.8473s |
| model_object_destruction | 1.232s |
| eager_loading_single_query_with_1_to_n_to_n_records | 1.2192s |
| eager_loading_single_query_with_1_to_n_to_n_records (txn) | 1.2174s |
| model_object_and_associated_object_creation | 1.0791s |
| model_object_select_and_save | 0.7963s |
| model_object_select_and_save (txn) | 0.3855s |
| light_threading | 0.3575s |
| lazy_loading_with_1_to_1_records (txn) | 0.3465s |
| lazy_loading_with_1_to_1_records | 0.3404s |
| model_object_update_json | 0.2407s |
| model_object_select_by_attr (txn) | 0.1708s |
| model_object_select_by_attr | 0.1591s |
| model_object_select_json_nested | 0.1392s |
| model_object_select_json_nested (txn) | 0.1383s |
| model_object_update_json_nested | 0.1219s |
| model_object_update_json (txn) | 0.1008s |
| model_object_update_json_nested (txn) | 0.0655s |
| eager_loading_query_per_association_with_1_to_n_to_n_records (txn) | 0.0545s |
| eager_loading_query_per_association_with_1_to_n_to_n_records | 0.0521s |
| lazy_loading_with_1_to_n_records | 0.0474s |
| lazy_loading_with_1_to_n_records (txn) | 0.0471s |
| eager_loading_single_query_with_1_to_n_records | 0.0392s |
| eager_loading_single_query_with_1_to_n_records (txn) | 0.0375s |
| eager_loading_query_per_association_with_1_to_n_records (txn) | 0.0303s |
| eager_loading_query_per_association_with_1_to_n_records | 0.0302s |
| model_object_select_by_pk | 0.0299s |
| model_object_select_by_pk (txn) | 0.0295s |
| eager_loading_single_query_with_1_to_1_records (txn) | 0.0265s |
| eager_loading_single_query_with_1_to_1_records | 0.0263s |
| eager_loading_query_per_association_with_1_to_1_records | 0.0249s |
| eager_loading_query_per_association_with_1_to_1_records (txn) | 0.0247s |
| model_object_creation | 0.0017s |
| model_object_creation (txn) | 0.0015s |
| model_object_destruction (txn) | 0.0009s |
| model_object_and_associated_object_creation (txn) | 0.0007s |
| model_object_select (txn) | 0.0001s |
| model_object_select | 0.0s |
PostgreSQLの場合、調整されていないデフォルトのSQLiteよりは数秒速いものの、微調整済みのSQLiteインストールよりはほぼ2倍遅いという結果になりました。また、ベンチマークとPostgreSQLサーバーの両方が同一コンピュータ上で実行されているため、これは完全に現実的なネットワーク遅延が存在しないケースとなります。このシンプルなベンチマークでも、特にWebアプリケーションに合わせて調整した場合のSQLiteのパフォーマンスが実証されることを願っています。
関連記事
-
原注: ここで言う「関連(relevant)」とは、
PRAGMA
のサブセットを意味します。つまり、SQLiteドキュメントで提供されている完全なセットを取得してから、「非推奨のプラグマ」「特殊なプラグマ」「内部データプラグマ」を除外して生成されたものです。その結果、40個の「設定」プラグマのリストが残りました。完全なリストについては、SQLite をPRAGMA
で微調整する過去記事に記載されています。 ↩ - 原注: ここで使った調整済みベンチマークはこのGistで参照できます。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
参考: Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)|YassLab 株式会社
日本語タイトルは内容に即したものにしました。