1 分

最初のマイグレーション前に行うPostgreSQLスキーマ検証

PostgreSQLスキーマ検証では、誤った対応付け、不十分な制約、欠けたインデックス、安全でない変更を、最初のマイグレーションがデータに触れる前に見つけられます。

最初のマイグレーション前に行うPostgreSQLスキーマ検証

AIビルダーは有効なPostgreSQLを生成できても、誤ったデータベースを推測することがあります。構文は簡単な部分です。危険なのは、一見もっともらしい誤りです。任意の関連が必須になったり、ステータス文字列に不完全なCHECK制約が付いたり、本来残すべきレコードまで削除が連鎖したり、マイグレーションがテーブルを作り直してカラムをひそかに失ったりします。

そのためPostgreSQLスキーマの検証では、意味、マイグレーション時の挙動、復旧を別々にテストする必要があります。私は、既知のデータセット、明示的な不変条件、代表的なクエリ、破壊的変更のレビュー、復元のリハーサルを通過した推測スキーマだけを承認します。どれか一つでも欠けていれば、そのマイグレーションはまだ提案にすぎません。

本番データベースが空でも、最初のマイグレーションにはこの厳密さが必要です。初期のスキーマの誤りはすぐ固定化します。アプリケーションコード、シードデータ、レポート、後続のマイグレーションが、その誤りに依存し始めるからです。最初の実行前に15分レビューするほうが、6か月後に、なぜ別々の概念が一つのNULL許容テキストカラムを共有しているのか説明するより、たいてい安く済みます。

推測したスキーマは信頼できない仕様書

推測スキーマは、実行してよい真実ではなく、仕様書の草案として扱ってください。ビルダーが見たのは、プロンプト、サンプル画面、取り込んだレコード、生成されたアプリケーションコードかもしれません。将来データベースに入る、あらゆる業務上の例外、保存ルール、一括インポート、サポートによる修正、支払い失敗まで見届けたわけではありません。

チームが混同しがちな三つの問いを、最初に分けます。スキーマの正しさは、テーブルと制約がドメインを表せているかを問います。マイグレーションの安全性は、提案された操作が既存データを保ち、実行中もデータベースを使える状態に保つかを問います。復旧の準備は、途中まで進んだ変更や意味を誤った変更の後で、既知の状態へ戻れるかを問います。一つに合格しても、残り二つについてはほとんど何も分かりません。

CREATE TABLE文は意図した最終構造を表せても、危険な操作でそこに至ることがあります。たとえばビルダーがcustomer_name textcustomer_id bigintへ変えるとします。最終的な外部キーは妥当かもしれません。しかし、過去の名前を顧客と照合する前に名前のカラムを削除するマイグレーションは、その照合に必要な唯一の証拠を壊します。スキーマレビューは到達点を承認し、マイグレーションレビューはそこまでの道のりを調べます。

提案されたモデルを、ドメインの言葉で声に出して読んでください。invoices.customer_idcustomers.idを参照すると言う代わりに、各請求書は正確に一人の法的な顧客に属すると言います。前者では役に立つ反論が出ます。顧客選択前に下書きが存在するかもしれず、取り込んだ請求書がアーカイブ済みの顧客を参照するかもしれず、法的記録には発行時点の顧客名を固定しておく必要があるかもしれません。SQLの語彙は、そうした意見の相違を隠してしまいます。

私は、推測されたすべてのテーブルの横に前提メモを置くよう求めます。そこには、一行が何を意味するか、行をどう識別するか、誰が所有するか、見かけ上の親がなくても存在できるか、削除が何を意味するかを書きます。チームがこの点に答えられないなら、ビルダーが推測したデータベースを、チームはまだ設計していません。

既知のレコードで誤ったテーブル対応を見つける

既知のデータセットには、意味の網羅性を考えて選んだレコードを含めるべきです。大きなランダムサンプルは、同じ簡単なケースを繰り返しがちだからです。ほぼ同じ成功例の行を一万件集めるより、慎重に選んだ十件のレコードのほうが多くを明らかにできます。

DDLを実行する前に、対応表を作ります。表の一行では、各ソース概念が提案された行き先でどう扱われるかを追い、期待する件数または値を記録します。注文アプリケーションなら、成果物は次のようになります。

既知の事実提案する行き先期待する結果
注文Aには明細が二件あるordersorder_items注文行一件と子行二件
注文Bには割り当て済みアカウントがないorders.account_idaccountがNULLの行一件
二人が同じメールアドレスを共有しているcontacts.email一意性が明示されたルールでない限り、両方の行が残る
商品コードには先頭のゼロがあるproducts.codeテキスト値00417が変わらない
キャンセル済みの注文にも請求は残るorderschargesキャンセル後も請求行が残る

これにより、制約の細部がレビューの注意をそらす前に、テーブル対応の誤りを見つけられます。AIビルダーは繰り返し現れるオブジェクトを別テーブルに正規化しがちで、通常はそれが妥当です。しかし、繰り返しがあるからといって同一性が証明されるわけではありません。同じテキストの二つの配送先住所は、編集可能な一つの住所行への参照ではなく、過去時点のスナップショットかもしれません。それらを統合すると、後から住所を編集したときに履歴を書き換えてしまいます。

反対の誤りも起こります。画面に一緒に表示されているという理由で、ビルダーが顧客のフィールドをすべての注文にコピーすることがあります。一部の値は顧客に属しますが、ほかの値は注文時点のスナップショットとして残す必要があります。適切な設計には、customer_idbilling_nameのような発行済み文書のフィールドが両方あることがあります。これを重複と呼んで片方を消すと、現在の本人性か過去の事実のどちらかを失います。

既知のデータセットは、アプリケーションで使うものと同じインポートまたはシードの経路で、使い捨てのデータベースへ読み込みます。その後、単なる行数ではなく、事実に対するアサーションを書きます。

SELECT
    (SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
    (SELECT count(*) FROM order_items i
       JOIN orders o ON o.id = i.order_id
      WHERE o.external_id = 'ORDER-A') AS order_a_items,
    (SELECT account_id IS NULL FROM orders
      WHERE external_id = 'ORDER-B') AS order_b_unassigned;

合格する形を明示します。

 order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
       1 |             2 | t

生成されたアプリケーションが表示できるからといって、説明できない差分を受け入れてはいけません。UIは、重複した親、失われた子、切り詰められたコード、作り出されたデフォルト値を隠せます。本番展開を議論する前に、意図して用意したすべてのフィクスチャを照合してください。

制約はドメイン上の真実を表現する

データベース制約は、どの画面、API、インポート、修正スクリプトが行を書き込んでも、常に無効な状態を拒否すべきです。ルールに例外があったり、変わり得る外部の事実に依存したりするなら、それを単純な制約に押し込むと、作業が止まるか、不正確なデータになることが多いです。

主キーは行を識別しますが、それだけで意味のある業務上の識別子にはなりません。内部のbigint IDは、テナント内で一意の注文番号と共存できます。業務上、注文番号がテナントごとに一意なら、UNIQUE (tenant_id, order_number)がそのルールを表します。全体で一意にする制約は正当なレコードを拒否し、制約がなければリトライ時に曖昧さを許します。

CHECK制約は、quantity > 0finished_at >= started_atのような安定した行の事実に向いています。PostgreSQLのマニュアルでは、データベースはCHECK式が制約の存続中に不変だと仮定すると説明されています。そのため、後から振る舞いが変わる関数をCHECKから呼ぶと、古い行が見かけ上のルールに違反したまま残ることがあります。固定した真実には固定した式を使います。管理者が制御する現在許可された集合のような変わる方針は、参照テーブルまたはアプリケーションのワークフローに置きます。

生成されたステータス制約は疑ってかかる必要があります。ビルダーは現在の例を調べ、次のように出力するかもしれません。

status text NOT NULL
    CHECK (status IN ('draft', 'active', 'closed'))

これは、それらが完全で永続的な状態である場合にだけ正しいものです。失敗、キャンセル、停止、インポート済み、不明なレガシーレコードについて確認してください。状態遷移がまだ変化しているなら、ルックアップテーブルで追加を明示できますが、遷移の検証の代わりにはなりません。行にclosedを入れられることから、draftから直接closedへ移れるかどうかは分かりません。

一意性は意図して使います。PostgreSQLは一意制約を一意なB-treeインデックスで実装しますが、部分一意インデックスは別のルールを表します。論理削除では、生きている行だけで一意性が必要になることがよくあります。

CREATE UNIQUE INDEX users_tenant_email_live_uq
    ON users (tenant_id, lower(email))
    WHERE deleted_at IS NULL;

これはUNIQUE (tenant_id, email, deleted_at)と同じではありません。PostgreSQLは一意性のルールに従ってNULL値を扱い、削除日時を加えると、強制される同一性そのものが変わります。カラムの一覧から挙動を推測するのではなく、フィクスチャで正確な重複ケースをレビューしてください。

NULL許容は業務上の判断

ドメイン上、正当なすべての行に値が必要であり、すべての書き込み経路で値を渡せる場合にだけ、カラムをNOT NULLにします。画面設計は弱い証拠です。現在のフォームで必須のフィールドだからといって、インポート、下書き、システム生成の行、履歴データについては何も分かりません。

四つの状態を別々に確認します。ソースがフィールドを省略した、ソースが明示的にnullを送った、ソースが空の値を送った、意味のある値を渡した、の四つです。JSON API、フォーム、CSVインポート、PostgreSQLでは、これらの状態の扱いが異なることがあります。アプリケーションが挿入前に四つすべてをまとめてしまうなら、データベースが解決したふりをせず、スキーマレビューでその判断を明らかにするべきです。

デフォルト値も同様に注意が必要です。デフォルト値はINSERT時にカラムを省略した場合に値を入れます。明示的なNULLを直すものではなく、その値が正しい証拠にもなりません。不明な国があり得るなら、country_code DEFAULT 'US'は危険です。行には自信に満ちた誤りが入り、レポートやコンプライアンスのロジックがそれを信頼するかもしれません。

よくある生成マイグレーションでは、一つの文で必須カラムを追加します。

ALTER TABLE customers
    ADD COLUMN account_type text NOT NULL DEFAULT 'standard';

文は実行できるかもしれませんが、根拠なしに、すべての過去の顧客がstandardになります。より安全な手順では、NULL許容カラムを追加し、既知のデータから値を導き、未解決の行を測定し、アプリケーションの書き込みで新たな欠落を止め、ドメインがそれを支持するときにだけNOT NULLを追加します。不明が正当なままなら、NULLを維持し、クエリと画面でどう表示するかを定義します。

PostgreSQLでは、一部の制約を有用に分けて扱えます。CHECKまたは外部キーはNOT VALID付きで追加でき、作成時に既存の全行を検証せず、後からVALIDATE CONSTRAINTで確認できます。マニュアルでは、最初のテーブルスキャンを先送りする方法として説明されています。古い違反を無視してよいという意味ではありません。新しい書き込みには制約が適用され、承認前には検証手順に必ず通らなければなりません。

NULL許容を厳しくする前に、実際の分類を示す分布クエリを実行します。

SELECT
    count(*) AS total,
    count(*) FILTER (WHERE account_type IS NULL) AS nulls,
    count(*) FILTER (WHERE account_type = '') AS empty_strings,
    count(*) FILTER (WHERE account_type NOT IN
        ('standard', 'partner', 'internal')) AS unexpected
FROM customers;

AIが生成したデフォルト値は、マイグレーション後にこのクエリをきれいに見せることがあります。バックフィル前にも実行し、その結果を残してください。そうしないと、導いた値と作り出した値を区別するための証拠を失います。

インデックスは観測したアクセスパターンに答えるべき

生成前に承認する
チームがスキーマの前提を承認してから、チャットでアプリケーションを改善していきましょう。

既知のクエリを支える、明示した一意性ルールを強制する、または運用上の要件を満たす場合に、インデックスを承認します。識別子のように見えるカラムすべてにインデックスを付けると、ストレージと書き込みの負担を無駄にします。一方、重要な複合インデックスを一つ欠くだけで、普段の一覧ページが拡大し続けるスキャンになることがあります。

まず、生成されたアプリケーションが実際に発行するクエリから始めます。絞り込みカラム、テナント境界、結合カラム、並び順、期待する結果件数を記録します。最近の注文ページでは、テーブル図より次のクエリ形状のほうが重要です。

SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 50;

tenant_idだけのインデックスでは、多くのテナント行を調べて並べ替える可能性があります。(tenant_id, status, created_at DESC)のインデックスは、このアクセスパターンによりよく合います。カラムの順序は人気投票ではありません。実際のクエリにおける等価条件、範囲条件、並び順、選択性に従います。

代表的なデータに対してEXPLAIN (ANALYZE, BUFFERS)を実行します。ただし、小さなフィクスチャ一つを性能の証明にしてはいけません。PostgreSQLが小さなテーブルでシーケンシャルスキャンを選ぶのは正しいことがあります。検証では、意図したインデックスが存在することと、本番規模のリハーサルでプランナーが現実的な選択をできることを確認します。承認のためにインデックススキャンを作り出そうとして、シーケンシャルスキャンを無効にしてはいけません。

外部キーには、もう一つよくある意外な点があります。PostgreSQLは参照先の主キーまたは一意カラムにはインデックスを付けますが、参照する子側のカラムには自動でインデックスを作りません。そのため親を削除または更新すると、参照を確認するために子テーブルをスキャンする可能性があります。子から親への結合にも、その子側インデックスが必要になることがあります。想定される読み取りと親の変更に基づき、すべての関連を確認してください。

初期提案では、重複したインデックスと使われないインデックスを却下します。適切な(tenant_id, status, created_at)インデックスがすでにあるなら、(tenant_id, status)は冗長かもしれません。ただしワークロードの詳細によって判断は変わります。名前ではなく定義を比べてください。AIビルダーは機能ごとに一つのインデックスを作り、複数の機能が同じ先頭カラムを求めていることを見落としがちです。

すでに負荷の高いデータベースでは、CREATE INDEX CONCURRENTLYはトランザクションブロック内で実行できず、より多くの作業が必要で、失敗後に無効なインデックスを残すことがある点を覚えておいてください。PostgreSQLのマニュアルは、こうした運用上の違いを明記しています。すべてのマイグレーションをトランザクションで包むフレームワークでは、期待だけでキーワードを置き換えるのではなく、明示的な例外処理とクリーンアップ手順が必要です。

外部キーには所有関係と削除ルールが必要

外部キーが正しいと言えるのは、その関連が所有、参照、任意の文脈、過去の帰属のどれを表すかをチームが決めてからです。よく似たカラムでも、削除時には正反対の挙動が必要になることがあります。

projects.owner_user_idinvoices.customer_idaudit_events.actor_user_idを考えてみます。プロジェクトは所有者を移せるかもしれません。請求書は顧客アカウントを閉じても残る必要があるかもしれません。監査イベントでは、本人情報を削除した後も、行為者の以前の識別子を保持するかもしれません。すべてがusersを参照しているからといって、三つすべてにON DELETE CASCADEを付けるのは、破壊的な虚構を組み込むことになります。

子が親なしでは意味を持たず、親の削除が本当に集約全体の削除を意味する場合にCASCADEを使います。注文の明細はよく当てはまります。支払い記録、発行済み文書、インポート、ログ、モデレーションの証拠は、そうでないことが多いです。こうした場合は、拒否、アーカイブ、管理された匿名化、またはスナップショットフィールドを残したNULL許容参照のほうが適しているかもしれません。

SET NULLにも意味のレビューが必要です。子行は残りますが、直接の関連は消えます。後でスタッフが、どのアカウントがレポートを作成したか説明する必要があるなら、NULL参照だけでは不十分かもしれません。識別性のない過去のトークンまたはスナップショットを残せば、すべての個人データを保持せずに説明責任を保てます。ただし、正確な保持方法はAIの推測ではなく、プロダクトの方針で決めるべきです。

基数は両方向で確認します。ビルダーは、一意制約なしに外部キーを置くことで一対一をモデル化し、複数の子行をひそかに許すことがあります。履歴には複数バージョンが必要なのに、一意性を強制することもあります。親に対して子がゼロ、一つ、複数あるフィクスチャケースを書き、どのINSERTが成功すべきかを明記してください。

遅延可能な制約には、具体的な理由が必要です。トランザクションで一時的に参照順序を破る必要がある場合や、相互依存する行を更新する場合に役立ちます。しかし、すべての外部キーをデフォルトで遅延にすると、エラーがコミット時まで移り、失敗箇所が見つけにくくなります。実際のトランザクション手順で先送りが必要な場合を除き、即時の強制を維持してください。

リハーサル用データベースにマイグレーションを適用した後、カタログを確認します。

SELECT
    conname,
    contype,
    convalidated,
    pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;

代表的な出力は次のような形になります。

       conname        | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
 orders_pkey          | p       | t            | PRIMARY KEY (id)
 orders_customer_fk   | f       | t            | FOREIGN KEY (customer_id) REFERENCES customers(id)
 orders_total_check   | c       | t            | CHECK ((total_cents >= 0))

返された定義を、承認済みの所有ルールと比較してください。マイグレーションが成功しただけでは、削除アクションの欠落、想定外の遅延、未検証の制約は分かりません。

破壊的変更はもっともらしいSQLの中に隠れる

マイグレーションを確認可能に保つ
生成したソースをエクスポートし、アプリケーションコードと一緒にマイグレーションをチームで確認しましょう。

一見整ったDDLでも意味を捨てることがあるため、マイグレーションはデータ変換としてレビューしてください。まず直接的な破壊を探し、その後にキャスト、バックフィル、再書き込み、リネーム、制約の置き換えを調べます。

リネームと削除は、最終スキーマが同じに見えても運用上は違います。surnamefamily_nameに変えるなら、リネームのほうがデータと依存関係を正しく保てます。古いカラムを削除して新しいカラムを追加しても図は同じですが、値はすべて空になります。生成されたマイグレーションは、継続性を理解せずに最終状態を推測しがちです。

型変更には、変換のサンプルと拒否されるケースが必要です。テキストの識別子を整数に変えると、先頭のゼロを失ったり、英数字混在の識別子を拒否したりします。数値の精度を下げると丸めが起きることがあります。タイムスタンプの変換には、明示したタイムゾーンの前提が必要です。カラムを変更する前に、最小値、最大値、null、不正形式、過去の異例値で実際のUSING式をテストしてください。

次の操作には、文書化した理由が必要です。テーブルまたはカラムの削除、情報を失うキャストによる型変更、値の入ったカラムの置き換え、CASCADEの追加、生成したバックフィル後のNOT NULL設定、異なるカラムでの一意性の作り直しです。生成した関数やマイグレーションコールバックに埋め込まれた生SQLも調べます。テキスト検索は最初の絞り込みであり、レビューのすべてではありません。

繰り返し現れる失敗パターンがあります。既知のデータには、任意の会社に属する連絡先が含まれていますが、サンプル画面には法人連絡先しか出ていません。ビルダーはcontacts.company_idをNOT NULLにし、対応しない行のためにUnknownという名前の会社を生成して挿入します。マイグレーションは成功し、件数は一致し、外部キーもすべて検証されます。それでもデータは誤っています。個人の連絡先が会社に所属しているように見え、レポートは関係のない人をまとめ、プレースホルダーを削除すると実在の連絡先まで連鎖削除されるかもしれません。

修正は別のデフォルト値ではありません。ソース状態を復元し、関連をNULL許容にし、証拠で裏付けられた対応だけを移行し、対応しない集合が既知の個人連絡先と一致するというアサーションを加えます。だからこそ、意味のフィクスチャには期待する関連も記録する必要があり、期待する行数だけでは足りません。

スキーマ差分ツールは役立ちますが、私は差分だけで承認することには反対です。差分はコンパクトでレビューしやすいため、この方法はよく勧められます。しかし、唯一の関門にするのは誤りです。差分が示すのは構造の変更であり、バックフィルした値の由来、トランザクション境界、ロックの挙動、移行後の事実までは示しません。

リハーサルでは結果と失敗時の挙動を証明する

既知のデータセットを復元した使い捨ての環境で、完全なマイグレーションを実行します。その後、意図した結果だけでなく、中断または拒否される経路もテストします。空の新規データベースは順序の誤りを見つけるのに役立ちますが、情報を失う変換、過去の無効な行、遅い検証は見つけられません。

このリハーサル手順をリリース成果物として使ってください。

  1. 変更前のデータセットを隔離したデータベースに復元し、行数と意味に関するアサーションを記録する。
  2. 現在のスキーマを取得し、正確なマイグレーション成果物を適用し、時間とトランザクション境界を含むすべての出力を保存する。
  3. カタログ確認、対応アサーション、制約の拒否テスト、代表的なアプリケーションクエリを実行する。
  4. 対応しないカテゴリとnullのカテゴリを含め、重要な値を記録した期待値と比較する。
  5. 文書化した復旧方法を実行し、復旧後のデータベースでマイグレーション前のアサーションを再実行する。

前後でスキーマのみのダンプを取得します。

pg_dump --schema-only --no-owner --no-privileges \
  --dbname "$DATABASE_URL" > schema.sql

アプリケーションに関係するテーブル、シーケンス、インデックス、制約、関数、トリガー、拡張機能、権限を確認してください。ORMモデルの差分では、アプリケーションがモデル化していないデータベースオブジェクトが漏れることがあります。特にトリガー、式インデックス、部分インデックス、手動で導入した関数が該当します。

無効な状態を制約が拒否することを示す、否定的なテストを加えます。テスト用トランザクションでは、無効なINSERTを試し、結果にかかわらずロールバックできます。

BEGIN;

INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);

ROLLBACK;

期待する出力には、違反した制約名が含まれるべきです。たとえば次のようになります。

ERROR:  new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL:  Failing row contains (..., 0, 2500, ...).

詳細は環境によって変わることがあるため、すべての環境でエラーテキスト全体を比較しないでください。自動テストではSQLSTATEまたは制約の識別子をアサートし、レビュアー向けには読みやすい出力を残します。

意図した展開に似た十分な大きさのデータセットで、ロックと所要時間を測定します。50行で瞬時に終わる操作でも、数百万行を検証すると書き込みを止めることがあります。最初の本番データベースが空なら、直近のリスクは小さくなります。それでもリハーサルは、取り込んだシードデータをテストし、後続の変更の基準を作ります。

復旧には逆方向のマイグレーション以上のものが必要

先にスキーマを計画する
コードを生成する前に、計画モードでテーブルの所有関係、NULL許容、削除の判断を整理しましょう。

復旧が信頼できるのは、サービスが許容できる時間内にデータとアプリケーションの互換性を戻せる場合だけです。削除したカラムを作り直す逆方向のマイグレーションでは、以前の値は戻りません。

実行前に復旧単位を選びます。空の初期データベースなら、ユーザーの書き込みがまだ始まっていない場合に限り、データベースを削除して作り直すことが許容されるかもしれません。実際の書き込みが存在するようになれば、復旧にはデータベースのスナップショット、論理バックアップ、古いカラムの保持、または前向きな修正が必要になることがあります。適切な方法は、マイグレーション中と後にどれだけの新規データが入るかで決まります。

頼りにする前に、復元コマンドと認証情報をテストしてください。バックアップが存在しても、デプロイ担当者が復元できなければ復旧計画にはなりません。別のデータベースに復元し、所有権と拡張機能を確認してから、マイグレーション前に使った既知のアサーションを同じように実行します。

スナップショットとトランザクションロールバックが解決する失敗は異なります。マイグレーションがコミット前に失敗したとき、すべての操作がそのトランザクションに参加していれば、トランザクションは文を元に戻せます。スナップショットはデータベース全体を以前の状態へ戻せますが、スナップショット後に行われた正当な書き込みを捨てることがあります。どちらも、その書き込みを自動で調整しません。

不確実さが残るなら、追加的な変更を優先します。新しいカラムまたはテーブルを追加し、測定可能なルールでデータをコピーし、必要なら管理された期間に両方のコードパスを動かし、検証後にだけ古い構造を取り除きます。この拡張と縮小の方法には追加の作業が必要ですが、証拠を保てます。リネームした古いカラムを一リリース分残すほうが、ログから再構成するより安く済むことが多いです。

復旧を始める条件を前もって書いておきます。たとえば、意味のアサーションが失敗した、想定外に対応しないレコードがある、制約が無効である、マイグレーションが承認済みのロック時間を超えた、バージョン不一致でアプリケーションエラーが起きた、などです。ユーザーが待っている間に、担当者が判断を作り出すべきではありません。

古いデータベースを復元するなら、古いアプリケーションも復元する必要がある時点を記録します。新しいアプリケーションは新しいカラムに依存することがあり、古いアプリケーションは新しいenum値を拒否したり、古い形で書き込んだりするかもしれません。データベースとアプリケーションの復旧では、互換性のあるバージョンを使う必要があります。

承認に必要なのは確信ではなく証拠

保存された成果物から、別の人が安全性の根拠を再現できる場合にだけ、最初のマイグレーションを承認してください。きれいなコードレビューや洗練された生成UIから得た確信は、最初の説明できないデータ不一致の前では役に立ちません。

承認記録には、推測した前提、テーブル対応表、既知のデータセットの識別情報、スキーマ差分、正確なマイグレーション、検証クエリと結果、拒否された入力のテスト、インデックスの理由、破壊的操作の根拠、テスト済みの復旧手順を含めるべきです。レビュアーを記録し、未解決の判断はチャット履歴に埋めず、ブロッカーとして残してください。

Koder.aiでアプリケーションを生成する場合は、マイグレーションを許可する前に計画モードでこれらのスキーマ判断を書き出し、レビュー用にソースをエクスポートしてください。スナップショットとロールバックは、既知のデータセットでリハーサルが必要な復旧ツールとして扱います。

テストに通るまでコードを再生成させることで、ビルダー自身の推測を承認させてはいけません。そのループは、モデルを正すのではなく、アプリケーションを誤ったスキーマに合わせてしまうことがあります。特に本人性、削除、保持、不明な値については、データベースがドメインと一致するかを人が判断しなければなりません。

最終承認のクエリは、退屈なものであるべきです。既知のすべての事実が一つの期待結果に対応し、すべての制約が意図した反例を拒否し、すべての破壊的操作に理由があり、復元後にはマイグレーション前のアサーションが再現されます。不一致を許すために説得力のある説明が必要なら、マイグレーションを止めてください。PostgreSQLはスキーマを正確に強制します。ビルダーが誤って推測した部分も含めてです。

よくある質問

AIが生成したPostgreSQLスキーマでは何を確認すべきですか?

生成されたDDL、マイグレーション操作、その両方にある前提を確認してください。最終的なスキーマが正しくても、データを削除したり書き込みを止めたり、誤解を招くデフォルト値を作ったりするマイグレーションで到達することがあります。

スキーマ検証用のデータセットはどのくらいの大きさが必要ですか?

通常の行、境界値、存在しない関連、重複、NULL、空文字列、過去の例外的なデータを含む小さなデータセットを使います。目的は量ではありません。本番データが問題を起こす前に、ビルダーの前提が誤っていると示すことです。

テスト用マイグレーションが成功すれば、スキーマは安全だと証明できますか?

いいえ。成功したマイグレーションが示すのは、PostgreSQLがその時点のデータベース状態で文を受け入れたことだけです。テーブルの対応付けが正しいこと、データの意味が保たれたこと、インデックスが実際のクエリを支えること、復旧できることまでは証明しません。

PostgreSQLのカラムをNOT NULLにすべきなのはいつですか?

すべての正当なレコードに値があり、アプリケーションがすべての書き込み経路でその値を渡せるときだけ、フィールドをNOT NULLにすべきです。制約を満たすだけの作り物のデフォルト値は使わないでください。見えていた欠損データを、もっともらしい誤ったデータに置き換えてしまいます。

一意制約と一意インデックスのどちらを使うべきですか?

一意制約は、ほかのデータベースオブジェクトから参照できるルールを表し、PostgreSQLはインデックスでそれを支えます。一意インデックスは、削除されていないレコードや正規化したメールアドレスのように、選んだ行や式だけに一意性を適用したい場合に役立ちます。

PostgreSQLの外部キーは自動でインデックスを作成しますか?

親行を探すカラム、よく使うクエリの絞り込みカラム、大きなテーブルの結合カラム、一意性を守るカラムにはインデックスを付けます。PostgreSQLは外部キーの参照元側を自動でインデックス化しないため、制約が処理してくれると思い込まず、子側のカラムを別途確認してください。

ON DELETE CASCADEを安全に使えるのはいつですか?

CASCADEを使うのは、親がなくなった後に子行が独立した意味を持たない場合だけです。削除が業務上の判断を伴う場合や、請求書・監査記録のように子が証跡である場合は、削除を拒否するか、明示的な手順で処理してください。

マイグレーションの破壊的な変更を見つけるにはどうすればよいですか?

すべてのDROP、情報を失うキャスト、テーブルの再書き込み、新しい必須カラム、置き換えた制約は、破壊的になり得るものとして扱います。マイグレーションのテキストを検索するだけでなく、生成された関数や生SQLも調べてください。そこに破壊的な動作が隠れていることがあります。

マイグレーションの復旧を最も安全にテストする方法は何ですか?

変更前のデータベースを別の場所へ復元し、そこでマイグレーションを実行して意味を確認するクエリを走らせ、記録済みの期待値と比較します。逆方向のマイグレーションだけをテストしても、失われたデータを見逃し、誤った安心感を得ることがあります。

スキーマを承認した後、どんな証拠を保存すべきですか?

生成されたDDL、マイグレーションのテキスト、スキーマ差分、検証クエリと結果、復旧手順、レビュアーの情報をまとめて保管してください。その記録が何を承認したかを説明し、次のマイグレーションレビューで変化した前提を見つける助けになります。

Related posts