はじめに
こんにちは!エンジニア2年目のTKDSです!
今回はPGliteについて調べてみました!
概要・使い方・速度実験・まとめの内容で記事は構成されています。
使ってみた結果として、軽量高速であり色々使いみちがありそうなツールだと感じました。
ぜひ最後まで読んでいただけると幸いです。
PGliteの概要
PGliteは、PostgreSQLをWebAssembly(WASM)にコンパイルした軽量なデータベースエンジンです。
これにより、ブラウザ、Node.js、Bun、DenoなどでPostgreSQLの機能を利用でき、開発者はローカルやサーバーレス環境でデータベース操作を行うことが可能です。
PGliteは、インメモリデータベースやファイルシステム(Node.jsやBun)、IndexedDB(ブラウザ)での永続化をサポートします。
PGliteの特徴
公式サイトによると、
1. Lightweight
2. Extendable
3. Reactive
上記、3つの特徴が挙げられていました。
- 1:PGliteのWASMバイナリが圧縮状態で3MB程度であること
- 2:PostgreSQLの拡張機能が適用可能であること
- 3:に関してはテーブルが変更されたときに更新された結果を受け取る機能をサポートしていること
から来ているそうです。
3はCDC(Change Data Capture)の機能に似てますね!
フロントエンドは詳しくないのですが、状態管理などにも使えるのでは?など思い浮かびました。
PGliteを試す
ドキュメントをみつつ環境構築してみましょう。
手軽に試したい場合は、こちらのリンクから試すこともできます。
今回は手元で試してみます。
npm install @electric-sql/pglite
application.jsに下記の内容を書き込みます。
const { PGlite } = require('@electric-sql/pglite'); (async () => { try { const db = new PGlite(); await db.exec(` CREATE TABLE IF NOT EXISTS todo ( id SERIAL PRIMARY KEY, task TEXT, done BOOLEAN DEFAULT false ); INSERT INTO todo (task, done) VALUES ('Install PGlite from NPM', true); INSERT INTO todo (task, done) VALUES ('Load PGlite', true); INSERT INTO todo (task, done) VALUES ('Create a table', true); INSERT INTO todo (task) VALUES ('Update a task'); `); const ret = await db.query(` SELECT * from todo WHERE id = 1; `); console.log("Query Result:", ret.rows); } catch (error) { console.error("Error executing query:", error); } })();
ドキュメントの内容をそのままコピーしても動かないので、少々修正してあります。
結果は以下のとおりです。
無事に動かすことができました。
雑に時間を測ってみると起動からSQL実行まで2.08秒ほどで完了しています。非常に高速ですね!
ブラウザで使う
コマンドラインで決まったSQLを実行する以外にも、ブラウザでREPLを使って、対話的にPGliteにアクセスできます。
ドキュメントにコードが記載されていますが、2024/8/18時点では、そのまま使用できません。
開発者ツールでみると、Failed to load resource: the server responded with a status of 404 ()
とメッセージが表示されています。
ソースからファイルを開くと
Couldn't find the requested file /dist-webcomponent/Repl.js in @electric-sql/pglite.
と表示されています。
おそらくドキュメントのコードのリンクが間違っていそうです。
JSDELIVERで調べてみましょう。
調べてみると、下記画像のようにpglite-replがありました!このパスに変更すればいけそうです。
念のため中身をチェックしてから使用しました。
では、以下のコードをファイルに書き、ブラウザで開いてください。
<!doctype html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>PGlite REPL Example</title> <script src="https://cdn.jsdelivr.net/npm/@electric-sql/pglite-repl@0.2.1/dist-webcomponent/Repl.js" type="module" ></script> </head> <body> <h1>PGlite REPL</h1> <!-- PGlite REPLコンポーネントがここに表示されます --> <pglite-repl id="repl"></pglite-repl> <script type="module"> import { PGlite } from "https://cdn.jsdelivr.net/npm/@electric-sql/pglite/dist/index.js"; // PGliteインスタンスを作成 const pg = new PGlite(); // REPL要素を取得 const repl = document.getElementById("repl"); // PGliteインスタンスをREPLに設定 repl.pg = pg; </script> </body> </html>
REPLを開くことができ入力もできました!
PGliteの速度計測
次にPGliteの速度を測ってみます。
SELECT、 INSERT、UPDATE、DELETEについて、それぞれ実験します。
以下のコードで実験します。
各DB操作を行い、1000, 2000, 3000, 4000, 5000, 10000と扱う件数が増えていきます。
import { PGlite } from "@electric-sql/pglite"; import { performance } from "perf_hooks"; const measureDbStartup = () => { const dbStartTime = performance.now(); // DB起動時間の測定開始 const db = new PGlite(); // データベースインスタンスを初期化 const dbEndTime = performance.now(); // DB起動時間の測定終了 const dbStartupTime = dbEndTime - dbStartTime; console.log(`DB startup time: ${dbStartupTime.toFixed(3)} ms`); return db; }; const initDb = async (db, numRows) => { await db.exec(` CREATE TABLE IF NOT EXISTS test_table ( id SERIAL PRIMARY KEY, name TEXT, value INTEGER ); `); const values = []; for (let i = 0; i < numRows; i++) { values.push(`('name_${i}', ${i})`); } await db.exec( `INSERT INTO test_table(name, value) VALUES ${values.join(", ")}`, ); }; const measureOperationTime = async (operation, db, numRows) => { let totalTime = 0; for (let trial = 1; trial <= 3; trial++) { const startTime = performance.now(); await operation(db, numRows); const endTime = performance.now(); const duration = endTime - startTime; totalTime += duration; console.log( `Trial ${trial}, ${operation.name.toUpperCase()} ${numRows} rows: ${duration.toFixed(3)} ms`, ); } const averageTime = totalTime / 3; console.log( `Average ${operation.name.toUpperCase()} time for ${numRows} rows: ${averageTime.toFixed(3)} ms`, ); console.log(`Time per row: ${(averageTime / numRows).toFixed(6)} ms/row`); }; const selectOperation = async (db, numRows) => { await db.query(`SELECT * FROM test_table LIMIT ${numRows}`); }; const insertOperation = async (db, numRows) => { const values = []; for (let i = numRows; i < numRows * 2; i++) { values.push(`('name_${i}', ${i})`); } await db.exec( `INSERT INTO test_table(name, value) VALUES ${values.join(", ")}`, ); }; const updateOperation = async (db, numRows) => { await db.exec( `UPDATE test_table SET value = value + 1 WHERE id <= ${numRows}`, ); }; const deleteOperation = async (db, numRows) => { await db.exec(`DELETE FROM test_table WHERE id <= ${numRows}`); }; // 実行 const main = async () => { const operationCounts = [1000, 2000, 3000, 4000, 5000, 10000]; const operations = [ selectOperation, insertOperation, updateOperation, deleteOperation, ]; for (const count of operationCounts) { for (const operation of operations) { console.log( `\nRunning ${operation.name.toUpperCase()} test with ${count} rows:`, ); const db = measureDbStartup(); // DB起動時間の測定とインスタンス作成 await initDb(db, count); // DB初期化とデータ挿入 await measureOperationTime(operation, db, count); // 操作のパフォーマンス測定 } } }; main() .then(() => console.log("All tests completed")) .catch((err) => console.error("Error:", err));
結果を以下にまとめます。
行数 | 平均起動時間 (ms) | SELECT (ms) | INSERT (ms) | UPDATE (ms) | DELETE (ms) | SELECT (ms/row) | INSERT (ms/row) | UPDATE (ms/row) | DELETE (ms/row) |
---|---|---|---|---|---|---|---|---|---|
1000 | 0.141 | 2.539 | 3.651 | 4.893 | 0.476 | 0.002539 | 0.003651 | 0.004893 | 0.000476 |
2000 | 0.051 | 2.801 | 7.267 | 9.216 | 0.701 | 0.001401 | 0.003633 | 0.004608 | 0.000350 |
3000 | 0.071 | 3.867 | 10.362 | 13.177 | 0.835 | 0.001289 | 0.003454 | 0.004392 | 0.000278 |
4000 | 0.035 | 5.588 | 14.742 | 17.931 | 1.776 | 0.001397 | 0.003685 | 0.004483 | 0.000444 |
5000 | 0.047 | 7.007 | 19.127 | 23.373 | 2.102 | 0.001401 | 0.003825 | 0.004675 | 0.000420 |
10000 | 0.036 | 10.971 | 37.880 | 43.966 | 3.535 | 0.001097 | 0.003788 | 0.004397 | 0.000354 |
起動時間は概ね1msかからず、高速であることがわかります。
データ操作も高速です。
1万件でも一番遅くて、UPDATEの43msのため、テスト用途などであれば十分実用に耐えるのではないかと感じました。
まとめ
今回はPGliteを動かすまでの方法と試行錯誤について書きました。
まだ開発中ということもあり、色々不正確なことや情報がなく大変でした。
PGlite自体は非常に高速でREPLも用意されており、ツールとして非常に魅力的でした!
今回は非常に簡単な使い方だけだったので、今後は起動状態を維持し、クライアントライブラリから接続して使ってみたり、テストでのDBとして使ってみたいと考えています。
ここまで読んでいただきありがとうございました!