Query Go
データの整合性を保証 - 制約 (PRIMARY KEY / FOREIGN KEY / UNIQUE / CHECK / NOT NULL) の使い方・オプション・サンプル

データの整合性を保証 - 制約 (PRIMARY KEY / FOREIGN KEY / UNIQUE / CHECK / NOT NULL)

データ整合性を DB レベルで守るための制約。主キー・外部キー・一意・CHECK・複合キー・DEFERRABLE を整理

概念図

制約 (PRIMARY KEY / FOREIGN KEY / UNIQUE / CHECK / NOT NULL) diagram

構文

sql
CONSTRAINT name PRIMARY KEY | FOREIGN KEY | UNIQUE | CHECK (...)

サンプル

PRIMARY KEY・FOREIGN KEY・UNIQUE・CHECK を一通り使った注文テーブル定義

sql
CREATE TABLE orders (
  id          BIGSERIAL PRIMARY KEY,
  user_id     BIGINT      NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
  order_no    VARCHAR(20) NOT NULL,
  amount      NUMERIC(10, 2) NOT NULL CHECK (amount >= 0),
  status      VARCHAR(20) NOT NULL,
  placed_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT uq_orders_user_order_no UNIQUE (user_id, order_no),
  CONSTRAINT ck_orders_status CHECK (status IN ('pending','paid','shipped','canceled'))
);

制約の種類

  • NOT NULL: 列に NULL を許さない
  • UNIQUE: 値の重複を許さない(NULL の扱いは方言差あり)
  • PRIMARY KEY: UNIQUE + NOT NULL の複合。1 テーブル 1 つだけ
  • FOREIGN KEY: 他テーブルの列を参照する整合性
  • CHECK: 任意の条件式を行レベルで検証

制約には列定義に書く列制約と、テーブル末尾に書くテーブル制約の 2 形式があり、複合キーや複雑な CHECK はテーブル制約で書きます。

複合キーと名前付け

複数列を組み合わせたキーは複合キーと呼びます。PRIMARY KEY (tenant_id, id) のようにマルチテナント設計で頻出します。

制約に明示的な名前を付けると、エラーメッセージからどの制約で弾かれたか特定しやすく、マイグレーションでの ALTER も楽になります。命名規約の例: pk_テーブル / fk_テーブル_参照先 / uq_テーブル_列 / ck_テーブル_条件

sql
CREATE TABLE user_roles (
  user_id BIGINT NOT NULL,
  role_id BIGINT NOT NULL,
  assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT pk_user_roles PRIMARY KEY (user_id, role_id),
  CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id),
  CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id)
);

FOREIGN KEY の参照アクション

親レコードが削除・更新されたときの挙動は ON DELETE / ON UPDATE で指定します。

  • RESTRICT / NO ACTION: 子が残っていれば親の削除を拒否(既定)
  • CASCADE: 親に合わせて子も削除・更新
  • SET NULL: 子の参照列を NULL に(NULL 許容が前提)
  • SET DEFAULT: 子の参照列をデフォルト値に

カスケード削除はデータ消失につながるので、論理削除やドメインロジックで扱うほうが安全な場面も多いです。

DEFERRABLE 制約 (PostgreSQL)

PostgreSQL は制約を DEFERRABLE 宣言することで、制約チェックをトランザクションコミット時までに遅らせることができます。相互に参照し合うデータの一括投入や、順序を気にせず更新したい場面で便利です。

MySQL・SQL Server はこの機能を持たないため、ポート時には挿入順の工夫や SET FOREIGN_KEY_CHECKS=0 などの回避策が必要です。

sql
-- PostgreSQL: コミット時までチェック遅延
CREATE TABLE a (
  id INT PRIMARY KEY,
  b_id INT,
  CONSTRAINT fk_a_b FOREIGN KEY (b_id) REFERENCES b(id)
    DEFERRABLE INITIALLY DEFERRED
);

BEGIN;
INSERT INTO a VALUES (1, 10);
INSERT INTO b VALUES (10);
COMMIT; -- ここで制約チェック

落とし穴

  • UNIQUE の NULL 挙動: 標準 SQL / PostgreSQL / MySQL は NULL を重複とみなさないが、SQL Server は 1 つしか許さない。SQL:2023 の UNIQUE NULLS NOT DISTINCT で切替可(PostgreSQL 15+)
  • 外部キーにインデックスが無い: 親の更新・削除で子のフルスキャンが走る。FK 列には基本インデックスを作る(MySQL は自動生成するが PostgreSQL はしない)
  • CHECK に部分的サブクエリ: 多くの RDBMS で CHECK にサブクエリは書けない。トリガーか一意制約で代替
  • 制約名を付けない: 自動生成名は推測困難でマイグレーションが壊れやすい

関連トピック