# DB操作・処理の新旧比較

この資料は、業務PostgreSQLへのアクセス方式を、**2026-09-29時点**と**現在**で比較します。旧方式の基準はGit tag `v2026.09.29`（commit `eba90cd721d3905ae2d09ae05d2e8db94606259f`）、現在の基準は2026-10-01の`main`（commit `30b101f70ea31c11706ea148f7272fbc29053a69`）です。

対象は、かけあいWebとAI Workerが扱う業務PostgreSQLです。Cloudflare Auth WorkerのD1操作は、別のデータストア・実行環境であるため、この比較には含めません。

## 結論

9月29日時点は、組織全体を1つの`AppData` snapshotとして読み、更新時には組織共通lockの中で全量読込・clone・差分書込を行う**組織全体スナップショット + 粗粒度ロック方式**でした。

現在は、画面・API・AI処理ごとに必要な行だけを読み、更新対象の行だけを短いtransactionでlockまたはCASする**対象別クエリ + Entity Transaction方式**です。異なるentityの更新は互いに待たず、同じentityの競合だけを409または再試行可能な失敗として扱います。

## 2026-09-29時点の旧方式

[![2026-09-29時点のDB操作・処理。readは組織全体snapshot、writeは組織共通lock内のsnapshot mutation、AI Workerも実行文脈と結果反映で全snapshotに依存する。](06-database-operation-legacy.png)](06-database-operation-legacy.svg)

### Read

画面やAPIは`withPostgresStoreRequest`を通り、最初に組織revisionを確認します。同じrevisionのimmutable snapshotがcacheにあれば再利用しますが、cache missまたはrevision更新後は、組織に属する41の業務表を読み、全体を`AppData`へdecodeします。route handlerは、その大きなオブジェクトから対象ユーザーや成果物を配列検索していました。

したがって「全要求が必ず41表を物理読込する」という意味ではありません。revision別cacheと同時読込のcoalescingはありました。ただし、更新で組織revisionが進むとsnapshotを作り直すため、更新頻度と組織内データ量の影響を受けます。

### Write

同じ組織の更新は、対象entityが別でも`pg_advisory_xact_lock`を共有します。lock取得後に全`AppData`を読み、`structuredClone`したdraftへdomain operationを適用し、変更されたaggregateだけをUnit of Workとして書き込みます。最後に組織revisionを増やし、全`AppData`をもう一度読み直して整合性を確認します。

旧方式は「全表を毎回置換する方式」ではありません。書込自体は変更範囲へ絞られていました。問題は、組織共通lockのcritical sectionに、全量読込・clone・差分判定・全量再検証が含まれていたことです。

同一組織のwriteが並ぶと、lock待ちのtransactionがDB connectionを保持します。調査記録では、組織lock待ち、2秒のlock timeout、pool枯渇、5秒のpool取得timeoutを経て、read・認証・readinessにも500/503が波及する連鎖が確認されています。

### AI Worker

永続command/job、claim、leaseは9月29日時点ですでに存在しました。AI処理を同期HTTPの中で最後まで実行する方式ではありません。

一方で、Workerが生成・feedbackの入力文脈を解決するときは組織全snapshotを読み、結果のmaterializeにもsnapshot mutationを使っていました。永続queueがあっても、前後のDB処理は組織全体のデータ量と共通lockの影響を受ける構造でした。

## 現在の方式

[![現在のDB操作・処理。readはroute・target別の限定query、writeはentity transactionと行lockまたはCAS、AI Workerは対象文脈だけを読みprovider I/Oをtransaction外で実行する。](07-database-operation-current.png)](07-database-operation-current.svg)

### Read

主要画面と通常APIは、最初にrequest identityからactorを解決し、repository内でsession、組織、role、owner、teamなどのlive DB認可を再確認します。その後、routeや対象に応じて必要な行だけを取得します。

これは「1 request = 1 SQL」という意味ではありません。たとえばdashboardやAI入力は、認可、対象成果物、依存関係、最近の履歴を、同じ短いread transaction内の複数SQLで組み立てます。違いは、組織全体を読むのではなく、対象IDと認可条件をqueryへ含め、履歴に`LIMIT`やcursor paginationを使うことです。

既存の画面selectorを安全に再利用する箇所では、取得した行だけを小さな`AppData`互換projectionへdecodeします。型が`AppData`でも、組織全snapshotではありません。

### Write

通常更新は機能別のentity repositoryへ入ります。transaction内でlive認可とruntime fenceを確認し、対象行だけを`FOR UPDATE`等でlockするか、`organization_id`、entity ID、revision、leaseなどを条件にした`UPDATE ... RETURNING`でCASします。

更新対象に応じて、履歴、監査、outbox、job状態も同じtransactionで確定します。組織全体のsnapshotは作らず、通常のentity更新では`organizations.revision`も増やしません。そのため、異なるentity A/Bは独立して進み、同じentityの競合だけが直列化されます。

通常production HTTPから旧snapshot mutationへ到達した場合は、handler本体を実行せず503で拒否します。全量snapshot repositoryは削除されておらず、bootstrap、import、保守処理と非本番fallbackに用途を限定して残っています。

### AI Worker

WebはPostgreSQLへ永続command/jobをenqueueし、Workerは`FOR UPDATE SKIP LOCKED`とleaseで1件をclaimします。Workerは対象artifactまたはworkflow sessionを起点に、所属team、参照元、直近messageなど、provider入力に必要なgraphだけを読みます。

OpenAI呼び出しはDB transactionの外で実行するため、応答待ちの間にDB connectionを保持しません。応答後はleaseと現在の対象状態を検証し、対象別transactionで結果とjob状態を確定します。

AI enqueueのsingle-flightなど、対象を限定したadvisory lockは現在も使います。また、複数Workerのjob claimには`SKIP LOCKED`を使います。「advisory lockを一切使わない」のではなく、通常の利用者更新を組織全体の1本のlockへ集約しない設計です。

### DB接続の保護

DB poolのcheckout前にbounded admissionを置き、通常要求の同時数と待機数を制限します。認証・readiness等のcritical処理には予約枠を残し、queue上限または待機期限を超えた要求は、pool内部へ無制限に積まず、`Retry-After`付きの503へ変換します。

## 比較表

| 観点 | 2026-09-29時点 | 現在 |
|---|---|---|
| 一般的な方式名 | 組織全体スナップショット + 粗粒度ロック | 対象別クエリ + Entity Transaction |
| Readの範囲 | revision別cache miss時に組織内41業務表を読込 | route、actor、team、artifact等の対象行だけ |
| 履歴 | snapshotへ全履歴を展開 | cursor／`LIMIT`で必要な最近履歴を取得 |
| 画面用データ | 全`AppData`からメモリ内検索 | 小さなDTOまたは`AppData`互換projection |
| Writeの排他単位 | organization共通advisory transaction lock | 対象行lock、entity revision／lease CAS |
| Write内容 | 全量read・clone後、変更aggregateを書込 | 対象行と必要な関連行だけを書込 |
| 独立entityの同時更新 | 同じ組織なら共通lockで待つ | 原則として互いを待たない |
| 同一entityの競合 | 組織revisionまたはsnapshot整合性で検出 | revision／lease条件で1件だけcommit、他は409等 |
| AI queue | PostgreSQL永続job + claim／lease | 同じ基本構造を維持し、release fenceとclaimを強化 |
| AI入力 | 組織全snapshotから対象を解決 | 対象workflow graphと直近履歴だけ |
| OpenAI待機中のDB transaction | provider呼出し自体は別処理だが、前後がsnapshot依存 | provider I/Oは明示的に対象read transaction外 |
| 結果反映 | snapshot mutation | 対象別materialize transaction |
| 過負荷制御 | lock待ちがpoolへ波及 | checkout前のbounded queue、critical予約枠、`Retry-After` |
| 全量snapshotの現在位置 | 通常read/writeの中心 | import・保守・bootstrap・非本番fallbackに限定 |

## 根拠と再現方法

旧方式は、現在のファイル名だけから推測せず、tagに固定したソースを確認しました。主な確認箇所は次のとおりです。

```bash
git show v2026.09.29:okr-cai-saas/src/lib/postgres-request-store.ts
git show v2026.09.29:okr-cai-saas/src/lib/db/postgres/repository.ts
git show v2026.09.29:okr-cai-saas/src/lib/db/postgres/reader.ts
git show v2026.09.29:okr-cai-saas/src/lib/ai/generation-job.ts
git show v2026.09.29:okr-cai-saas/src/lib/ai/feedback-job.ts
```

現在方式の主な根拠は、`okr-cai-saas/src/lib/db/postgres/dashboard-reader.ts`、`postgres-entity-api.ts`、`profile.ts`、`workflow-collaboration.ts`、`ai-workflow.ts`、`ai-generation-jobs.ts`、`pool-admission.ts`です。移行理由、負荷時の障害連鎖、実装・Staging検証結果は、`docs/engineering/history/100-concurrent-db-remediation-plan-2026-09-30.md`に記録されています。

## 確認範囲

現在方式はリポジトリの実装と学生側Stagingの試験・公開配備記録まで確認しています。100/200同時、AI burst、2時間soak等の記録はありますが、この資料作成時に本番DBや稼働hostへ接続して再確認したものではありません。管理者ログインが必要な一部操作と、本番環境で現在動いているreleaseは別途確認が必要です。
