Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

SQLite on Railsシリーズ(09)SQLiteのパフォーマンス測定結果(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

参考: Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)|YassLab 株式会社

日本語タイトルは内容に即したものにしました。

SQLite on Railsシリーズ(09)SQLiteのパフォーマンス測定結果(翻訳)

Ruby on Railsアプリケーションで SQLiteを使うのであれば、以下の過去記事で見てきたようなさまざまな微調整が不可欠です。

SQLite on Railsシリーズ(02)SQLiteをチューニングで強化する(翻訳)

SQLite on Railsシリーズ(05)SQLiteのコンパイルオプションを最適化する(翻訳)

SQLiteは本来高速ですが、デフォルト設定のままではWebアプリ向けに調整されていません(訳注: Rails 8では既にSQLiteのチューニングは完了しています)。本記事では、いくつかのベンチマークを調べ、SQLiteデータベースを微調整する価値がどれほど大きいかを掘り下げたいと思います。


データベースやORM(Object/Relationマッピング)のベンチマークには、さまざまな形やサイズがあります。
Transaction Processing Performance CouncilTPC-Eのような企業レベルの評議会が作成した標準もあれば、Telecom Application Transaction Processing Benchmarkのように学術研究でよく使われる大規模なベンチマークもあります。また、個人が通常使うようなアドホックなカスタムベンチマークもあります。

私としては独自のベンチマークスイートを書きたくなかったのですが、SQLiteやRailsとシームレスに連携するベンチマークスイートも必要でした。ありがたいことに、Ruby世界の真の宝石の1人であるJeremy Evansが公開しているベンチマークスイートを使えば、さまざまなRuby ORMを各種データベースに対してベンチマークできます。

jeremyevans/simple_orm_benchmark - GitHub

これはRubyで書かれており、Ruby ORMを対象としていて、さまざまな操作がうまく組み合わされています。Jeremyが一流のプログラマーであることはわかっているので、何も言わなくても基本的に信頼しています。このベンチマーク操作スイートが、私のベンチマークの基盤となっています。

そういうわけで、私はコードを書き直しました。Jeremyのコードから複数のORMやデータベースエンジンをサポートするのに必要な間接参照を削除し、ベンチマーク結果を自分の興味に合わせて調整するためです。本記事で利用したベンチマークのコードは、このGistで参照できます。

目標は、上述の過去記事で解説した微調整オプションが、さまざまな操作・負荷・コンテキストにわたってActive Recordのパフォーマンスにどのように影響するかについての洞察を提供することです。
すべてのベンチマークはMacBook Pro 2021で実行しました。

  • Apple M1 Maxチップ
  • 32GB RAM
  • macOS Monterey(12.5.1)
  • 16インチ

また、各ベンチマーク実行には、sqlite3-rubygemのバージョン1.6.6を使っています。

sparklemotion/sqlite3-ruby - GitHub

個別のシナリオについて、設定されているすべてのコンパイル時オプションと、関連する1すべての PRAGMA の値を報告します。

ここで重要なのは、SQLiteがどのような形でベンチマーク用に設定されているかという全体像を示すことです。Purohith、Mohan、およびChidambaramによる論文「The Dangers and Complexities of SQLite Benchmarking」で詳述されているように、このコンテキストが欠けているベンチマーク結果は、結果を再現することも、より大きなベンチマークコンテキストに配置することもできません。

これは特にSQLiteに当てはまり、設定を1箇所変更するだけでパフォーマンスが10倍以上向上する可能性があります。ここでは、さまざまな設定がパフォーマンスにどのように影響するかに関心があるので、このコンテキストは不可欠です。

ここでは、以下の4つのシナリオでSQLiteをベンチマークします:

  1. 「デフォルトのコンパイルオプションを用いたsqlite3-ruby」「PRAGMA微調整なし」
  2. 「デフォルトのコンパイルオプションを用いたsqlite3-ruby」「PRAGMA微調整あり」
  3. 「推奨のコンパイルオプションを用いたsqlite3-ruby」「PRAGMA微調整なし」
  4. 「推奨のコンパイルオプションを用いた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_modesynchronizationjournal_size_limitプラグマの設定が必要不可欠であることは明らかです。また、コンパイルチューニングは、プラグマチューニングのような2倍の改善は得られませんが、今では非常に簡単にできるので、3~5%を無駄にするのはもったいないですよね。

せっかくなので、これらのさまざまなSQLite 設定が、ノートPC上の同じベンチマークでPostgreSQLをローカル実行する場合とどの程度パフォーマンスが違ってくるかも見てみたいと思いました。

pg gemの微調整方法についてはよく知らないので、Macbook Proにデフォルトでインストールされたpg(gemバージョン1.5.4、PostgreSQLバージョン14.0.9)でベンチマークを1回だけ実行しました。

ged/ruby-pg - GitHub

結果は次のとおりです: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のパフォーマンスが実証されることを願っています。

関連記事

SQLite on Railsシリーズ(01)Gitブランチごとにデータベースを切り替える(翻訳)

SQLite on Railsシリーズ(02)SQLiteをチューニングで強化する(翻訳)

Railsキャッシュストアのベンチマーク比較: Redis vs 他のデータストア(翻訳)


  1. 原注: ここで言う「関連(relevant)」とは、PRAGMAのサブセットを意味します。つまり、SQLiteドキュメントで提供されている完全なセットを取得してから、「非推奨のプラグマ」「特殊なプラグマ」「内部データプラグマ」を除外して生成されたものです。その結果、40個の「設定」プラグマのリストが残りました。完全なリストについては、SQLite をPRAGMAで微調整する過去記事に記載されています。 
  2. 原注: ここで使った調整済みベンチマークはこのGistで参照できます。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。