モジュール 03ターム1 · 基礎8 ポスター項目

データの基礎

ログインすると、進捗を複数のデバイスで保存できます。ログインするまでは、このブラウザに保存されます。

なぜ重要なのか

コードは再デプロイできます。壊れたデータは元に戻せません。データベースは、システムの中で最も間違いが許されない部分であり、同時に AI ツールが最も軽く扱う部分でもあります。

学習目標

  1. 初日から、キー、制約、マルチテナンシーを備えたデータモデルを設計できる。
  2. EXPLAIN とインデックスを使って、遅いクエリを見つけて直せる。
  3. N+1 クエリを排除できる。
  4. サーバーレス向けにコネクションプーリングを設定できる。
  5. トランザクションとロックを使い、並行処理下でもデータを正しく保てる。
  6. ダウンタイムなしでスキーマ変更をリリースできる。

レッスン

3.1 正しさのためのモデリング

  • ルールはデータベースに強制させます:NOT NULL、UNIQUE、CHECK、外部キー。アプリケーション側のチェックは競合状態に負けますが、制約は負けません。
  • お金:整数の最小通貨単位(セント)または numeric を使い、float は絶対に使いません。
  • 時刻:UTC(timestamptz)で保存し、表示時に変換します。
  • ID:公開 ID には UUIDv7 などを使います。攻撃者がレコードを列挙できる連番 ID は決して公開しません。
  • マルチテナンシー: すべての行は org_id / user_id に属し、すべてのクエリはそれでスコープされます。アプリのコードだけでなく、データベース側でも Row-Level Security(Postgres RLS)で強制します。

3.2 インデックスとクエリ最適化 (25, 26)

  • インデックスはソート済みの検索用構造です。インデックスがなければ、DB はすべての行をスキャンします。
  • 絞り込み、結合、ソートに使うカラムにインデックスを張ります。複合インデックスではカラムの順序が重要です(等価条件のカラムを先に、範囲条件のカラムを後に)。
  • インデックスは1つ増えるごとに書き込みを遅くし、ストレージを消費します。何にでもインデックスを張るのはやめましょう。
  • EXPLAIN ANALYZE を読みます:大きなテーブルでのシーケンシャルスキャン、過大な行数の見積もり、ディスクにあふれるソートに注目します。
  • スロークエリのログ(pg_stat_statements)を有効にし、毎週見直します。

3.3 N+1 クエリ (27)

// N+1: 1 query for orders + 1 per order for its customer
const orders = await db.order.findMany();
for (const o of orders) o.customer = await db.customer.findUnique({ where: { id: o.customerId } });

// Fixed: 2 queries total (or a join)
const orders = await db.order.findMany({ include: { customer: true } });

テストでクエリログを使って検出します:リクエストあたりのクエリ数をアサートしてください。

3.4 コネクションプーリング (28)

Postgres の接続は1本ごとにメモリを消費し、データベースは接続数に上限を設けています(多くの場合 100 前後)。サーバーレス関数は呼び出しごとに接続を1本開くため、負荷がかかると上限を使い切ってしまいます。サーバーレスでは、トランザクションモードのプーラー(PgBouncer、Supavisor、Neon pooler、Prisma Accelerate)を使ってください。

3.5 トランザクションとロック (36, 37)

  • 複数ステップの書き込みはトランザクションで囲みます:すべて成功するか、何も起きないかのどちらかです。
  • 楽観的ロック: version カラムを用意し、UPDATE ... SET version = version + 1 WHERE id = $1 AND version = $2 を実行します。更新された行が0件なら誰かが先に変更したということなので、リトライするか報告します。
  • 悲観的ロック: SELECT ... FOR UPDATE で、トランザクション内で行をロックします。残高や在庫のように競合が起きやすいリソースに使います。
  • バグの例:「ローンを承認」が同時に2回クリックされ、どちらも status = pending を読み取って、どちらも融資を実行してしまう。条件付き更新(WHERE status = 'pending')で修正し、影響を受けた行数を確認します。

3.6 マイグレーションとスキーマバージョニング (81, 82)

  • スキーマ変更はすべて、git 上でバージョン管理されたマイグレーションファイルとして作成し、CI で適用します。ダッシュボードで手作業で変更することは決してありません。
  • ダウンタイムをゼロにするための Expand and contract:(1) 新しいカラムを nullable で追加、(2) 両方に書き込むコードをデプロイ、(3) バッチでバックフィル、(4) 読み取りを切り替え、(5) 後のリリースで古いカラムを削除。
  • 危険な操作:大きなテーブルへのデフォルト値なしの NOT NULL 追加、使用中のカラムのリネーム、CONCURRENTLY なしのインデックス作成。
  • AI エージェントに、本番環境へ直接マイグレーションを実行させてはいけません。

Lab 3:データ層を壊して、直す

  1. 100 万行のデータを投入します。最も遅い3つのクエリを見つけ、インデックスで修正し、修正前後の EXPLAIN を示します。
  2. API テストにクエリ数のアサーションを追加し、N+1 を見つけて修正します。
  3. 「クーポンを利用」リクエストを 20 件同時に送るテストを書き、クーポンがちょうど1回だけ利用されることを証明します。
  4. すべてのテーブルで RLS を有効にし、公開 anon キーを使ってもユーザー A がユーザー B の行を読めないことを確かめるテストを書きます。
  5. Expand-and-contract 方式で、ダウンタイムなしにカラムのリネームを行います。

完了の定義(Definition of Done)

AI プロンプト集

  • 「このエンドポイントが実行するクエリをすべて列挙し、ループ内にあるものを指摘してください。」
  • 「ユーザーが自分の所属する組織の行にだけアクセスできるよう、これらのテーブルの RLS ポリシーを書いてください。そのうえで、それを回避しようとするテストを書いてください。」
  • 「このスキーマ変更を、複数回のデプロイに分けた expand-and-contract 方式のマイグレーションとして計画してください。」