bench/modelcase
は小さいけれどごく普通の Go プロジェクトです。ショップのデータ層
(database/sql、トリガーとビューを含む 5 本の SQL マイグレーション、
マスタデータとしてカテゴリ 20 件、商品 500 件、ユーザー 1,000 件)と、
それに対する 70 本のテストからなります。テストはケース表で書いてあり、
参照だけのシナリオ 5 つ(ユーザーごとの注文一覧、注文詳細、売れ筋商品、
商品検索、カテゴリ別売上)が各 10 ケースで 50 本、テーブルを更新する
シナリオ 5 つ(注文の作成・キャンセル・出荷、ユーザーと注文の削除、
値引きの適用)が各 4 ケースで 20 本あり、更新後の行を検証します。どのテストも同じ 40 件のサンプル注文から始まります。
テスト関数は両モデルで同一で、背後のフィクスチャだけが違います。
- pgmem:
TestMain がテストプロセス内でサーバーを起動し、スキーマと
マスタデータを投入してスナップショットを 1 回取ります。参照テストに
コピーは要りません。スナップショットの後にサンプルデータを 1 回だけ
投入したテンプレートサーバーをそのまま共用します。更新テストは
それぞれスナップショットから自分のフォークを作り、サンプルデータを
投入します。70 本すべてが t.Parallel() を呼ぶので、スナップショット
以降は -parallel の枠いっぱいで一斉に走ります。
- 共有サーバー 1 台:testcontainers-go の PostgreSQL モジュールで起動した
公式イメージをオンメモリで動かしたもの(PGDATA は tmpfs、
fsync、
synchronous_commit、full_page_writes はオフ、新しいテストプロセスが
起動する Ryuk も含む)、devbox のネイティブ PostgreSQL 18、公式イメージの docker run の
3 種類です。ドライバがサーバーを起動し、テストバイナリが共有データベースに
スキーマとマスタデータを投入します。参照テストは共有
データベース上で並列に走れますが、更新テストはそうはいきません。
トランザクション系テーブルを TRUNCATE し、在庫と価格を戻し、サンプル
データを投入してから走り、次の更新テストは順番を待ちます。1 台のサーバー
で結果を決定的に保つには、こうするしかありません。
同じ 70 本のテスト(参照 50、更新 20)を、同じスキーマ・マイグレーション・シードデータで、go test の並列度 8 で実行しました。各タイムラインは Apple M3(8 コア)で 5 回走らせたうち合計時間が中央値だった実行です。バーは各実行モデルのノードで、左に短くまとまっているほど速いことを表します。
スイート全体の実時間
pgmem204 ms
5 回の範囲 196 ms〜219 ms
Testcontainers 共有サーバー 1 台1.73 s
5 回の範囲 1.70 s〜1.81 s
PostgreSQL 共有サーバー 1 台646 ms
5 回の範囲 633 ms〜671 ms
docker run 共有サーバー 1 台2.48 s
5 回の範囲 2.26 s〜3.59 s
テストは go test -parallel の 8 枠に描いています。1 つの枠には、そこで順に走ったテストが並びます。 タイムラインはすべて同じ時間軸です。
- DB 起動
- スキーマ投入 + マスタデータ投入
- スナップショット
- フォーク(更新テスト専用のコピー)
- サンプルデータ投入
- データのリセット
- 参照テスト
- 更新テスト
pgmem 204 ms 合計テストプロセス内。参照テストはテンプレートを共用し、更新テストはスナップショットからフォークして、すべて並列に走るフォークは 20 本で、更新テスト(20 本)1 本につき 1 本です。参照テスト 50 本はフォークせず、テンプレートのデータベースをそのまま使います。Testcontainers 共有サーバー 1 台 1.73 s 合計公式イメージを testcontainers-go の postgres モジュールでオンメモリ起動(tmpfs、fsync なし)。更新テストはリセットを挟んで順番に走るコピーはなく、参照テスト 50 本と更新テスト 20 本が 1 つのデータベースを共有します。PostgreSQL 共有サーバー 1 台 646 ms 合計ネイティブの PostgreSQL(devbox)。書き込みテストはリセットを挟んで順番に走るコピーはなく、参照テスト 50 本と更新テスト 20 本が 1 つのデータベースを共有します。docker run 共有サーバー 1 台 2.48 s 合計公式イメージ。書き込みテストはリセットを挟んで順番に走るコピーはなく、参照テスト 50 本と更新テスト 20 本が 1 つのデータベースを共有します。ノードごとの時間
| ノード | pgmem | Testcontainers 共有サーバー 1 台 | PostgreSQL 共有サーバー 1 台 | docker run 共有サーバー 1 台 |
|---|
| DB 起動 | 39 ms | 761 ms | 192 ms | 1.55 s |
|---|
| スキーマ投入 + マスタデータ投入 | 47 ms | 108 ms | 57 ms | 97 ms |
|---|
| スナップショット | 3.2 ms | — | — | — |
|---|
| フォーク(更新テスト専用のコピー) | 218 ms | — | — | — |
|---|
| サンプルデータ投入 | 288 ms | 624 ms | 231 ms | 570 ms |
|---|
| データのリセット | — | 140 ms | 111 ms | 161 ms |
|---|
| 参照テスト | 61 ms | 161 ms | 105 ms | 163 ms |
|---|
| 更新テスト | 16 ms | 21 ms | 10 ms | 38 ms |
|---|
| 実時間(起動 + テストバイナリ) | 204 ms | 1.73 s | 646 ms | 2.48 s |
|---|
コピーやテストごとの値を合算しています。pgmem では各コピーが同時に走るため、合算値は実時間を超えます。
バーはそれぞれ実行モデルの 1 ノードで、テストバイナリの中で計っています
(MODELCASE_TRACE)。外部サーバーの起動は、app データベースで SELECT 1
が通るまでをドライバが計り、テストバイナリの実時間も計ります。合計は起動と
テストバイナリの和で、ノードに含まれない数ミリ秒はバイナリ自身の起動と
終了です。各タイムラインは合計時間が中央値だった実行で、ビルド直後の
初回実行はウォームアップとして記録しません。go test の -parallel は
既定の GOMAXPROCS です。タイムラインはすべて同じ時間軸です。
読み取れること:
- 共有サーバーは更新テストごとに払います。 リセットとサンプルデータ
投入のコストは、pgmem のフォークとサンプルデータ投入とほぼ同じですが、
共有サーバーではそれが直列に、フォークでは横に並んで走ります。
- pgmem の固定費は起動とスナップショットで、どちらもネイティブ
サーバーの起動より小さく、コンテナとは比べものになりません。
- フォークは軽く、重なって進みます。 フォークはデータディレクトリを
copy-on-write でクローンし(ツリーだけ複製し、ファイルの中身は書き込む
まで共有)、共有メモリを二重にゼロ埋めしなくなったバックエンドを起動する
ので、1 つだけなら約 6 ms です。8 つ同時だとページフォルトで競合して
1 つあたり 10 ms ほどになりますが、20 本のフォークとサンプル投入は
実時間 120 ms ほどに収まります。
- 共有サーバー上の参照テストはトランザクション境界で直列化されます
(pgmem のサーバー 1 つにつきバックエンドセッションは 1 つ)。そのため pgmem
では、マルチプロセスのサーバーより 1 本ずつは少し遅くなります。どちらに
しても短い時間です。
RUNS=5 ./run.sh # pgmem、Testcontainers、devbox、docker
TARGETS="pgmem devbox" ./run.sh # Docker なし
スクリプトはテストバイナリを 1 回コンパイルし、対象ごとにドライバを走らせて
results/raw.jsonl(各実行とそのスパン)、results/summary.json、そして
このページが描画する website/src/data/modelcase.json を書きます。同じ
ディレクトリで go test ./... を実行すると、トレースなしの普通のテスト
として pgmem 上でスイートが走ります。
| 起動〜最初のクエリ | テストごとの複製 | インデックス SELECT | 20万行ソート | DB と計測プロセスのメモリ | ダウンロード |
|---|
| pgmem(Go ライブラリ) | 70 ms | 15 ms (フォーク) | 29 µs9 µs プロセス内接続 | 117 ms | 103 MiB計測プロセスの増分 103 MiB | 33.1 MB (テストバイナリへのリンク増分) |
|---|
| pgmem バイナリ(Python / Java / Node.js) | 66 ms | 15 ms (フォーク) | 29 µs | 117 ms | 110 MiB | 16.3 MB (wheel / jar / npm の中身) |
|---|
| docker run | 1.65 s | 61 ms (CREATE DATABASE … TEMPLATE) | 93 µs | 259 ms | 77 MiBコンテナ使用量 76 MiB, 計測プロセスの増分 1 MiB, VM の差分 433 MiB, VM 絶対値 1335 MiB | 118 MB (イメージレイヤ) |
|---|
| Testcontainers(Go) | 1.09 s | 21 ms (Snapshot / Restore) | 102 µs | 258 ms | 137 MiBコンテナ使用量 134 MiB, 計測プロセスの増分 2 MiB, VM の差分 295 MiB, VM 絶対値 1192 MiB | 120 MB (イメージレイヤ + Ryuk) |
|---|
| devbox(nix の PostgreSQL) | 411 ms | 83 ms (CREATE DATABASE … TEMPLATE) | 28 µs | 228 ms | 36 MiBPostgreSQL プロセス合計 | 113 MB (nix closure) |
|---|
2026-09-18 に計測した pgmem とバイナリの中央値(5 回)。Apple M3、8 コア、16 GB、Darwin 27.0.0。OrbStack 2.2.3(Docker 29.4.0)、arm64。Docker、Testcontainers、devbox の比較値は 2026-09-14 の基準値です。メモリ列は DB 使用量と計測プロセスの増分を比べます。Testcontainers は PostgreSQL と Ryuk のコンテナ使用量を含みます。VM の差分と絶対値は参考値として分けており、OrbStack がメモリを解放すると差分は小さくなります。
ベンチマーク中の DB と計測プロセスのメモリ
棒は中央値、ひげは各回の最小値から最大値です。Docker と Testcontainers はコンテナ使用量と計測プロセスの増分を合算します。Testcontainers には PostgreSQL と Ryuk が含まれます。OrbStack VM は負荷中にメモリを解放することがあるため、VM の差分は別に示します。
エクスポートを絞った AOT ビルドで、pgmem ライブラリと単独バイナリの行を
2026-09-18 に再計測しました。起動・メモリなどは各 5 回、Go ベンチマークは
3 回実行しています。darwin/arm64 の strip 済みバイナリは 36.7 MB、zip の
配布物は 16.3 MB、strip 済み Go テストバイナリへリンクした増分は 33.1 MB
でした。Docker、Testcontainers、devbox は再計測せず、比較行は 2026-09-14 の
基準値を残しています。
どの計測も、新しいプロセスでサーバーを 1 つ起動し、同じスキーマを流し込んでから測ります。ハーネスは bench/alternatives にあります。
- 起動〜最初のクエリ:起動処理を呼び出してから、TCP 経由の
SELECT 1 が成功するまで。docker pull と devbox install は計測前に実行し、Devbox 環境の解決も計測外の準備で済ませます。docker run は新規コンテナと DB クラスタの初期化を含みます。Devbox は初期化済みの永続クラスタと app DB を計測前に準備し、devbox services up -b の実行から SQL ready までを計ります。Testcontainers は新しいテストプロセスが起動する Ryuk コンテナも含みます。pgmem バイナリは ready 行が出るまでを計り、Python、Java、Node.js のラッパーはその行を待ちます。
- テストごとの複製:準備済みデータベース(ユーザー 1,000 件、注文 10,000 件)の独立した複製を 20 回作り、そのたびに接続して結合クエリを 1 本流したときの中央値です。pgmem はスナップショットからフォークします。docker run と devbox は
CREATE DATABASE … TEMPLATE と DROP DATABASE、Testcontainers は PostgreSQL モジュールの Snapshot と Restore を使います。後者も内部ではテンプレートデータベースで同じことをしています。
- インデックス SELECT:pgx で
SELECT v FROM bench WHERE id = $1 をウォームアップ後に 3,000 回。pgmem の 2 つ目の値は、TCP の代わりにプロセス内ダイヤラ(Server.Dial)を使ったものです。
- 20万行ソート:
SELECT count(*) FROM (SELECT md5(i::text) AS h FROM generate_series(1, 200000) i ORDER BY h) t を、ウォームアップ 1 回の後に 3 回流した中央値です。
- メモリ:負荷中に OrbStack VM がメモリを返すと、コンテナの使用量が多くても Testcontainers の値が
docker run より小さく見えることがあります。そこでグラフは VM の差分を使わず、DB 側の使用量と計測プロセスの増分を比べます。pgmem はプロセスの phys_footprint 増分、バイナリは単独の pgmem プロセスを使います。Docker と Testcontainers は各コンテナの Docker 使用量に Go の計測プロセスの増分を足します。Testcontainers には PostgreSQL と Ryuk が含まれ、メモリ値には docker stats の表示を使います(Linux CLI はキャッシュを差し引きます)。devbox は PostgreSQL のポストマスターと子プロセスを合算します。OrbStack VM の差分と絶対値は表に分けて載せています。
- ダウンロード:初回に取得するもの。Go ライブラリとしての pgmem は、strip したテストバイナリがリンクによって増える量です。バイナリの行は wheel、jar、npm パッケージが同梱する圧縮済みバイナリの大きさです。イメージは計測したアーキテクチャの圧縮レイヤ、nix closure は devbox が cache.nixos.org から取得する量です。
- 既定値が違います。 サーバーは PostgreSQL の既定値(
shared_buffers=128MB)を使います。Testcontainers の計測では PostgreSQL 18 のデータディレクトリ(/var/lib/postgresql/18/docker)を tmpfs にマウントし、docker run と devbox はディスクを使います。pgmem は shared_buffers=32MB でファイルシステムもメモリ上にあるため、一時ファイルにあふれるソートでもディスクに触れません。
- グラフは DB と計測プロセスのメモリを比べます。 スキーマ準備後とクエリ・分離計測後の中央値および計測回ごとの範囲を示します。Testcontainers は PostgreSQL と Ryuk のコンテナ使用量を含みます。Linux の
docker stats CLI は表示値からキャッシュを差し引きます。
- OrbStack VM の差分は別の参考値です。 コンテナの計測ごとに VM を再起動し、アイドル時を記録します。VM は実行中にメモリを解放することがあり、コンテナ使用量が増えても VM の差分が下がる場合があります。表では差分と VM の絶対値を分けています。
- pgmem のメモリは負荷で増えます。 WebAssembly のリニアメモリはサーバー稼働中は OS に返らず、生きているフォークはそれぞれ自分の複製を持ちます。生きているフォーク 1 つあたりアイドル時の値程度を見込み、
MaxForks で数を抑えてください。
- コンテナ方式の差には実行条件も含まれます。 docker run はディスク上に保存しますが、Testcontainers は PGDATA を tmpfs に置き、Ryuk も起動します。そのため、差には保存先と起動方式の違いが含まれます。
RUNS=5 MEM_RUNS=3 ./run.sh
OrbStack と devbox が必要です。メモリ計測のパスでは OrbStack を再起動しますが、ほかのコンテナが動いているときは再起動せずに止まります。生の計測値を results/raw.jsonl、中央値を results/summary.json に書き、要約を website/src/data/benchmarks.json にコピーします。このサイトの表はそのファイルを描画しています。