PostgreSQLを"組み込む"2つのアプローチ:postgresql-embedded vs PGlite比較


TL;DR

PostgreSQLをアプリに組み込む方法として、Rust向けのpostgresql-embeddedとJS/TS向けのPGliteがある。前者はネイティブのPostgreSQLバイナリをダウンロード・起動する方式で、フルPostgreSQL互換。後者はWASMにコンパイルされた軽量版(3MB gzip)で、ブラウザやエッジでも動く。対象言語・実行環境・マルチコネクション対応の有無が主な違いで、どちらを選ぶかはアプリのランタイムで決まる。

なぜ「組み込みPostgreSQL」が必要なのか

テストや開発環境でPostgreSQLを使いたいが、Dockerコンテナを立てるほどでもない。あるいは、ブラウザ内でSQLを実行したい。こうした「本物のPostgreSQLが欲しいけど、外部プロセスの管理はしたくない」というニーズは意外と多い。

SQLiteならシングルファイルで済むが、PostgreSQL固有の機能(jsonbpgvector、Window関数の細かな挙動など)に依存している場合、SQLiteでは代替にならない。

この問題に対して、まったく異なるアプローチで挑んでいる2つのプロジェクトがある。

postgresql-embedded:ネイティブバイナリを自動管理する

postgresql-embeddedは、Rustのクレートだ。やっていることはシンプルで、PostgreSQLのネイティブバイナリをダウンロードし、一時ディレクトリで起動し、終わったら片付ける

use postgresql_embedded::PostgreSQL;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut postgresql = PostgreSQL::default();
    postgresql.setup().await?;  // バイナリをDL・展開
    postgresql.start().await?;  // PostgreSQLプロセスを起動

    let database_url = postgresql.settings().url("test");
    // ここから普通のPostgreSQLとして使える

    postgresql.stop().await?;   // プロセスを停止・クリーンアップ
    Ok(())
}

起動後は通常のPostgreSQLそのものなので、psqlで接続することもできるし、マルチコネクションも当然サポートする。C言語で書かれた拡張機能もそのまま動く。

ただし、トレードオフは明確だ。

  • 初回起動時にバイナリのダウンロードが走る(数百MB)
  • OSネイティブのプロセスなので、ブラウザやエッジ環境では動かない
  • Rustエコシステム専用(他言語からはFFI経由で使えなくはないが、実用的ではない)

ユースケースとしては、Rustアプリケーションの統合テストやCI環境でのDB起動が中心になる。

PGlite:WASMでPostgreSQLをインプロセス実行する

PGliteはElectricSQLが開発しているプロジェクトで、PostgreSQLをEmscriptenでWASMにコンパイルし、JavaScriptランタイム内でインプロセス実行する

import { PGlite } from "@electric-sql/pglite";

const db = new PGlite(); // メモリ内DB(デフォルト)
// const db = new PGlite("idb://my-db"); // ブラウザ: IndexedDB永続化
// const db = new PGlite("./path/to/data"); // Node.js: ファイルシステム永続化

await db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL
  );
`);

const result = await db.query("SELECT * FROM users;");
console.log(result.rows);

gzip圧縮で約3MBという軽量さが最大の特徴だ。ブラウザのIndexedDBに永続化できるので、offline-firstアプリケーションでSQLiteの代わりにPostgreSQLの構文・機能をそのまま使える。

pgvector拡張にも対応しており、ブラウザ内でベクトル検索を実行することもできる。

一方で、構造的な制約もある。

  • シングルプロセス・シングルコネクション:複数クライアントからの同時接続は想定されていない
  • C言語拡張は動かない:WASMにコンパイルされた拡張のみ利用可能
  • パフォーマンス:WASM実行層を経由するため、ネイティブ比で遅い

アーキテクチャの根本的な違い

両者の違いを一言でまとめると、PostgreSQLの「何を」組み込んでいるかが異なる。

flowchart LR
    subgraph pe["postgresql-embedded"]
        A["Rustアプリ"] -->|"プロセス起動"| B["PostgreSQL\nネイティブバイナリ"]
        B -->|"TCP/Unix Socket"| A
    end

    subgraph pg["PGlite"]
        C["JS/TSアプリ"] -->|"関数呼び出し"| D["PostgreSQL\nWASMモジュール"]
        D -->|"直接返却"| C
    end

postgresql-embeddedは「PostgreSQLプロセスのライフサイクル管理」を提供する。アプリとDBは別プロセスで、通信はTCP/Unixソケット経由だ。つまり外部プロセスの自動化ツールである。

PGliteは「PostgreSQLエンジンそのもの」をWASMとしてアプリ内に埋め込む。プロセス間通信は存在せず、関数呼び出しでクエリを実行する。つまりインプロセスデータベースエンジンである。

比較表

観点postgresql-embeddedPGlite
言語RustTypeScript / JavaScript
PostgreSQL組み込み方式ネイティブバイナリ起動WASMインプロセス実行
バイナリサイズ数百MB(フルPostgreSQL)約3MB(gzip)
マルチコネクション❌(シングルユーザー)
ブラウザ動作
Node.js / Deno / Bun
C言語拡張
pgvector
永続化ファイルシステム(標準PGDATA)IndexedDB / ファイルシステム / メモリ
PostgreSQL互換性完全ほぼ完全(一部制約あり)

どちらを選ぶか

判断基準は実はシンプルで、アプリケーションのランタイムで決まる

  • Rustで書いている → postgresql-embedded
  • JS/TSで書いている → PGlite
  • ブラウザで動かしたい → PGlite一択
  • マルチコネクションが必要 → postgresql-embedded
  • CIでの統合テスト → どちらもあり得るが、言語に合わせる

両者は競合関係にあるわけではなく、対象とするエコシステムがそもそも違う。「RustアプリにPGliteを組み込む」ことも「ブラウザでpostgresql-embeddedを使う」こともできないので、選択で迷うことは実際にはほとんどないだろう。

むしろ注目すべきは、「PostgreSQLを外部サービスとして立てずにアプリに組み込みたい」という同じニーズに対して、言語エコシステムごとに異なるアプローチが確立されつつあるという点だ。SQLiteの組み込みDBとしての地位は揺るがないが、PostgreSQL固有の機能が必要な場面で「じゃあDockerで」以外の選択肢が増えているのは良い傾向だと思う。

以上。

References