🔖

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種類のクエリを検証しました:

  1. SelectByID: SELECT id, name, hp, attack FROM monster WHERE id = ?
  2. SelectAll: SELECT id, name, hp, attack FROM monster

検証パターン

全部で9パターンの設定を試しました:

  1. P1: Default - ドライバーのデフォルト設定(cache=shared, read only)
  2. P2: P1 + WAL - WALジャーナルモード有効化
  3. P3: P2 + Immutable - P2の設定に_immutable=1を追加
  4. P4: P3 + mmap - P3の設定に_pragma=mmap_size(256MB)を追加
  5. P5: P2 + mmap + OpenMode最適化 - P2に_pragma=mmap_size(256MB)mode=ro+shared+nomutexを追加
  6. P6: P5 + PRAGMA最適化① - P5に_pragma=locking_mode(EXCLUSIVE)_pragma=synchronous(OFF)を追加
  7. P7: P6 + PRAGMA最適化② - P6に_pragma=temp_store(MEMORY)_pragma=cache_size(10000)を追加
  8. P8: P7 + ANALYZE - P7に_pragma=analysis_limit(1000)_pragma=optimizeを追加
  9. 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が高並列環境で優れたパフォーマンスを示す理由はいくつか考えられます:

  1. Pure Go実装のメリット:CGO呼び出しのオーバーヘッドがなく、Goのgoroutineスケジューラとの親和性が高い
  2. メモリ管理の最適化:Goのガベージコレクションと連携した効率的なメモリ管理
  3. 内部的な並列処理の最適化:SQLiteの並列アクセス制限を克服する実装
  4. メモリDBとの相性:メモリDBの特性を活かした設計

とくに1000並列環境でのSelectByIDで著しい性能向上が見られたのは、modernc.org/sqliteがメモリDB上での高並列アクセスに特化した最適化がされている可能性があります。(SQLiteの内部実装に依存する部分もあるため、正確な理由は不明ですが)

まとめ

Go言語のSQLiteドライバー3種のパフォーマンス検証から、以下のことがわかりました:

  1. modernc.org/sqliteが総合的に優秀:とくに高並列環境で圧倒的なパフォーマンス
  2. メモリDBの効果は劇的:適切なドライバーと設定で最大3倍の高速化
  3. 高負荷環境ではPure Go実装が有利:CGOのオーバーヘッドなしでスケーラビリティ向上
  4. 適切なPRAGMA設定が重要:ジャーナルモード、同期モード、キャッシュサイズなどの最適化

SQLiteはシンプルなRDBMSとして知られていますが、適切なドライバーと設定を選ぶことで、高負荷なプロダクション環境でも十分な性能を発揮できることがわかりました。
とくにmodernc.org/sqliteのメモリDB(P9)設定は、高並列環境で驚異的なパフォーマンスを示し、実用的な選択肢となりえます。

コードとベンチマーク詳細はGitHubで公開しています:

参考資料

GitHubで編集を提案

Discussion