0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQLのNOT VALIDで制約追加を段階化する:CHECKと外部キーの実務手順

0
Posted at

稼働中のデータベースに「注文金額は0以上」「注文に紐づく顧客が存在する」といった制約を追加したいとします。制約はデータ不整合を防ぐ仕組みですが、大きなテーブルにそのまま追加すると、既存行の検査に時間がかかり、書き込みが長時間待たされる場合があります。

PostgreSQLの NOT VALID → VALIDATE CONSTRAINT を使うと、制約の登録と既存データの全件検証を別々に行えます。この記事ではCHECK(同じ行の値に条件を課す制約)と外部キー(別テーブルの行を参照する制約)を例に、SQL・確認方法・ロックの注意点を解説します。

NOT VALIDは「無停止を保証する」機能ではありません。 本稿は2026年10月時点の現行安定版であるPostgreSQL 18の公式仕様に基づきます。

NOT VALIDで何が変わる?

通常の制約追加では、既存データがルールを守っているか検査されます。データ量が大きいと、その検査が長引くことがあります。

NOT VALID付きで追加すると、その時点で既存行を全件検査しません。ただし、追加後のINSERT・UPDATEには新しい制約が適用されます。つまり「過去の不整合は一時的に残せるが、新しい不整合は防ぐ」状態にできます。

処理 既存行の検査 追加後の新規・更新行
通常のADD CONSTRAINT 追加時に実施 制約を適用
ADD CONSTRAINT ... NOT VALID 追加時は省略 制約を適用
VALIDATE CONSTRAINT この段階で検査 引き続き制約を適用

例:過去に不整合がある注文テーブル

以下は学習用データベースで試すための例です。実サービスのデータには実行しないでください。

CREATE TABLE customers (
  id bigint PRIMARY KEY,
  name text NOT NULL
);

CREATE TABLE orders (
  id bigint PRIMARY KEY,
  customer_id bigint NOT NULL,
  total_cents integer NOT NULL
);

INSERT INTO customers (id, name)
VALUES (1, 'Alice');

INSERT INTO orders (id, customer_id, total_cents) VALUES
  (101, 1, 1200),
  (102, 999, -100);

注文102には二つの問題があります。顧客999は存在せず、金額はマイナスです。こうした既存の不整合がある状態で制約を導入します。

1. 検査を後回しにして制約を追加

ALTER TABLE orders
  ADD CONSTRAINT orders_total_nonnegative
  CHECK (total_cents >= 0) NOT VALID;

ALTER TABLE orders
  ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id)
  REFERENCES customers (id) NOT VALID;

制約名(orders_total_nonnegativeなど)を指定しておくと、後続の検証や障害調査で対象を間違えにくくなります。ここでは既存行102が不整合でも、登録時には全件走査しません。

一方、追加後の次のSQLはそれぞれエラーになります。

-- 存在しない顧客を参照するため外部キー違反
INSERT INTO orders VALUES (103, 999, 100);

-- 金額がマイナスなのでCHECK違反
INSERT INTO orders VALUES (104, 1, -1);

なお、CHECK (total_cents >= 0)だけではNULLは禁止できません。SQLのCHECKは式がFALSEの場合に違反と判定するためです。この例では列定義のNOT NULLでも保護しています。

2. 未検証であることを確認

PostgreSQL内部のpg_constraint(制約の管理情報を持つシステムカタログ)を見ると、検証状態が確認できます。

SELECT conname, contype, convalidated
FROM pg_constraint
WHERE conrelid = 'orders'::regclass
  AND conname IN (
    'orders_total_nonnegative',
    'orders_customer_fk'
  )
ORDER BY conname;

contypeはcがCHECK、fが外部キーです。convalidated = falseは「既存行を全件検証していない」を意味します。登録成功と既存データ検証完了を混同しないことが重要です。

3. 既存データの問題を調べる

-- CHECKに違反している既存注文
SELECT id, total_cents
FROM orders
WHERE total_cents < 0;

-- 参照先が存在しない既存注文
SELECT o.id, o.customer_id
FROM orders AS o
LEFT JOIN customers AS c ON c.id = o.customer_id
WHERE c.id IS NULL;

この状態でVALIDATE CONSTRAINTを実行すると、注文102の違反を検出して失敗します。これは期待した動作です。検証失敗後も登録した制約は残り、新規データの違反防止は続きます。

デモ用データなら次のように修正できます。

UPDATE orders
SET customer_id = 1, total_cents = 100
WHERE id = 102;

本番では金額・顧客IDを勝手に補正しないでください。 データの意味や監査上の要件を確認し、正しい値への修復、データ隔離、業務上の取消などを選びます。大量更新は処理を小分けにして、ロックとI/Oへの影響を監視します。

4. 既存データを検証して完了させる

ALTER TABLE orders
  VALIDATE CONSTRAINT orders_total_nonnegative;

ALTER TABLE orders
  VALIDATE CONSTRAINT orders_customer_fk;

両方のSQLが成功した後、pg_constraint.convalidatedがtrueになっていることを確認します。検証が失敗したら、対象行をさらに調査し、修正後に再実行します。

注意:NOT VALIDでもロックは発生する

ロックは、別々のSQLが同じテーブルへ同時に矛盾した操作をしないようにする仕組みです。PostgreSQL 18の公式仕様は、ロックの違いも明示しています。

操作 ロックと注意点
ADD CHECK ... NOT VALID 主にACCESS EXCLUSIVE。全件スキャンを省いても、取得中は他処理をブロックし得る
ADD FOREIGN KEY ... NOT VALID 参照元・参照先にSHARE ROW EXCLUSIVE。両テーブルの更新と競合し得る
VALIDATE CONSTRAINT 対象表にSHARE UPDATE EXCLUSIVE。通常の書き込みを一律に止めずに検査できる。外部キーでは参照先にROW SHAREも必要

追加するCHECKのロックが重い点は重要です。NOT VALIDにしただけでロックが消えるわけではありません。別の長いトランザクションがロックを保持していると、制約追加が待機し、その後ろのSQLにも影響が波及することがあります。

ロック待ち時間を制限するSQLの例です。

BEGIN;
SET LOCAL lock_timeout = '3s';

ALTER TABLE orders
  ADD CONSTRAINT orders_total_nonnegative
  CHECK (total_cents >= 0) NOT VALID;

COMMIT;
  • BEGIN:トランザクション(変更を一括で確定・取り消しする処理単位)を開始します。
  • SET LOCAL lock_timeout = '3s':このトランザクション中のロック取得待ちを3秒までに制限します。SQL全体の実行時間の上限ではありません。
  • COMMIT:正常時に確定してロックを解放します。エラーが出たらROLLBACKし、状況を確認します。

上の例は制約がまだ存在しないテーブルに対する独立した実行例です。先ほどのデモを同じデータベースで順番に実行している場合は、同じ制約名で再追加するとエラーになるため実行しないでください。

よくあるハマりどころ

外部キーの参照元にインデックスがない

外部キーを追加しても、参照元の列へインデックスが自動で作られるわけではありません。顧客行を削除・更新するときに参照元の注文を探す処理が重い場合は、アクセスパターンと負荷を確認してインデックスを検討します。

CREATE INDEX CONCURRENTLY orders_customer_id_idx
  ON orders (customer_id);

CONCURRENTLYは書き込みへの影響を抑えながらインデックスを作る方法ですが、作業負荷がゼロになるわけではありません。また、明示的なトランザクションブロック内では実行できません。制約追加とは別のマイグレーションにします。

UNIQUEやPRIMARY KEYにも使える?

NOT VALIDはすべての制約に付けられるオプションではありません。PostgreSQL 18では外部キー・CHECK・NOT NULL制約が対象です。UNIQUEやPRIMARY KEYでは、適切な条件を満たす場合、CREATE UNIQUE INDEX CONCURRENTLYでインデックスを用意してからADD CONSTRAINT ... USING INDEXにする別手順を検討します。

「制約追加済み」と「検証完了」を混同する

NOT VALID追加直後は、将来の不整合を防ぎながらも、過去の不整合が残り得ます。convalidated = trueの確認までをマイグレーションの完了条件として管理すれば、途中で止まった状態にも気づけます。

どんな場面で使い分ける?

  • 小さなテーブル・すでに全件が正しい:通常の制約追加の方が簡潔です。
  • 大きな稼働中テーブル:登録と検証の時間帯を分けるNOT VALID方式を検討します。
  • 古い不整合がある:まず追加後の違反を防ぎ、既存データを業務的に正しく修復してから検証します。
  • 高トラフィック環境:ロック待ち、更新競合、I/O負荷、失敗時の復旧をステージングで測定してから適用します。

結論は、新しい不整合を止めることと、既存データが正しいと検証することを分けることです。段階ごとの状態を明示すれば、移行作業を観測しやすくなり、安全性を高められます。

参考資料(公式)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?