プログラミング
SQLiteではなくDuckDBを選択する
Choose DuckDB rather than SQLite (tracewayapp.com)
要約
この記事では、観測データ処理におけるDuckDBとSQLiteのパフォーマンスを比較しています。DuckDBはSQLiteと比較して、書き込み速度が3倍から15倍、ダッシュボードの読み込み性能が100倍向上し、10億件のメトリックポイントを10.8GBに格納できることが示されています。これにより、小規模なサーバーでも大量のデータを自己ホストできるようになります。
全文翻訳
エンジニアリング
SQLite vs DuckDB
同じ16ドルのサーバーで、すべてのクリフが100倍に
Jovan Stojiljkovic・2026年7月21日
同じ月額16.49ドルのサーバー、同じTracewayバイナリ、2つの組み込みデータベース。DuckDBはSQLiteより4倍から15倍高速に書き込み、100倍の行数でダッシュボードを提供し、10億件のメトリックポイントを10.8GBに格納します。完全な数値と方法論は内部にあります。
要約
私は6日間かけて、DuckDB上の観測データのベンチマークを作成・実行しました。前回のブログ記事ではSQLiteで行い、今回は安価なCCX13 HetznerインスタンスでDuckDBがどのように比較されるかを本当に知りたかったのです。測定方法は、DuckDBをTracewayの有効なストレージエンジンとして実装し、そのベンチマークスイートを実行することでした。ベンチマークによると、DuckDBのカラム型エンジンは、バックエンドに大きな変更を加えることなく、100倍のデータポイントをクエリできます。書き込みスループットも3倍から15倍高くなります。その結果、小規模なサーバーで大量のデータを処理できる完全なOTelスタックを自己ホストできるようになります。測定方法については、続きを読むとわかります。
これが結果です。
信号 SQLite (投稿2) DuckDB (この記事) 変化
Metrics書き込み 61,712 pts/sec 254,242 pts/sec 4x
Spans書き込み 30,508 spans/sec 95,737 spans/sec 3x
Logs書き込み 4,877 rec/sec 75,225 rec/sec 15x
Metrics読み込みクリフ 1M rows (median 3.85 s) 100M rows (median 3.0 s) 100x
Spans読み込みクリフ 100k rows (median 2.27 s) 10M rows (median 902 ms) 100x
Logs読み込みクリフ 100k rows (median 114 ms) 10M rows (median 85 ms) 100x
読み込みクリフとは、このシリーズ全体を通して、実際のダッシュボードページがまだロードされる最大のテーブルサイズのことです。これは、3つのエンドポイントプローブの中央値が5秒以下で、タイムアウトがないことを意味します。その名前は、その10倍先に何があるかから来ています。クエリは遅くなるのではなく、応答しなくなります。各シグナルの読み込みクリフは、SQLiteよりもDuckDBの方が正確に100倍遠くにあり、同等以上のレイテンシで、同じハードウェア上で実行されます。私は、この対称性を生のJSONに対して2回確認してから信じました。このサーバーは、1時間以内に10億件のメトリックポイントを10.8GBのディスクに投入し、稼働し続けました。これは、SQLiteのベンチマークが一度も近づけなかった規模です。ログは、投稿2でSQLiteには全く向かないと言われたシグナルですが、DuckDBの最大の勝利です。
実際に比較されているもの
投稿2ではClickHouseとの比較を約束していましたが、その投稿はまだ公開されていません。しかし、私がそれに座るたびに、同じ質問が頭をよぎりました。クライアントサーバー型のOLAPデータベース(独自のコンテナ、独自のメモリ消費、独自の障害モードを持つ)に手を伸ばす前に、組み込み型データベースはどこまで使えるのか?TracewayはDuckDBテレメトリバックエンドをオプションビルドとして出荷しています。これは、SQLiteビルドと同じ単一バイナリ、同じデプロイメントストーリーで、テレメトリテーブルの下に異なるストレージエンジンを使用しています。もしあなたが1台の安価なサーバーで自己ホストする場合、この2つのビルドがあなたの目の前にある実際の選択肢であり、私はそれに関する数値を公開している人を見つけることができませんでした。そこで、投稿2の全手法をDuckDBビルドに対して実行し、結果を並べてみました。
計画は以下の通りです。
まず方法論、投稿2以降にハーネスで変更された点と、これらの数値が依存するバックエンドの1つの修正を含めます。次に書き込み、3つのシグナルすべてをSQLiteベースラインに対して行います。次に読み込み、同じ形状です。次に10億行の実行、クリフを越えたクエリのコスト、両ビルドのフリート計算、そして実際に実行するビルドについて説明します。
測定方法
セットアップは投稿2と同じなので、簡単に説明します。テスト対象システム(SUT)はTracewayバイナリを実行し、プライベートリンク上の別のロードジェネレーターは、両方ともHetznerのニュルンベルクデータセンターにあり、OTLP(OpenTelemetryワイヤープロトコル)、gzip圧縮されたprotobuf経由で、バウンドされた範囲内の均一ランダムデータを使用します。Tracewayはコミット14b4aa6eからビルドされ、Ubuntu 24.04、ベンチマーク中はリテンションオフです。
各シグナルにつき2つのシナリオ。
スループットランプ:固定レートでのバッチサイズランプ、次に勝利したバッチでのリクエストレートランプ(今回は、興味深いクリフが投稿2の粗いステップ間に位置することが判明したため、より細かいレートラダー、1〜25 req/secを使用)、次にSDKフリートのような形状の小バッチ高レートランプ。ステップは5%未満のエラーで、ターゲットレートの70%以上が達成された場合に合格とします。
読み込みプローブ:テーブルを1M、10M、100M、1B、次に5B行まで満たし、各レベルで各シグナルの3つの実際のダッシュボードエンドポイントをロードします。レベルは中央値が5秒以下で、何も6秒のタイムアウトにヒットしない場合に合格とします。5秒のバーは意図的に寛大であり、投稿2でのその議論は変更なく適用されます。
投稿2のハーネスから3つの点が異なります。すべて開示します。
ロードジェネレーターが成長しました。投稿2では、2番目のCCX13から負荷を生成しましたが、SQLiteの天井はそれを決してストレスしませんでした。最初のDuckDBスパン実行では、データベースが0%のエラーで稼働している間にロードジェネレーターボックスがランプの途中でクラッシュしました。トラフィックジェネレーターは、それが壊しているものの前に壊れました。ロードジェネレーターは現在CCX23です。SUT、つまりすべての数値が説明するボックスは変更されていません。
消化ゲート。DuckDBは、インジェストが停止した後にライトアヘッドログをチェックポイントします。その間にプローブすると、クエリではなくビジーなエンジンを測定することになります。読み込みプローブは現在、データベースとWALファイルサイズが安定するまでバックエンドのディープヘルスエンドポイントをポーリングし、各レベルの待機時間を記録します(最終実行では各レベルで5秒)。その後、プローブします。SQLiteはエンジンのゲージを報告せず、待機をスキップするため、投稿2の数値は同様に生成されました。
受け入れられた行のみをカウントします。フィルカウントは、試行されたアイテムではなく、2xx応答によって確認されたアイテムをカウントします。これは、バックエンドが作業を拒否することを学習すると重要になります。フィルはターゲットを超過し、最小レベルでは最大2.6倍になります。そのため、差が重要になる場合は実際の行数を引用します。
注意点。これらはシングルショット実行です。各シグナルにつき1回のスループット実行と1回の読み込みラダーです。投稿2では、この記事から3回の繰り返しの中央値を約束していましたが、それをもう一度撤回します。以下の修正・再実行サイクルで予算を使い果たしてしまい、それらを保留にするよりも、その告白とともにシングルショットの数値を公開する方が良いと考えました。主要なセルはデバッグ実行全体で再現されました(異なる日の4回の実行で、100Mメトリクス読み込みは2.7秒から3.2秒の間でした)が、プロトコルはシングルショットです。「合格」は、アイドル状態のボックスで新しく書き込まれたデータに対する3つの名前付きエンドポイントを意味します。読み込み中のインジェストは今後の作業です。
もう1つの開示事項は、ハーネスではなくバックエンドに関するものです。最初のDuckDB実行では、信頼できない数値が得られました。ボックスはベンチマーク中にクラッシュし続け、その原因は私のインジェストパスの実際のバグであり、アドミッションコントロールがなく、持続的なファットバッチのバーストがプロセスをメモリ不足に追い込んでいました。私は、同時処理を制限し、過負荷に503とRetry-Afterで応答するインジェストゲート(上記のコミットを参照)でそれを修正し、すべてを再実行しました。この記事のすべての数値は修正されたバックエンドからのものです。修正前の結果はリポジトリの履歴でまだ見られますが、それらはそれ以前のものであり、DuckDBを過小評価しています。スパンは23%、ログはほぼ半分です。なぜなら、クラッシュがランプを短縮していたからです。SQLiteはバグを表面化させませんでした。その遅い挿入パスは圧力下で早期にエラーを発生させます。これは、投稿2の5 req/secのログウォールがその役割を果たしているからです。
書き込み:4倍、3倍、15倍
信号 SQLite DuckDB 勝利形状 (DuckDB) 最低失敗ステップ
Metrics 61,712/sec 254,242/sec バッチ 16384 × 20 req/sec, p50 3.2 s 22.5 req/sec, 12.2% エラー
Spans 30,508/sec 95,737/sec バッチ 16384 × 7.5 req/sec, p50 1.2 s 8.75 req/sec, 9.7% エラー
Logs 4,877/sec 75,225/sec バッチ 16384 × 5 req/sec, p50 3.0 s 5.625 req/sec, 11.4% エラー
当初、このテーブルに対する私の静かな目標は「SQLiteの2倍になれば勝ち」というものでした。そして、私が最も懸念していたのはログでした。投稿2の最も顕著な結果は、ログが4,877/secで書き込まれ、5 req/secで壁にぶつかり、1.5倍の要求でエラー率が0%から98%に跳ね上がったことでした。その壁が最も変化したセルです。DuckDBビルドは、SQLiteの15倍の速度でログを書き込み、壁は丁寧な拒否のスロープに変わりました。SQLiteの天井が最も低く、実際のボリュームが最も高いシグナルは、カラム型エンジンが最も役立つものであり、これは後知恵で明らかなのか、それとも投稿2が訓練したすべてから逆算されたものなのかはわかりません。