概要
- データベース は本質的に ファイル操作 で成り立つ
- ファイル直書きとデータベース利用の パフォーマンス比較 を実施
- 小規模アプリケーションでは 独自ファイル管理 も十分実用的
- データベースが 必要になる条件 を明確化
- ベンチマーク結果に基づいた 選択指針 を解説
データベースとファイルの本質
- SQLite は単一ファイル、 PostgreSQL はディレクトリ+プロセス構成
- どのデータベースも ファイルシステム への読み書きで動作
- 問題は「ファイルを使うか」ではなく「 誰のファイルを使うか」という選択
- 多くの初期段階アプリでは 自前ファイル管理 も十分に機能
- DB Proのような 専用クライアント もあるが、導入判断は規模次第
ストレージ戦略の比較
- 三つのファイル(users.jsonl, products.jsonl, orders.jsonl) を用意
- 各ファイルは JSONL形式 (1行1レコード)で管理
- エンドポイント はPOST /users(作成)とGET /users/:id(取得)のみ
- アプローチ1:毎回ファイル全体を読む
- リクエストごとにファイル全スキャン、 O(n) の計算量
- ファイルが大きくなるほど遅くなる
- アプローチ2:メモリ上に全データをロード
- 起動時に全件を ハッシュマップ へ格納、書き込みは両方に
- 読み込みは O(1)、高速化・並列化も容易
- アプローチ3:ディスク上でバイナリサーチ
- ファイルを ID順ソート +固定長インデックスを併用
- O(log n) のディスクアクセスで目的レコードに到達
- 追記時はインデックス再構築や LSM-tree的マージ が必要
ベンチマーク結果
- データセット:10k, 100k, 1Mレコード でwrk負荷テスト
- Go, Bun(JavaScript), Rust の3言語で実装・比較
- Go追加検証 :ディスクバイナリサーチ&SQLite(純Go実装)
- 主な結果(1Mレコード時のリクエスト/秒)
- Go: 線形スキャン23、バイナリサーチ38,866、SQLite 25,085、メモリマップ97,829
- Bun: 線形スキャン19、メモリマップ105,367
- Rust: 線形スキャン152、メモリマップ169,106
- 平均レイテンシ(1Mレコード時)
- Go: 線形スキャン1,010ms、バイナリサーチ1.4ms、SQLite 2.1ms、メモリマップ584µs
- Bun: 線形スキャン1,060ms、メモリマップ463µs
- Rust: 線形スキャン753ms、メモリマップ221µs
考察と選択指針
- 線形スキャンは規模とともに劣化、1M件で実用不可レベル
- ディスクバイナリサーチは高速かつ安定、データ増加にも強い
- SQLiteは常に安定したパフォーマンス、機能も豊富
- メモリマップは最速・最小レイテンシ、ただしRAM依存
- BunはGoよりメモリマップで高速、Rustは線形スキャンで圧倒的
- 用途別最適解
- 絶対速度:Rustメモリマップ
- RAM非依存の高速:Goバイナリサーチ
- SQLクエリ重視:SQLite
- 最短開発:Go線形スキャン
25,000リクエスト/秒の意味
- 25,000 req/s は大規模Webサービス級の負荷
- ピーク時は平均の2倍程度(例:12,500 req/s平均ならピーク25,000 req/s)
- 1ユーザーあたり1時間10回DBアクセス、ピーク同時接続10%想定
- 例:10,000人SaaS利用でピーク3 req/s、100,000人アプリで30 req/s
- 大半のアプリはベンチマーク値を大きく下回る負荷
データベースが本当に必要なケース
- データ量がRAMに収まらない場合
- メモリマップ方式は起動時全件ロードが必須
- 複数フィールドでの検索やJOINが必要
- ID以外の高速検索や複雑なクエリ
- 複数プロセスからの同時書き込み
- 外部一元管理が必須
- エンティティ間のアトミックな書き込み
- トランザクションやACID保証が必要
まとめ
- 多くの初期・小規模アプリケーションではファイル管理で十分
- 移行も容易(JSONLはどのDBにもインポート可)
- 必要になった時だけデータベース導入 で問題なし
- ベンチマークやサンプルコードも公開、実際に試せる環境提供