プログラミング
SQLiteがあれば十分
SQLite Is All You Need (dbpro.app)
要約
この記事では、多くの開発者が習慣で利用しているPostgresのような重いデータベースの代わりに、SQLiteを本番環境のアプリケーションに活用できる可能性を探求しています。5万ユーザー、100万投稿を持つソーシャルネットワークをSQLiteで構築し、そのパフォーマンスベンチマーク、設定、限界について詳細に解説しています。結論として、多くのユースケースにおいてSQLite(WALモード)で十分であり、過剰なデータベース設定は不要であると主張しています。
全文翻訳
先週、Evan Hahn氏の「Prefer STRICT tables in SQLite」を読み、考えさせられました。SQLiteを、実際の型付けされた本番データを格納するデータベースとして扱っている人がいるのです。そこで当然の疑問が湧き上がります。それは、どこまでそれが通用するのか?ということです。私たちはそれを突き止めました。SQLite上にソーシャルネットワークを構築し、5万人のユーザーと100万件の投稿を単一の343MBのファイルに収め、Node APIの背後に配置し、Evan氏のアドバイスに従って全てのテーブルをSTRICTにし、数字がそれ以上良くならなくなるまで負荷をかけました。この記事は、その結果として得られたベンチマーク、設定、コード、そして限界についてです。要するに、1つのファイル、1つのプロセスで、アプリケーションの中で最も負荷の高いページがラップトップ上で1日3億1500万リクエストを処理しました。あなたが構築しているほとんどのアプリケーションにとって、WALモードのSQLiteで十分であり、習慣で起動したPostgresコンテナは必要なかったのです。議論ではなく証拠を見たい場合は、直接ベンチマークに進んでください。限界もここにあり、それは現実のものです。
私たちが構築したもの
ソーシャルアプリは、ハードなクエリを避けられないため、良いストレステストになります。ホームタイムラインは、あなたが投稿したものと私がフォローしているものを結合し、時間順に並べ替え、いいねの数を数える必要があります。最初の要求でキャッシュで回避することはできませんし、グラフが成長するにつれて遅くなります。そこで、Chirp、1つのファイルに存在するソーシャルネットワークです。
テーブル
行
ユーザー 50,000
投稿 1,000,000
フォロー 2,498,799
いいね 4,999,764
合計 343 MB 1つのファイルに
全てのテーブルはSTRICTであり、これについては後述します。バックエンド全体は、隣にあるchirp.dbと通信する単一のNodeプロセスです。データベースサーバーなし、接続文字列なし、ポート5432なし、コンテナなし。本番環境の設定全体は、5つのpragmaです。
JAVASCRIPT
const db = new Database('chirp.db');
db.pragma('journal_mode = WAL'); // リーダーとライターがお互いをブロックしないように
db.pragma('synchronous = NORMAL'); // コミットごとではなくチェックポイントでfsync
db.pragma('busy_timeout = 5000'); // 書き込みロックを待つのではなく、スローする
db.pragma('foreign_keys = ON'); // デフォルトでオフ、これは人々を驚かせる
db.pragma('cache_size = -64000'); // 64MBのページキャッシュ
これが全体のセットアップです。プロビジョニングするステップはありません。
数字
以下は全て、Apple M1ラップトップ(8コア、16GB RAM)で、Node 22とbetter-sqlite3(SQLite 3.49.2)を使って実行されました。サーバーではありません。ラップトップです。
まず、実際のHTTP経由のエンドツーエンドのパフォーマンスです。実際のNodeサーバー、実際のソケット、実際のJSONシリアライゼーション、50の同時接続、相手側はautocannonです。これはスタック全体であり、データベース単体ではありません。全ての数値は4回の実行の中央値であり、実行ごとのばらつきは約10%です。
エンドポイント
req/s
50p99
エラー
GET /post/:id (ポイントリード)
51,427
0ms
1ms
0
GET /u/:handle (プロフィール)
47,776
0ms
2ms
0
GET /timeline/:id (重いもの)
3,543
13ms
27ms
0
Mixed: 95% タイムラインリード、5% 書き込み
3,654
13ms
27ms
0
最後の行をしばらく見てください。これはアプリケーションの中で最も負荷の高いクエリであり、250万件のフォローと100万件の投稿を結合し、全ての結果に対するいいねの数を数え、ライブ書き込みと並行して、単一のNodeプロセスからラップトップ上で処理されています。秒間3,654リクエストは、1日あたり3億1500万リクエストです。最も負荷の高いエンドポイントで。ポイントリードは44億を処理するでしょう。もしあなたの製品が1日あたり3億1500万件のタイムラインロードを処理しているなら、あなたはトップ0.01%におり、すでに自分が誰であるかを知っているでしょう。それ以外の人々は、ファイルに収まるワークロードのためにコネクションプーラーについて議論しています。
HTTPレイヤーの下では、生のクエリの数値は以下のようになります。各ベンチマークは、新しいプロセスで、データベースの新しいコピーに対して3回実行され、中央値を取ります。
クエリ
ops/s
50p99
ポイントリード (IDによる投稿)
232,011
0.004ms
0.007ms
プロフィールページ (2つの集計)
175,850
0.005ms
0.007ms
ホームタイムライン (20投稿 + いいね数)
4,247
0.232ms
0.337ms
投稿の挿入 (各トランザクション)
23,459
0.012ms
0.089ms
投稿へのいいね (各トランザクション)
12,618
0.016ms
0.243ms
投稿の挿入 (バッチ処理、トランザクションあたり100件)
32,217 行/秒
そこでの全ての書き込みは、実際のコミットされた耐久性のあるトランザクションです。バッチでも、バッファでも、キューでもありません。秒間2万3千件のコミットされたトランザクションが、ラップトップ上で、外部キーが有効な状態で実行されています。
WALが重要な部分です
SQLiteに対する古い反論は、ロックがかかることです。1人のライターがデータベースを占有し、他の全員が待ちます。その反論はロールバックジャーナルに関するもので、2010年以来ほとんどのアプリで間違ったデフォルトとなっています。Write-Ahead Logging (WAL) は、問題の形状を変えます。ライターはページをインプレースで変更する代わりにログに追記するため、書き込みが進行中でもリーダーは最後にコミットされたスナップショットを読み続けます。リーダーはライターをブロックしません。ライターはリーダーをブロックしません。
私たちはそれを主張するのではなく、テストしました。7つのリーダー・スレッドがタイムラインクエリを実行します。まず単独で、次に現実的な毎秒1,000件の書き込みを行うライターの隣で、次にフルスピードで動作するライターの隣で。違いがわかるように、WALモードと古いロールバックジャーナルモードで同じテストを実行しました。
journal_mode = WAL
シナリオ
reads/s
99p
worst read
SQLITE_BUSY
リーダーのみ
17,581
1.53ms
7ms
0
リーダー + 毎秒1,000件の書き込み
2,792
4.40ms
17ms
0
リーダー + フルスピードのライター (毎秒14,838件の書き込み)
2,854
5.16ms
30ms
0
journal_mode = DELETE (ロールバックジャーナル、人々が覚えているもの)
シナリオ
reads/s
99p
worst read
SQLITE_BUSY
リーダーのみ
18,439
1.23ms
6ms
0
リーダー + 毎秒1,000件の書き込み
497
133.85ms
794ms
0
リーダー + フルスピードのライター (毎秒2,806件の書き込み)
227
586.02ms
1,762ms
0
同じクエリ、同じデータ、同じマシンです。1つのpragmaの違いです。毎秒1,000件の書き込みを実行するライターがいる場合、WALは読み取りスループットの5.6倍を提供し、99パーセンタイルを4.40msに保ちます。ロールバックジャーナルはp99で133msに低下し、個々の読み取りは800ミリ秒近く停止します。これがSQLiteについて人々が不満を言うものであり、それは10年前のデータベースです。WALの下では、全てのシナリオで、SQLITE_BUSYエラーはゼロでした。「少数」ではなく、ゼロです。
数字が止まる場所
この記事に良い表だけが含まれていたら、信頼すべきではありません。SQLiteを不利にしない発見をここに示します。何かが書き込んでいると、読み取りのスケーリングは停止します。ライターがいない7つのリーダー・スレッドは、毎秒17,581件の読み取りを実行します。毎秒1,000件の書き込みしか行わないライターを追加すると、読み取りは2,792件に低下します。これは6.3倍の低下であり、毎秒14,000件の書き込みに増やしても意味のある悪化は見られません。これは書き込み量ではなく、キャッシュの無効化の問題であることを示しています。それらのリーダーは、256MBのメモリマップドウィンドウとウォームなページキャッシュから速度を得ていました。コミットごとにマップされたページが無効化されるため、リーダーは実際のI/Oと再検証にフォールバックします。
設定をスキャンして確認しました。mmapをオフにすると、読み取り専用の数値は毎秒17,069件から毎秒6,034件に低下し、同時ライターからのペナルティはほとんど消えます。17,581という数値は読み取り専用の成果です。正直な混合ワークロードの数値は、マシンあたり約2,800件の最も負荷の高いクエリの読み取り/秒であり、これが私たちが議論の基盤としたものです。
1人のライター、グローバルに。
SQLiteはデータベース全体に対して単一の書き込みロックを取ります。書き込みは並列実行されず、キューイングされます。秒間23,000件のコミットされたトランザクションで、そのキューは速く空になりますが、それはキューであり、いくらハードウェアを増やしても2つのキューにはなりません。1台のマシンです。フェイルオーバーはありません。ボックスが故障した場合、復旧するまでダウンしており、復旧時間はファイルのリストアにかかる時間です。多くの製品にとって、これは運用上のシンプルさに対する完全に許容できるトレードオフです。一部の製品にとってはそうではなく、規制産業でSLA(サービスレベルアグリーメント)がある場合は、どちらの選択肢かを知っているでしょう。
Postgresに手を伸ばすべき時
同じ行に対して複数のライターが競合する場合、リードレプリカや自動フェイルオーバーが必要な場合、数億行に対する実際の分析エンジンが必要な場合、またはチームが拡張機能のエコシステムを本当に必要とする場合です。それらは正当な理由です。「いつかスケールするかもしれない」は理由の一つではなく、これらのコンテナのほとんどが存在する理由です。
「しかしラップトップでテストしたのでしょう?」
公平です。M1は2026年の基準では高速なマシンではありませんが、$6のVPSでもありません。そして、これらの数字がApple Silicon上でしか存在しないのであれば、議論全体が成り立たなくなります。私たちはボックスをレンタルして再実行しなかったため、以下は推定値です。