SSD Nodes Learn メモリ 8GB — 年額 $66
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-01

DuckDBとSQLiteの違いは?サーバーでの使い分け

SQLiteはトランザクション型のアプリ状態、DuckDBはParquetやCSVの分析に使います。同じVPSで両方を動かす理由を、実例付きで解説します。

サーバー上のDuckDBとSQLite:一文での回答

SQLiteはOLTPエンジン(オンライントランザクション処理)です。データを行として保存し、一度に少数の行を安全かつ高速に読み書きするよう設計されています。DuckDBはOLAPエンジン(オンライン分析処理)です。データを列として保存し、数百万行をスキャンして1つの集計結果を返すよう設計されています。どちらも組み込みライブラリで、通常のファイルを開きます。また、監視や管理が必要なサーバープロセスも実行しません。

したがって、「どちらを選ぶべきか」という問いへの正直な答えは、ほぼ常に「同じVPS上で両方」です。アプリケーションは稼働中の状態をSQLiteに保存します。レポート処理はDuckDBでParquetファイルとCSVファイルを読み取ります。両者は同じ役割を担っていないため、競合しません。

行ストレージと列ストレージによって答えが変わる理由

SQLiteは、1行をページ内の1つの連続した領域として書き込みます。主キーで1件の注文を取得すると、1つのインデックスページと1つのデータページにアクセスするため、読み取りは2回です。これは、アプリケーションが1秒あたり数千回実行する処理そのものです。ユーザーを読み取り、セッションを更新し、注文を挿入します。

DuckDBは、各列を個別に書き込み、圧縮します。500万行に対してamount_centsを合計すると、amount_cents列だけを読み取り、ファイル内の他のすべてのバイトをスキップし、値のバッチに対してベクトル化コードで合計を実行します。他の列はディスクから一切読み取られません。これが高速な理由です。

次に、それぞれのエンジンで相手のワークロードを実行します。SQLiteで列を合計する場合、1つのフィールドに到達するためにすべての行を走査し、ページから行全体を取り出す必要があります。そのため、必要以上に多くのディスクを読み取ります。DuckDBで1件の注文を挿入する場合、1つの値を書き込むために、すべての列のストレージにアクセスする必要があります。また、その処理のためにデータベースファイル全体に書き込みロックを取得します。どちらのエンジンにも不具合があるわけではありません。それぞれ、想定された用途とは異なる質問に答えているだけです。

SQLiteが有効な用途: トランザクション型のアプリケーション状態

書き込みが少量で頻繁に発生し、失われてはならない場合はSQLiteを選択します。セッション、注文、キューの行、設定など、Webリクエストによって作成されるデータが対象です。

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

同じ手順でテーブルを作成し、先行書き込みログを有効にします。

sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
  id           INTEGER PRIMARY KEY,
  customer     TEXT    NOT NULL,
  placed_at    TEXT    NOT NULL,
  amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
  VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQL

出力の1行目はwalです。これはモードが切り替わったことを示すPRAGMAであり、サーバーで最も有用な設定です。デフォルトのロールバックジャーナルモードでは、書き込み処理がすべての読み取り処理をブロックします。WALモードでは、1つの書き込み処理が追記している間も、読み取り処理は最後にコミットされた状態を読み続けます。そのため、時間のかかるレポート処理によってWebリクエストが停止することはありません。

行が返されたことを確認します。

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

1|ana|2026-07-30T09:14:00Z|4200が返されます。データベースの隣にapp.db-walapp.db-shmという2つのファイルも作成され、どちらもデータベースに属します。アプリケーションの実行中にapp.dbだけをコピーすると、不完全なバックアップになります。この問題については後で説明します。

SQLiteでは、一度に1つの書き込み処理しか実行できません。この制限はロックであり、キューではありません。そのため、待機時間が長すぎる2番目の書き込み処理は、永久にブロックされずdatabase is lockedで失敗します。アプリケーションが開くすべての接続で、PRAGMA busy_timeout = 5000;を使用して待機時間を延長します。通常のWebワークロードでは、5秒待機するだけでこれらのエラーの大半を解消できます。

DuckDBが有効な場面: 既存ファイルの分析

「how many」、「how much」、「which top ten」のような問いから始まり、入力がCSVまたはParquetファイルの集合である場合は、DuckDBを選択します。コマンドラインクライアントをインストールします。2026年7月時点のバージョンは1.5.5です。

curl https://install.duckdb.org | sh

このスクリプトはバイナリを~/.duckdb/cli/latest/duckdbの下にインストールし、PATHにパスを追加する行を出力します。実行できることを確認します。

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

クエリ対象として現実的なファイルを作成します。これは、zstdで圧縮した注文データの5000000行をParquetに書き込みます。

mkdir -p /srv/data
duckdb :memory: "
COPY (
  SELECT i                                                     AS id,
         'cust_' || (i % 5000)                                 AS customer,
         TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
         (i * 37) % 20000                                      AS amount_cents
  FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"

次に、分析上の問いを実行します。シェルを開き、タイマーを有効にして、インポート手順なしでファイルを直接クエリします。

.timer on
SELECT customer,
       count(*)                AS orders,
       sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;

公開された値をそのまま信頼せず、.timerから実際の値を読み取ってください。結果はディスクとコア数によって変わるためです。重要なのは値の傾向です。CREATE TABLEINSERTもロード手順もありません。DuckDBはParquetのフッターを読み取り、クエリに必要な列チャンクを特定して、それらだけを読み取りました。globのFROM '/srv/data/orders-*.parquet'を使えば、ディレクトリ全体も同じように処理できます。これにより、1か月分の日次エクスポートを1つのクエリで処理できます。

ディスク速度がこの処理全体の下限になります。列スキャンは長いシーケンシャル読み取りだからです。そのため、VPSでのNVMeと旧式のSATAストレージの違いは、SQLiteの小さなランダム読み取りよりも、ここで明確に現れます。

DuckDBからSQLiteデータベースを読み取る

2つのエンジンは、DuckDBのsqlite拡張機能を介して連携します。分析クエリから稼働中の状態に書き込めないよう、アプリケーションデータベースを読み取り専用でアタッチします。

INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;

これはコピーを作成せず、クエリの実行時にSQLiteファイルから行を読み取ります。便利ですが、高速ではありません。ディスク上のデータは行指向のままであり、DuckDBがすべての行を走査する必要があるためです。エクスポートに使用し、30秒ごとに再読み込みするダッシュボードには使用しないでください。

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

この1つのステートメントが、パターン全体です。SQLiteが直近の稼働中の行を管理します。スケジュールされたエクスポートによって、終了した期間のデータをParquetに変換します。DuckDBは数か月にまたがるすべての質問に回答し、アプリケーションデータベースは小さく保たれます。その結果、書き込みが高速になります。

エクスポートは手動ではなく、スケジュールに従って実行します。この用途にはsystemdのサービスとタイマーの組み合わせが適しています。COPYを実行するunitと、それを毎晩起動するtimerを1つずつ用意します。

1台のVPSで両方を実行する

ここではコンテナもポートも必要ありません。両方のエンジンはライブラリであるため、インストールするのはパッケージとファイルパスです。スタックの他の部分がすでに同じVPS上のDocker Composeで実行されている場合は、データディレクトリを必要とするコンテナにマウントしてください。データベースサービスを追加する必要はありません。追加できるサービス自体が存在しないためです。

この構成を問題なく運用するには、2つのルールを守ります。

各エンジンに専用のディレクトリを割り当てます。/srv/appにはアプリケーションが書き込むSQLiteファイルを置き、/srv/dataには分析処理が読み取るParquetファイルを置きます。1つのディレクトリを共有すると、一方をスナップショットするバックアップジョブが、もう一方と競合する可能性があります。

2つのプロセスから、同じDuckDBデータベースファイルを読み書きモードで開かないでください。DuckDBファイルを書き込み用に保持できるプロセスは1つだけです。2つ目のプロセスはファイルを開けずに失敗します。すべてのプロセスがaccess_mode = 'READ_ONLY'を設定していれば、読み取りプロセスは複数あっても問題ありません。これは、複数のプロセスが通常どおり1つのファイルを共有できるSQLiteから移行した人には意外かもしれません。分析処理がParquetファイルを読み取るだけなら、この問題は発生しません。永続データをSQLiteに保持する理由が、もう1つ増えます。

バックアップは異なり、その違いが問題になります

実行中の SQLite データベースは3つのファイルで構成されます。書き込みの途中で cp を使ってコピーすると、開くことはできても内容が正しくないファイルになります。アプリケーションが書き込みを続けている間に一貫性のあるスナップショットを取得する、エンジン独自のバックアップコマンドを使用してください。

sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"

正常なコピーでは integrity_checkok を出力します。それ以外の場合は、そのスナップショットを破棄して、もう一度取得してください。

Parquet ファイルは書き込まれた後に変更されないため、特別な処理は必要ありません。ディレクトリをバックアップしてください。VPS からの restic バックアップを使用して両方のパスをサーバー外に送信すれば、データ層全体を1つのバックアップジョブで2つのディレクトリとして扱えます。

障害パターンと表示される正確な文字列

SQLite の Error: database is locked は、別の接続がタイムアウトで許可された時間を超えて書き込みロックを保持していたことを示します。データ破損ではありません。すべての接続で PRAGMA busy_timeout を設定し、その後、本来は複数の短いトランザクションに分けるべきだった長時間のトランザクションを探します。

権限を変更した後の Error: unable to open database file は通常、プロセスがファイルには書き込めるものの、そのディレクトリには書き込めないことを示します。SQLite はデータベースの隣に app.db-walapp.db-shm を作成するため、.db ファイルだけでなく、ディレクトリ自体にも書き込み権限が必要です。

DuckDB の IO Error: Could not set lock on file は、別のプロセスがすでにそのデータベースを開いて書き込みを行っていることを示します。別のシェルを閉じるか、自分の接続を読み取り専用で開きます。

小規模な VPS で DuckDB の Out of Memory Error が表示される場合、クエリが使用可能な量を超える作業メモリを必要としたことを示します。DuckDB は可能な場合にディスクへ退避するため、:memory: の代わりにディスク上のデータベースファイルを開き、退避先を用意します。また、SET memory_limit = '2GB'; で使用量の上限を設定します。他のサービスも実行しているホストでは、この上限により、アドホッククエリがアプリケーションを RAM 不足に陥らせることを防げます。

Parquet のクエリ実行時に Binder Error: Referenced column "amount" not found が表示される場合、ほぼ常に、ファイルのスキーマが想定していたものと異なります。DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; を実行し、実際の列名を確認します。

実際の選び方

書き込みパターンを確認します。停電後も保持する必要がある小さな書き込みが多数ある場合は、SQLiteを選びます。読み取りパターンも確認します。長期間の履歴に対して、集計を伴う全件スキャンを行う場合は、DuckDBを選びます。実際の多くのシステムでは、両方の条件に当てはまります。その場合は、一方のエンジンにすべてを担わせるのではなく、それぞれが得意とする処理を担当させます。

避けるべき移行は、レポートが遅いという理由で、稼働中のアプリケーション状態をDuckDBに移すことです。レポートが遅い原因がストレージレイアウトにあるなら、必要なのは書き込み処理の作り直しではなく、エクスポートです。

FAQ

DuckDBはアプリケーションデータベースとしてSQLiteの代わりになりますか?

頻繁に書き込むデータベースには適していません。DuckDBはデータベースファイル全体に書き込みロックを取得し、読み書きするプロセスを一度に1つだけ許可します。また、単一行の挿入ではなく、一括変更向けに最適化されています。トランザクション状態はSQLiteに保持し、レポートで必要になったらATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);を使ってDuckDBから読み取ってください。

分析処理ではDuckDBのほうが本当にSQLiteより高速ですか?

大きなテーブルに対するスキャンや集計では高速です。理由はチューニング手法ではなく、ストレージレイアウトにあります。DuckDBはクエリで指定された列だけを読み取り、値をバッチ処理します。一方、SQLiteは1つのフィールドに到達するために行全体を走査する必要があります。主キーで単一行を取得する場合は逆になります。SQLiteは2ページにアクセスしますが、DuckDBはすべての列のストレージにアクセスするためです。

VPSでDuckDBを実行するには大量のRAMが必要ですか?

必要ありません。ただし、上限とディスクを設定してください。:memory:ではなくデータベースファイルを開くと、DuckDBは中間結果をディスクに退避できます。そのうえで、SET memory_limit = '2GB';をVPSで確保できる値に設定してください。上限を設定しないと、大きなGROUP BYによってOut of Memory Errorが発生したり、他のサービスがRAMから追い出されたりする可能性があります。

SQLiteのデータをParquetに移行するにはどうすればよいですか?

DuckDBからSQLiteファイルをアタッチし、COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);を使ってクエリ結果を直接コピーしてください。先月の行など、確定した期間についてスケジュール実行し、アプリケーションが引き続き書き込む最新の行はSQLiteに残します。

どちらをどのようにバックアップすべきですか?

方法を変えて、両方をバックアップしてください。実行中のデータベースは-walであり、-shmファイルでもあるため、cpではなくsqlite3 app.db ".backup '/srv/backup/app.db'"を使ってSQLiteのスナップショットを取得してください。単純なコピーでは内容が不完全になる可能性があります。Parquetファイルは一度書き込むと変更されないため、ディレクトリをコピーするだけで十分です。

#duckdb#sqlite#database#analytics#parquet#self-hosting