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固有の機能(jsonb、pgvector、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-embedded | PGlite |
|---|---|---|
| 言語 | Rust | TypeScript / 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で」以外の選択肢が増えているのは良い傾向だと思う。
以上。