Bytebase の SQL レビュー には、CHECK 制約を追加する際に "NOT VALID" オプションを必須にするルールがあります。
CHECK 制約の追加は既存データの検証を伴い、ACCESS EXCLUSIVE のテーブルロックを必要とします。これは読み書きをブロックし、業務中断を招き得ます。新規データの検証には "NOT VALID" オプションを付け、変更完了後に既存データを手動で検証することを推奨します。
PostgreSQL ではテーブルにデータ品質ルールを強制するために CHECK 制約を追加できます。これは便利ですが、間違ったやり方で CHECK 制約を追加すると読み書きがブロックされ、想定外のダウンタイムを招きます。
本 SQL レビュールールは、すべての新規 CHECK 制約が安全でノンブロッキングな方法で作成されることを保証します。
CHECK 制約の仕組み
CHECK 制約は、カラムの値が指定の条件を満たすことを保証します。例:
CHECK (amount > 0)PostgreSQL はこのルールに違反する INSERT/UPDATE を拒否します。
CHECK 制約を追加するには通常こう書きます。
ALTER TABLE orders
ADD CONSTRAINT orders_positive CHECK (amount > 0);これは将来の書き込みにルールを強制すると同時に、既存の全行を直ちに検証します。 リスクはここから来ます。
CHECK 制約の追加でなぜダウンタイムが起きるのか
PostgreSQL が既存テーブルを検証するとき、制約に違反していないかを確認するために全行を走査する必要があります。この間、ACCESS EXCLUSIVE ロックを取得します。これは PostgreSQL で最も強いロックで、次をブロックします。
- 読み取り
- 書き込み
- 他のスキーマ変更
大きなテーブルや忙しい本番データベースでは、このロックが次を引き起こします。
- クエリタイムアウト
- アプリケーションエラー
- サービス劣化
- 全面的な停止
本 SQL レビュールールは、デプロイ中に偶発的にブロッキングなスキーマ変更が紛れ込むのを防ぎます。
安全なやり方: NOT VALID を使う
PostgreSQL には、既存テーブルに CHECK 制約を追加するためのより安全な方法があります。 発想は、制約の作成と検証を切り分けることです。
ステップ 1: 既存行を検証せずに制約を作成する
ALTER TABLE orders
ADD CONSTRAINT orders_positive CHECK (amount > 0) NOT VALID;- 必要なのは短いカタログロックだけ
- 制約は新規 INSERT/UPDATE すべてに対して強制される
- 既存行はまだ走査されない
ステップ 2: 都合の良いタイミングで検証する
ALTER TABLE orders
VALIDATE CONSTRAINT orders_positive;検証は読み書きをブロックしない、より軽いロックを使います。 低トラフィックの時間帯や、計画されたロールアウトの一部として実行できます。
例
安全でないパターン (許可されない):
ALTER TABLE accounts
ADD CONSTRAINT check_balance CHECK (balance >= 0);安全なパターン (推奨):
ALTER TABLE accounts
ADD CONSTRAINT check_balance CHECK (balance >= 0) NOT VALID;別途検証:
ALTER TABLE accounts
VALIDATE CONSTRAINT check_balance;これによりユーザートラフィックを乱さずに制約を安全に適用できます。
まとめ
NOT VALID を付けずに CHECK 制約を追加すると、読み書きがブロックされ、ダウンタイムにつながります。
本 SQL レビュールールは、PostgreSQL の 2 段階のベストプラクティスを強制します。
NOT VALID付きで制約を追加する- ノンブロッキングな操作で後から検証する
このパターンに従うことで、本番の安定性を損なうことなく制約の正しさを担保できます。