Go SQLiteドライバーのパフォーマンス比較検証 〜メモリDBで3倍高速化も〜
とあるサービスの負荷試験をした際に SQLite の SELECT 性能で苦しめられたことがありました。
その時は時間的制約と自分の知識が足りず、SQLiteのパフォーマンスを改善しきることができませんでした。
今回はAIの力を借りて、SQLiteのパフォーマンスを改善する方法をいくつか検証してみました。
実際に使用したのはGo言語のSQLiteドライバー3種です。
-
mattn/go-sqlite3
(C言語バインディング) -
modernc.org/sqlite
(Pure Go実装) -
glebarez/go-sqlite
(Pure Go実装)
※注意:
- この検証は2025年5月時点のものであり、SQLiteやドライバーのバージョンによって結果が異なる可能性があります
- また厳密なベンチマークではなく、あくまで参考程度にしてください
- それぞれのドライバーには独自の特性があり、使用する環境や要件によって最適な選択肢は異なることを理解してください
- この記事はあくまで個人的な学びの記録であり、特定のドライバーや実装を推奨するものではありません
検証環境と方法
検証環境
- OS: darwin
- Arch: arm64
- CPU: Apple M3 Max
- Go Version: 1.23.1
検証対象とデータ
対象テーブルは1000レコードを持つmonster
テーブルです。
CREATE TABLE monster (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hp INTEGER NOT NULL,
attack INTEGER NOT NULL
);
以下の2種類のクエリを検証しました:
-
SelectByID:
SELECT id, name, hp, attack FROM monster WHERE id = ?
-
SelectAll:
SELECT id, name, hp, attack FROM monster
検証パターン
全部で9パターンの設定を試しました:
- P1: Default - ドライバーのデフォルト設定(cache=shared, read only)
- P2: P1 + WAL - WALジャーナルモード有効化
-
P3: P2 + Immutable - P2の設定に
_immutable=1
を追加 -
P4: P3 + mmap - P3の設定に
_pragma=mmap_size(256MB)
を追加 -
P5: P2 + mmap + OpenMode最適化 - P2に
_pragma=mmap_size(256MB)
とmode=ro+shared+nomutex
を追加 -
P6: P5 + PRAGMA最適化① - P5に
_pragma=locking_mode(EXCLUSIVE)
と_pragma=synchronous(OFF)
を追加 -
P7: P6 + PRAGMA最適化② - P6に
_pragma=temp_store(MEMORY)
と_pragma=cache_size(10000)
を追加 -
P8: P7 + ANALYZE - P7に
_pragma=analysis_limit(1000)
と_pragma=optimize
を追加 - P9: メモリDB - ファイルDBからデータをメモリにコピーして完全にインメモリで実行
用語の説明
- WAL: データベースのジャーナルモードのひとつで、書き込み操作をログに記録してからデータベースに反映する方式。並列読み取り性能が向上する。
- Immutable: データベースが変更されないことを保証する設定。読み取り専用のデータベースで使用され、パフォーマンスが向上する。
- mmap: データベースファイルをメモリにマッピングすることで、I/O性能を向上させる技術。
- OpenMode: データベースを開く際のモード設定。読み取り専用や共有モードなどを指定できる。
- PRAGMA: SQLiteの設定を変更するための特殊なSQLコマンド。キャッシュサイズや同期モードなどを調整可能。
- ANALYZE: データベースの統計情報を収集し、クエリプランの最適化に利用するコマンド。
ベンチマーク方法
各パターンについて、go test -bench=. -benchmem -count=5
で5回実行して平均を取りました。
また並列性の影響を見るために、100/1000 goroutineでのベンチマークを行いました。
// ベンチマーク関数の例
func BenchmarkModerncP9_SelectByID_100(b *testing.B) {
runParallelSelectByID(b, dsnModernc(9), 100)
}
メモリDBの実装方法
P9(メモリDB)の検証では、以下のように実装しています。
// メモリDBを作成し、ファイルDBからデータをコピーする
func setupMemoryDB(b *testing.B) *sql.DB {
// ファイルDBに接続
fileDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro&cache=shared", dbFileName))
if err != nil {
b.Fatalf("Failed to open file DB: %v", err)
}
defer fileDB.Close()
// メモリDBを作成
memDB, err := sql.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
b.Fatalf("Failed to open memory DB: %v", err)
}
// ファイルDBをATTACHする
_, err = memDB.Exec(fmt.Sprintf("ATTACH DATABASE '%s' AS filedb", dbFileName))
if err != nil {
memDB.Close()
b.Fatalf("Failed to ATTACH filedb: %v", err)
}
// テーブルをメモリにコピーする
_, err = memDB.Exec(`
CREATE TABLE monster AS SELECT * FROM filedb.monster
`)
if err != nil {
memDB.Close()
b.Fatalf("Failed to COPY table: %v", err)
}
// ATTACH解除
_, err = memDB.Exec("DETACH DATABASE filedb")
if err != nil {
memDB.Close()
b.Fatalf("Failed to DETACH filedb: %v", err)
}
// 最適化のためのPRAGMA設定
pragmas := []string{
"PRAGMA journal_mode=OFF",
"PRAGMA synchronous=OFF",
"PRAGMA cache_size=10000",
"PRAGMA locking_mode=EXCLUSIVE",
"PRAGMA temp_store=MEMORY",
}
for _, pragma := range pragmas {
_, err = memDB.Exec(pragma)
if err != nil {
memDB.Close()
b.Fatalf("Failed to set PRAGMA: %v", err)
}
}
return memDB
}
メモリDBは:memory:
を使いつつ、高速化のために以下のPRAGMAを設定しています:
- journal_mode=OFF(ジャーナル無効化)
- synchronous=OFF(同期処理無効化)
- cache_size=10000(キャッシュサイズ増加)
- locking_mode=EXCLUSIVE(排他ロックモード)
- temp_store=MEMORY(テンポラリストレージをメモリに)
ベンチマーク結果
SelectByID (100並列/1000並列)
Pattern | Driver | Options | Avg ns/op (100) | Avg B/op (100) | Avg allocs/op (100) | Avg ns/op (1000) | Avg B/op (1000) | Avg allocs/op (1000) |
---|---|---|---|---|---|---|---|---|
P1 | mattn/go-sqlite3 |
cache=shared, read only | 23,715 | 967 | 36.2 | 33,601 | 1,246 | 42.6 |
P1 | modernc.org/sqlite |
cache=shared, read only | 16,898.2 | 697.2 | 25 | 49,398 | 1,619.4 | 38.8 |
P1 | glebarez/go-sqlite |
cache=shared, read only | 16,299 | 770 | 31 | 258,395.8 | 2,840.8 | 75.8 |
P2 | mattn/go-sqlite3 |
P1 + WAL | 21,166 | 958.6 | 36.8 | 30,177 | 1,174.2 | 42.4 |
P2 | modernc.org/sqlite |
P1 + WAL | 14,809.4 | 698.4 | 25.2 | 62,188.6 | 1,865.4 | 44.2 |
P2 | glebarez/go-sqlite |
P1 + WAL | 16,395.4 | 765 | 31 | 73,066.2 | 2,696 | 73.8 |
P3 | mattn/go-sqlite3 |
P2 + Immutable | 20,113.6 | 954.2 | 36.6 | 39,436.6 | 1,265.8 | 45.2 |
P3 | modernc.org/sqlite |
P2 + Immutable | 16,038.6 | 706.2 | 26 | 56,366 | 1,764.4 | 43.8 |
P3 | glebarez/go-sqlite |
P2 + Immutable | 17,325.4 | 767.4 | 31 | 154,430.8 | 2,819.8 | 77.4 |
P4 | mattn/go-sqlite3 |
P3 + Mmap | 19,586.2 | 953.8 | 36.4 | 44,945 | 1,329.8 | 47.2 |
P4 | modernc.org/sqlite |
P3 + Mmap | 17,059.4 | 702.8 | 26 | 70,149.2 | 1,997.8 | 51.2 |
P4 | glebarez/go-sqlite |
P3 + Mmap | 16,563.2 | 763.2 | 31 | 155,914.6 | 2,861.6 | 81.6 |
P5 | mattn/go-sqlite3 |
P2 + Mmap + OpenMode最適化 | 20,382 | 959 | 36.6 | 36,747 | 1,300 | 45.2 |
P5 | modernc.org/sqlite |
P2 + Mmap + OpenMode最適化 | 17,013 | 711 | 26 | 64,518 | 2,153 | 58.4 |
P6 | mattn/go-sqlite3 |
P5 + PRAGMA最適化① | 21,585 | 959 | 36.6 | 30,034 | 1,180 | 42.8 |
P6 | modernc.org/sqlite |
P5 + PRAGMA最適化① | 19,249 | 740 | 26.4 | 79,329 | 2,939 | 77.2 |
P7 | mattn/go-sqlite3 |
P6 + PRAGMA最適化② | 19,241 | 963 | 36.6 | 57,234 | 1,421 | 48.8 |
P7 | modernc.org/sqlite |
P6 + PRAGMA最適化② | 13,939 | 723 | 26 | 70,927 | 2,770 | 78.6 |
P8 | mattn/go-sqlite3 |
P7 + ANALYZE | 18,885 | 962 | 36.6 | 82,928 | 1,627 | 54.6 |
P8 | modernc.org/sqlite |
P7 + ANALYZE | 14,525 | 740 | 27 | 82,923 | 3,794 | 97 |
P9 | mattn/go-sqlite3 |
メモリDB | 19,534 | 964 | 36.6 | 46,748 | 1,405 | 47.4 |
P9 | modernc.org/sqlite |
メモリDB | 15,940 | 706 | 25 | 16,792 | 1,060 | 35 |
P9 | glebarez/go-sqlite |
メモリDB | 16,544 | 740 | 31 | 154,236 | 1,838 | 57 |
注: 上記の Avg ns/op
, Avg B/op
, Avg allocs/op
は5回の実行結果の単純平均です。
SelectAll (100並列/1000並列)
Pattern | Driver | Options | Avg ns/op (100) | Avg B/op (100) | Avg allocs/op (100) | Avg ns/op (1000) | Avg B/op (1000) | Avg allocs/op (1000) |
---|---|---|---|---|---|---|---|---|
P1 | mattn/go-sqlite3 |
cache=shared, read only | 1,150,724 | 105,552 | 6,950 | 1,073,780 | 105,837 | 6,969 |
P1 | modernc.org/sqlite |
cache=shared, read only | 681,935 | 122,911 | 8,915 | 605,996 | 123,072 | 8,935 |
P1 | glebarez/go-sqlite |
cache=shared, read only | 2,482,862 | 137,447 | 9,959 | 2,462,973 | 139,864 | 10,022 |
P2 | mattn/go-sqlite3 |
P1 + WAL | 1,080,779 | 105,131 | 6,953 | 1,058,356 | 105,788 | 6,971 |
P2 | modernc.org/sqlite |
P1 + WAL | 609,229 | 122,085 | 8,916 | 612,405 | 122,975 | 8,935 |
P2 | glebarez/go-sqlite |
P1 + WAL | 2,642,251 | 137,254 | 9,961 | 2,463,109 | 139,515 | 10,017 |
P3 | mattn/go-sqlite3 |
P2 + Immutable | 1,088,846 | 105,062 | 6,953 | 1,109,677 | 105,888 | 6,973 |
P3 | modernc.org/sqlite |
P2 + Immutable | 613,335 | 122,175 | 8,918 | 601,144 | 122,972 | 8,937 |
P3 | glebarez/go-sqlite |
P2 + Immutable | 2,463,061 | 137,314 | 9,962 | 2,426,073 | 139,548 | 10,019 |
P4 | mattn/go-sqlite3 |
P3 + Mmap | 1,080,392 | 105,104 | 6,954 | 1,115,784 | 105,808 | 6,973 |
P4 | modernc.org/sqlite |
P3 + Mmap | 615,536 | 122,184 | 8,922 | 589,299 | 123,149 | 8,944 |
P4 | glebarez/go-sqlite |
P3 + Mmap | 2,848,841 | 137,837 | 9,968 | 2,771,080 | 139,769 | 10,028 |
P5 | mattn/go-sqlite3 |
P2 + Mmap + OpenMode最適化 | 1,101,117 | 106,211 | 6,955 | 1,001,443 | 105,808 | 6,973 |
P5 | modernc.org/sqlite |
P2 + Mmap + OpenMode最適化 | 671,349 | 122,965 | 8,933 | 599,452 | 123,953 | 8,959 |
P6 | mattn/go-sqlite3 |
P5 + PRAGMA最適化① | 1,068,819 | 105,148 | 6,955 | 1,050,044 | 105,863 | 6,974 |
P6 | modernc.org/sqlite |
P5 + PRAGMA最適化① | 691,863 | 122,895 | 8,942 | 638,836 | 124,212 | 8,973 |
P7 | mattn/go-sqlite3 |
P6 + PRAGMA最適化② | 1,002,754 | 105,229 | 6,955 | 1,124,167 | 105,971 | 6,974 |
P7 | modernc.org/sqlite |
P6 + PRAGMA最適化② | 738,011 | 123,211 | 8,952 | 663,591 | 124,375 | 8,984 |
P8 | mattn/go-sqlite3 |
P7 + ANALYZE | 1,041,525 | 105,305 | 6,957 | 1,091,693 | 105,979 | 6,974 |
P8 | modernc.org/sqlite |
P7 + ANALYZE | 739,762 | 123,871 | 8,965 | 647,132 | 125,252 | 8,999 |
P9 | mattn/go-sqlite3 |
メモリDB | 915,191 | 104,893 | 6,954 | 1,033,372 | 105,836 | 6,978 |
P9 | modernc.org/sqlite |
メモリDB | 609,228 | 122,134 | 8,929 | 570,468 | 123,132 | 8,954 |
P9 | glebarez/go-sqlite |
メモリDB | 2,562,978 | 136,108 | 9,941 | 2,428,093 | 138,409 | 9,999 |
注: 上記の Avg ns/op
, Avg B/op
, Avg allocs/op
は5回の実行結果の単純平均です。
それぞれ最速のドライバーとパターンをまとめると以下のようになります:
SelectByID (単一レコード検索) 結果
ドライバー | 通常DB (100並列) | メモリDB (100並列) | 通常DB (1000並列) | メモリDB (1000並列) |
---|---|---|---|---|
mattn/go-sqlite3 |
23,715 ns/op | 19,534 ns/op (1.2倍速) | 33,601 ns/op | 46,748 ns/op (0.7倍遅) |
modernc.org/sqlite |
16,898 ns/op | 15,940 ns/op (1.1倍速) | 49,398 ns/op | 16,792 ns/op (2.9倍速) |
glebarez/go-sqlite |
16,299 ns/op | 16,544 ns/op (同等) | 258,395 ns/op | 154,236 ns/op (1.7倍速) |
modernc.org/sqlite
が1000並列時に驚異的な速度向上(約3倍)を達成しています!
SelectAll (全件検索) 結果
ドライバー | 通常DB (100並列) | メモリDB (100並列) | 通常DB (1000並列) | メモリDB (1000並列) |
---|---|---|---|---|
mattn/go-sqlite3 |
1,150,724 ns/op | 915,191 ns/op (1.3倍速) | 1,073,780 ns/op | 1,033,372 ns/op (1.0倍速) |
modernc.org/sqlite |
681,935 ns/op | 609,228 ns/op (1.1倍速) | 605,996 ns/op | 570,468 ns/op (1.1倍速) |
glebarez/go-sqlite |
2,482,862 ns/op | 2,562,978 ns/op (0.9倍遅) | 2,462,973 ns/op | 2,428,093 ns/op (1.0倍速) |
SelectAllではmodernc.org/sqlite
が全体的に最速です。メモリDBの効果は限定的ですが、それでもmodernc.org/sqliteが僅かに向上しています。
メモリDBの詳細検証
メモリDBでの検証で明らかになった興味深い発見をいくつか紹介します。
ドライバー別の特性
各ドライバーは同じメモリDB設定でも全く異なる挙動を示しました:
modernc.org/sqlite
(Pure Go実装)
- 高並列環境(1000並列)で圧倒的なパフォーマンス
- SelectByIDで2.9倍の速度向上(49,398ns→16,792ns)
- C実装の
mattn/go-sqlite3
よりも高速で、スケーラビリティに優れている - アロケーション回数も少なめで効率的(約25回/op)
// modernc.org/sqliteの高速な結果例
BenchmarkModerncP9_SelectByID_1000-12 69972 16792 ns/op 1060 B/op 35 allocs/op
mattn/go-sqlite3
(C言語バインディング)
- 単一スレッドでは最速だが、並列性が増えると性能低下
- 1000並列では逆に遅くなる(33,601ns→46,748ns)
- メモリアロケーションは
modernc.org/sqlite
よりやや多い
glebarez/go-sqlite
(Pure Go実装)
- 単一または少数並列では良好だが高並列では大幅に性能低下
- メモリDBで改善は見られるものの依然として遅い(154,236ns/op)
- メモリ使用量が3ドライバー中もっとも多い
高並列時のスケーラビリティ
modernc.org/sqlite
の際立った特徴は高並列環境でのスケーラビリティです。
並列数が100から1000に増えても性能がほとんど低下せず、むしろメモリDBでは向上しています。
これは高負荷なアプリケーションやマイクロサービスでの利用に大きなメリットをもたらします。
とくに以下のような場面でmodernc.org/sqlite
の優位性が活きてきます:
- 多数のユーザーが同時アクセスするWebアプリケーション
- キャッシュレイヤーとしてのSQLite利用
- 読み取り集中型のマイクロサービス
メモリ使用量とアロケーション
ドライバー | SelectByID B/op | SelectByID allocs/op | SelectAll B/op | SelectAll allocs/op |
---|---|---|---|---|
mattn/go-sqlite3 |
964 | 36.6 | 104,893 | 6,954 |
modernc.org/sqlite |
706 | 25.0 | 122,134 | 8,929 |
glebarez/go-sqlite |
740 | 31.0 | 136,108 | 9,941 |
modernc.org/sqlite
はアロケーション回数がもっとも少なく効率的です。
mattnは小さなクエリでのメモリ使用量がやや多く、glebatarezは全体的にメモリ使用量が多めです。
modernc.org/sqlite
が高並列で優れているのか?
考察:なぜ modernc.org/sqlite
が高並列環境で優れたパフォーマンスを示す理由はいくつか考えられます:
- Pure Go実装のメリット:CGO呼び出しのオーバーヘッドがなく、Goのgoroutineスケジューラとの親和性が高い
- メモリ管理の最適化:Goのガベージコレクションと連携した効率的なメモリ管理
- 内部的な並列処理の最適化:SQLiteの並列アクセス制限を克服する実装
- メモリDBとの相性:メモリDBの特性を活かした設計
とくに1000並列環境でのSelectByIDで著しい性能向上が見られたのは、modernc.org/sqlite
がメモリDB上での高並列アクセスに特化した最適化がされている可能性があります。(SQLiteの内部実装に依存する部分もあるため、正確な理由は不明ですが)
まとめ
Go言語のSQLiteドライバー3種のパフォーマンス検証から、以下のことがわかりました:
-
modernc.org/sqlite
が総合的に優秀:とくに高並列環境で圧倒的なパフォーマンス - メモリDBの効果は劇的:適切なドライバーと設定で最大3倍の高速化
- 高負荷環境ではPure Go実装が有利:CGOのオーバーヘッドなしでスケーラビリティ向上
- 適切なPRAGMA設定が重要:ジャーナルモード、同期モード、キャッシュサイズなどの最適化
SQLiteはシンプルなRDBMSとして知られていますが、適切なドライバーと設定を選ぶことで、高負荷なプロダクション環境でも十分な性能を発揮できることがわかりました。
とくにmodernc.org/sqlite
のメモリDB(P9)設定は、高並列環境で驚異的なパフォーマンスを示し、実用的な選択肢となりえます。
コードとベンチマーク詳細はGitHubで公開しています:
Discussion