SQLの制約とは、テーブルに入れてよいデータの条件を決め、条件に合わないデータをデータベース自身が受け付けないようにするルールです。 代表的なものは PRIMARY KEY(主キー)・UNIQUE・NOT NULL・CHECK・FOREIGN KEY(外部キー)・DEFAULT の6種類で、CREATE TABLE のときにも、あとから ALTER TABLE でも追加・削除できます。

この記事では、6種類を 「役割/NULL可否/1テーブルに何個/追加構文/削除構文」の1枚の一覧表 にまとめ、追加・削除の ALTER TABLE 文を MySQL/PostgreSQL/Oracle/SQL Server で並べて 比べます。さらに、各制約に違反したときの 実際のエラーメッセージ と、既存データが違反していて制約を追加できないときの対処 を、MySQL 8.0.46 と PostgreSQL 16.15 で動かした出力つきで紹介します。

SQL制約6種の早見表

制約 役割 NULL を入れられるか 1テーブルに何個 追加構文(基本形) 削除構文(基本形)
PRIMARY KEY 行を1つに特定する。重複も NULL も受け付けない 入れられない 1個だけ(複数列を組み合わせた複合キーは可) ALTER TABLE t ADD CONSTRAINT pk_t PRIMARY KEY (id) DROP CONSTRAINT pk_t(MySQL は DROP PRIMARY KEY)
UNIQUE 列の値の重複を受け付けない 入れられる(複数の NULL の扱いは DB で違う) いくつでも ALTER TABLE t ADD CONSTRAINT uq_t_col UNIQUE (col) DROP CONSTRAINT uq_t_col(MySQL は DROP INDEX も可)
NOT NULL 空(NULL)を受け付けない 入れられない 列ごとに1つ DB ごとに違う(MODIFY / ALTER COLUMN) DB ごとに違う
CHECK 条件式を満たす値だけを受け付ける 入れられる(NULL は条件を通過する) いくつでも ALTER TABLE t ADD CONSTRAINT ck_t_col CHECK (col >= 0) DROP CONSTRAINT ck_t_col(MySQL は DROP CHECK も可)
FOREIGN KEY 別テーブル(親)に存在する値だけを受け付ける 入れられる(NULL は親を探さない) いくつでも ALTER TABLE t ADD CONSTRAINT fk_t_p FOREIGN KEY (p_id) REFERENCES p (id) DROP CONSTRAINT fk_t_p(MySQL は DROP FOREIGN KEY)
DEFAULT 値を省略したときに入る既定値を決める 入れられる(明示的に NULL を渡せば NULL) 列ごとに1つ ALTER TABLE t ALTER COLUMN col SET DEFAULT 値(Oracle・SQL Server は別の書き方) ALTER TABLE t ALTER COLUMN col DROP DEFAULT(同左)
  • DEFAULT は「入れてはいけない値を防ぐ」ルールではありませんが、列の定義に付ける決まりごととして、制約と一緒に説明されることが多いためここに含めています。SQL Server では DEFAULT も名前付きの制約として扱われます。
  • MySQL と PostgreSQL の構文とエラーは今回実行して確かめた内容です。Oracle と SQL Server は手元に実行環境がないため未実行 で、一般に知られた仕様として載せています(SQL Server の WITH NOCHECK と DEFAULT ... FOR 列 は、Microsoft Learn の ALTER TABLE table_constraint のページを開いて確認しました)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL制約一覧|主キー・UNIQUE・CHECK・NOT NULLの追加と削除」

CREATE TABLEで6種類の制約をまとめて定義する

まず、6種類すべてを含むテーブルを作ります。制約には CONSTRAINT 名前 で名前を付けておく と、あとで削除するときやエラーメッセージを読むときに役立ちます。次の SQL は MySQL 8.0.46 と PostgreSQL 16.15 の両方でそのまま実行できました。

CREATE TABLE departments (
  dept_id   INT          NOT NULL,
  dept_name VARCHAR(30)  NOT NULL,
  CONSTRAINT pk_departments PRIMARY KEY (dept_id)
);

CREATE TABLE members (
  member_id  INT          NOT NULL,
  email      VARCHAR(100) NOT NULL,
  name       VARCHAR(50)  NOT NULL,
  age        INT,
  status     VARCHAR(10)  DEFAULT 'active' NOT NULL,
  dept_id    INT,
  CONSTRAINT pk_members       PRIMARY KEY (member_id),
  CONSTRAINT uq_members_email UNIQUE (email),
  CONSTRAINT ck_members_age   CHECK (age >= 0),
  CONSTRAINT fk_members_dept  FOREIGN KEY (dept_id) REFERENCES departments (dept_id)
);

INSERT INTO departments VALUES (10, '開発'), (20, '総務');
INSERT INTO members (member_id, email, name, age, dept_id)
VALUES (1, 'sato@example.com', '佐藤', 30, 10);

status を省略して INSERT したので、DEFAULT の 'active' が入っています。

+-----------+------------------+--------+------+--------+---------+
| member_id | email            | name   | age  | status | dept_id |
+-----------+------------------+--------+------+--------+---------+
|         1 | sato@example.com | 佐藤   |   30 | active |      10 |
+-----------+------------------+--------+------+--------+---------+

制約名の付け方に決まりはありませんが、次のように種類がわかる接頭辞をそろえると読みやすくなります。

制約 名前の例 接頭辞の意味
PRIMARY KEY pk_members pk = primary key
UNIQUE uq_members_email uq = unique
CHECK ck_members_age ck = check
FOREIGN KEY fk_members_dept fk = foreign key
DEFAULT(SQL Server) df_members_status df = default

制約に違反したときのエラーメッセージ一覧

上のテーブルに、わざと制約に違反する INSERT を実行しました。エラーメッセージに制約名が出るので、名前を付けておくとどのルールに当たったかがすぐわかります。

MySQL 8.0.46 の実行結果

違反の内容 実行した値 エラーメッセージ
主キーの重複 member_id = 1(登録済み) ERROR 1062 (23000): Duplicate entry '1' for key 'members.PRIMARY'
主キーに NULL member_id = NULL ERROR 1048 (23000): Column 'member_id' cannot be null
UNIQUE の重複 email = 'sato@example.com'(登録済み) ERROR 1062 (23000): Duplicate entry 'sato@example.com' for key 'members.uq_members_email'
NOT NULL に NULL name = NULL ERROR 1048 (23000): Column 'name' cannot be null
NOT NULL の列を省略(DEFAULT なし) name を列リストから外す ERROR 1364 (HY000): Field 'name' doesn't have a default value
CHECK 違反 age = -1 ERROR 3819 (HY000): Check constraint 'ck_members_age' is violated.
外部キー違反(親にない値) dept_id = 99 ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`t180`.`members`, CONSTRAINT `fk_members_dept` FOREIGN KEY (`dept_id`) REFERENCES `departments` (`dept_id`))
外部キー違反(参照されている親を削除・更新) DELETE FROM departments WHERE dept_id = 10 ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`t180`.`members`, CONSTRAINT `fk_members_dept` ...)
参照されている親テーブルを DROP DROP TABLE departments ERROR 3730 (HY000): Cannot drop table 'departments' referenced by a foreign key constraint 'fk_members_dept' on table 'members'.
主キーを2つ定義 a INT PRIMARY KEY, b INT PRIMARY KEY ERROR 1068 (42000): Multiple primary key defined

PostgreSQL 16.15 の実行結果

PostgreSQL は ERROR の次の行に DETAIL として、当たった値や行の中身を表示します。

違反の内容 エラーメッセージ
主キーの重複 ERROR: duplicate key value violates unique constraint "pk_members" / DETAIL: Key (member_id)=(1) already exists.
主キーに NULL ERROR: null value in column "member_id" of relation "members" violates not-null constraint
UNIQUE の重複 ERROR: duplicate key value violates unique constraint "uq_members_email" / DETAIL: Key (email)=(sato@example.com) already exists.
NOT NULL に NULL、または列を省略 ERROR: null value in column "name" of relation "members" violates not-null constraint / DETAIL: Failing row contains (2, suzuki@example.com, null, 25, active, 10).
CHECK 違反 ERROR: new row for relation "members" violates check constraint "ck_members_age"
外部キー違反(親にない値) ERROR: insert or update on table "members" violates foreign key constraint "fk_members_dept" / DETAIL: Key (dept_id)=(99) is not present in table "departments".
外部キー違反(参照されている親を削除・更新) ERROR: update or delete on table "departments" violates foreign key constraint "fk_members_dept" on table "members" / DETAIL: Key (dept_id)=(10) is still referenced from table "members".
参照されている親テーブルを DROP ERROR: cannot drop table departments because other objects depend on it
主キーを2つ定義 ERROR: multiple primary keys for table "two_pk" are not allowed

Oracle と SQL Server の代表的なエラー番号(未実行)

違反の内容 Oracle SQL Server
主キー・UNIQUE の重複 ORA-00001 メッセージ 2627(一意インデックスの重複は 2601)
NOT NULL に NULL ORA-01400 メッセージ 515
CHECK 違反 ORA-02290 メッセージ 547
外部キー違反(親にない値) ORA-02291 メッセージ 547
外部キー違反(子が残っている親を削除) ORA-02292 メッセージ 547

この表は一般に知られた番号を並べたもので、今回は実行していません。メッセージ全文はお使いの環境で確かめてください。

NULLと制約の関係(つまずきやすい3点)

UNIQUEの列には複数のNULLを入れられる

CREATE TABLE memo (
  id   INT PRIMARY KEY,
  code VARCHAR(10),
  CONSTRAINT uq_memo_code UNIQUE (code)
);
INSERT INTO memo VALUES (1, NULL), (2, NULL), (3, 'A');
SELECT * FROM memo;
+----+------+
| id | code |
+----+------+
|  1 | NULL |
|  2 | NULL |
|  3 | A    |
+----+------+

MySQL 8.0.46 と PostgreSQL 16.15 では、UNIQUE の列に NULL を2行入れられました。 NULL は「値がわからない」という意味なので、NULL どうしは「同じ」と判定されないためです。一方、'A' をもう1行入れると重複エラーになります。

DB UNIQUE 列に複数の NULL
MySQL 入れられる(今回確認)
PostgreSQL 入れられる(今回確認)。15 以降は UNIQUE NULLS NOT DISTINCT で1つまでにできる
Oracle 単一列の UNIQUE なら入れられる(未実行)
SQL Server 1つだけ。2つ目の NULL は重複として扱われる(未実行)

PostgreSQL の NULLS NOT DISTINCT を試した結果です。

CREATE TABLE memo2 (
  id   INT PRIMARY KEY,
  code VARCHAR(10),
  CONSTRAINT uq_memo2 UNIQUE NULLS NOT DISTINCT (code)
);
INSERT INTO memo2 VALUES (1, NULL);
INSERT INTO memo2 VALUES (2, NULL);
ERROR:  duplicate key value violates unique constraint "uq_memo2"
DETAIL:  Key (code)=(null) already exists.

「未入力は許すが、入力されたら重複させない」なら通常の UNIQUE、「必ず入力させて重複もさせない」なら UNIQUE と NOT NULL の両方を付けます。

CHECKと外部キーはNULLを通す

INSERT INTO members (member_id, email, name, age, dept_id)
VALUES (2, 'suzuki@example.com', '鈴木', NULL, NULL);

age には CHECK (age >= 0)、dept_id には外部キーがありますが、どちらも NULL はそのまま登録できました(MySQL・PostgreSQL とも)。CHECK は条件式の結果が「偽」のときだけ拒否し、NULL と比較した結果(不明)は通過させるためです。NULL も受け付けたくない列には、NOT NULL を合わせて付けます。

主キーとUNIQUEの違い

比べる点 PRIMARY KEY UNIQUE
重複 不可 不可
NULL 不可(自動で NOT NULL になる) 可
1テーブルに作れる数 1個 いくつでも
主な用途 行を特定する ID メールアドレス、社員番号、コードなど「ID ではないが重複させたくない値」
外部キーから参照できるか できる できる

主キーを2つ作ろうとするとエラーになりますが、複数の列を組み合わせた1つの主キー(複合主キー) は作れます。

CREATE TABLE comp_pk (a INT, b INT, PRIMARY KEY (a, b));

ALTER TABLEで制約を追加する(4DB比較)

テーブルを作ったあとに制約を足すときは ALTER TABLE を使います。PRIMARY KEY・UNIQUE・CHECK・FOREIGN KEY は4つの DB でほぼ同じ形、NOT NULL と DEFAULT は DB ごとに書き方が違います。

制約 MySQL PostgreSQL Oracle SQL Server
PRIMARY KEY ALTER TABLE members ADD CONSTRAINT pk_members PRIMARY KEY (member_id); 同じ 同じ 同じ
UNIQUE ALTER TABLE members ADD CONSTRAINT uq_members_email UNIQUE (email); 同じ 同じ 同じ
CHECK ALTER TABLE members ADD CONSTRAINT ck_members_age CHECK (age >= 0); 同じ 同じ 同じ
FOREIGN KEY ALTER TABLE members ADD CONSTRAINT fk_members_dept FOREIGN KEY (dept_id) REFERENCES departments (dept_id); 同じ 同じ 同じ
NOT NULL ALTER TABLE members MODIFY name VARCHAR(50) NOT NULL; ALTER TABLE members ALTER COLUMN name SET NOT NULL; ALTER TABLE members MODIFY name NOT NULL; ALTER TABLE members ALTER COLUMN name VARCHAR(50) NOT NULL;
DEFAULT ALTER TABLE members ALTER COLUMN status SET DEFAULT 'active'; 同じ ALTER TABLE members MODIFY status DEFAULT 'active'; ALTER TABLE members ADD CONSTRAINT df_members_status DEFAULT 'active' FOR status;

ポイントを補足します。

  • MySQL の MODIFY と SQL Server の ALTER COLUMN は、列の型も一緒に書きます。 型を書き忘れたり、違う型を書いたりすると列の定義が変わるので、SHOW CREATE TABLE などで現在の型を確かめてから実行すると安心です。
  • PostgreSQL と Oracle は型を書かずに NOT NULL だけを付け外しできます。
  • MySQL の CHECK 制約が実際に働くのは 8.0.16 以降です。それより前の版では構文として受け付けるだけで、検査は行われないことが知られています。
  • MySQL では、PRIMARY KEY に CONSTRAINT pk_members と名前を付けても、制約名は常に PRIMARY になります(後述の確認クエリでも PRIMARY と表示されました)。

ALTER TABLEで制約を削除する(4DB比較)

制約 MySQL PostgreSQL Oracle SQL Server
PRIMARY KEY ALTER TABLE members DROP PRIMARY KEY; ALTER TABLE members DROP CONSTRAINT pk_members; ALTER TABLE members DROP CONSTRAINT pk_members;(DROP PRIMARY KEY も可) ALTER TABLE members DROP CONSTRAINT pk_members;
UNIQUE ALTER TABLE members DROP INDEX uq_members_email;(8.0.19 以降は DROP CONSTRAINT も可) ALTER TABLE members DROP CONSTRAINT uq_members_email; 同左 同左
CHECK ALTER TABLE members DROP CHECK ck_members_age;(DROP CONSTRAINT も可) ALTER TABLE members DROP CONSTRAINT ck_members_age; 同左 同左
FOREIGN KEY ALTER TABLE members DROP FOREIGN KEY fk_members_dept;(DROP CONSTRAINT も可) ALTER TABLE members DROP CONSTRAINT fk_members_dept; 同左 同左
NOT NULL ALTER TABLE members MODIFY name VARCHAR(50) NULL; ALTER TABLE members ALTER COLUMN name DROP NOT NULL; ALTER TABLE members MODIFY name NULL; ALTER TABLE members ALTER COLUMN name VARCHAR(50) NULL;
DEFAULT ALTER TABLE members ALTER COLUMN status DROP DEFAULT; 同じ ALTER TABLE members MODIFY status DEFAULT NULL; ALTER TABLE members DROP CONSTRAINT df_members_status;

MySQLで実行して確かめたこと

6種類を順に削除し、SHOW CREATE TABLE で確認しました。

ALTER TABLE members DROP FOREIGN KEY fk_members_dept;
ALTER TABLE members DROP CHECK ck_members_age;
ALTER TABLE members DROP INDEX uq_members_email;
ALTER TABLE members MODIFY name VARCHAR(50) NULL;
ALTER TABLE members ALTER COLUMN status DROP DEFAULT;
ALTER TABLE members DROP PRIMARY KEY;
SHOW CREATE TABLE members;
CREATE TABLE `members` (
  `member_id` int NOT NULL,
  `email` varchar(100) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `status` varchar(10) NOT NULL,
  `dept_id` int DEFAULT NULL,
  KEY `fk_members_dept` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

ここから読み取れる MySQL の特徴は3つです。

  • 外部キーを削除しても、同じ名前のインデックス(KEY fk_members_dept)は残りました。 不要なら ALTER TABLE members DROP INDEX fk_members_dept; で別に削除します。
  • 主キーを削除しても、列の NOT NULL は残りました(member_id int NOT NULL)。
  • MySQL の UNIQUE 制約は一意インデックスとして作られるため、DROP INDEX で削除できます。

MySQL 8.0.19 以降では、種類を問わない DROP CONSTRAINT 制約名 も使えます。8.0.46 で UNIQUE・CHECK・FOREIGN KEY の3つは DROP CONSTRAINT で削除できました。主キーだけは名前が PRIMARY に固定されているため、作成時に付けた名前では次のエラーになります。

ALTER TABLE members DROP CONSTRAINT pk_members;
ERROR 3940 (HY000) at line 83: Constraint 'pk_members' does not exist.

MySQL の主キーは DROP PRIMARY KEY で削除する、と覚えておきましょう。

PostgreSQLで実行して確かめたこと

ALTER TABLE members DROP CONSTRAINT fk_members_dept;
ALTER TABLE members DROP CONSTRAINT ck_members_age;
ALTER TABLE members DROP CONSTRAINT uq_members_email;
ALTER TABLE members ALTER COLUMN name DROP NOT NULL;
ALTER TABLE members ALTER COLUMN status DROP DEFAULT;
ALTER TABLE members DROP CONSTRAINT pk_members;

PostgreSQL は PRIMARY KEY・UNIQUE・CHECK・FOREIGN KEY の4つを、すべて DROP CONSTRAINT 制約名 で削除 できます。存在しない制約名を指定したときの挙動は次のとおりでした。

ALTER TABLE members DROP CONSTRAINT pk_members;            -- 2回目
ALTER TABLE members DROP CONSTRAINT IF EXISTS pk_members;
ERROR:  constraint "pk_members" of relation "members" does not exist
NOTICE:  constraint "pk_members" of relation "members" does not exist, skipping

繰り返し実行する移行用スクリプトでは IF EXISTS を付けておくと、2回目以降も止まらずに進みます。制約名だけを変えたいときは、削除せずに次の文で変更できました。

ALTER TABLE members RENAME CONSTRAINT ck_members_age TO ck_members_age_nonneg;

制約名の調べ方

削除には制約名が必要です。名前を付けずに作った場合や、ほかの人が作ったテーブルでは、次の方法で調べます。

DB 調べ方
MySQL SHOW CREATE TABLE テーブル名; / information_schema.TABLE_CONSTRAINTS
PostgreSQL psql の \d テーブル名 / pg_constraint / information_schema.table_constraints
Oracle USER_CONSTRAINTS(CONSTRAINT_TYPE が P・U・C・R)と USER_CONS_COLUMNS(未実行)
SQL Server EXEC sp_helpconstraint 'テーブル名'; / INFORMATION_SCHEMA.TABLE_CONSTRAINTS / sys.default_constraints など(未実行)

MySQL

SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE, ENFORCED
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = 't180' AND TABLE_NAME = 'members';
+------------------+-----------------+----------+
| CONSTRAINT_NAME  | CONSTRAINT_TYPE | ENFORCED |
+------------------+-----------------+----------+
| PRIMARY          | PRIMARY KEY     | YES      |
| uq_members_email | UNIQUE          | YES      |
| fk_members_dept  | FOREIGN KEY     | YES      |
| ck_members_age   | CHECK           | YES      |
+------------------+-----------------+----------+

TABLE_SCHEMA には自分のデータベース名を入れます。CHECK の条件式は information_schema.CHECK_CONSTRAINTS、外部キーの参照先は information_schema.KEY_COLUMN_USAGE で確認できました。

SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 't180' AND TABLE_NAME = 'members';
+------------------+-------------+-----------------------+------------------------+
| CONSTRAINT_NAME  | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+------------------+-------------+-----------------------+------------------------+
| PRIMARY          | member_id   | NULL                  | NULL                   |
| uq_members_email | email       | NULL                  | NULL                   |
| fk_members_dept  | dept_id     | departments           | dept_id                |
+------------------+-------------+-----------------------+------------------------+

PostgreSQL

SELECT conname, contype, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'members'::regclass
ORDER BY contype;
     conname      | contype |                      definition                       
------------------+---------+-------------------------------------------------------
 ck_members_age   | c       | CHECK ((age >= 0))
 fk_members_dept  | f       | FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
 pk_members       | p       | PRIMARY KEY (member_id)
 uq_members_email | u       | UNIQUE (email)

contype は c が CHECK、f が外部キー、p が主キー、u が UNIQUE です。psql を使っているなら \d members でも、列の NOT NULL・DEFAULT と合わせて一覧できます。

Indexes:
    "pk_members" PRIMARY KEY, btree (member_id)
    "uq_members_email" UNIQUE CONSTRAINT, btree (email)
Check constraints:
    "ck_members_age" CHECK (age >= 0)
Foreign-key constraints:
    "fk_members_dept" FOREIGN KEY (dept_id) REFERENCES departments(dept_id)

なお、PostgreSQL 16.15 で information_schema.table_constraints を見ると、NOT NULL が 2200_180384_1_not_null のような名前の CHECK として4行表示されました。自分で付けた制約だけを見たいときは pg_constraint のほうが読みやすいです。

名前を付けなかったときに自動で付く名前

-- MySQL
CREATE TABLE auto_name (
  id INT PRIMARY KEY,
  code VARCHAR(10) UNIQUE,
  qty INT CHECK (qty > 0),
  dept_id INT,
  FOREIGN KEY (dept_id) REFERENCES departments (dept_id)
);
制約 MySQL 8.0.46 PostgreSQL 16.15
PRIMARY KEY PRIMARY auto_name_pkey
UNIQUE code(列名と同じ) auto_name_code_key
CHECK auto_name_chk_1 auto_name_qty_check
FOREIGN KEY auto_name_ibfk_1 auto_name_dept_id_fkey

(PostgreSQL 側は dept_id INT REFERENCES departments (dept_id) と列に直接書いて確認しました。)Oracle では SYS_C で始まる連番、SQL Server では PK__テーブル名__英数字 のような名前が自動で付くことが知られています(未実行)。環境ごとに違う名前になると削除用の SQL を共通化しにくいので、最初から名前を付けておくのがおすすめです。

既存データが違反していて制約を追加できないときの対処

運用中のテーブルに制約を足そうとすると、すでに入っているデータが条件に合わず、追加そのものがエラーになることがあります。制約を外した状態で、わざと違反する行を入れて再現しました。

-- 重複した member_id・email、name が NULL、age が負、存在しない dept_id を持つ行
INSERT INTO members (member_id, email, name, age, status, dept_id)
VALUES (1, 'sato@example.com', NULL, -5, 'x', 99);

追加時のエラーメッセージ

追加しようとした制約 MySQL 8.0.46 PostgreSQL 16.15
PRIMARY KEY ERROR 1062 (23000): Duplicate entry '1' for key 'members.PRIMARY' ERROR: could not create unique index "pk_members" / DETAIL: Key (member_id)=(1) is duplicated.
UNIQUE ERROR 1062 (23000): Duplicate entry 'sato@example.com' for key 'members.uq_members_email' ERROR: could not create unique index "uq_members_email" / DETAIL: Key (email)=(sato@example.com) is duplicated.
NOT NULL ERROR 1138 (22004): Invalid use of NULL value ERROR: column "name" of relation "members" contains null values
CHECK ERROR 3819 (HY000): Check constraint 'ck_members_age' is violated. ERROR: check constraint "ck_members_age" of relation "members" is violated by some row
FOREIGN KEY ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`t180`.`#sql-af8_e`, CONSTRAINT `fk_members_dept` ...) ERROR: insert or update on table "members" violates foreign key constraint "fk_members_dept" / DETAIL: Key (dept_id)=(99) is not present in table "departments".

MySQL の外部キーのメッセージでは、テーブル名が #sql-af8_e という内部の作業用の名前で表示されました。自分のテーブル名と違って見えますが、原因は「親テーブルにない値がある」ことです。

手順1:違反している行を探す

エラーが出たら、まず どの行が条件に合っていないのかを SELECT で見つけます。 次のクエリは MySQL・PostgreSQL 共通です。

-- 主キー・UNIQUE にしたい列の重複
SELECT member_id, COUNT(*) FROM members GROUP BY member_id HAVING COUNT(*) > 1;
SELECT email, COUNT(*) FROM members GROUP BY email HAVING COUNT(*) > 1;

-- NOT NULL にしたい列の NULL
SELECT * FROM members WHERE name IS NULL;

-- CHECK の条件を満たさない行
SELECT * FROM members WHERE NOT (age >= 0);

-- 外部キー:親テーブルに存在しない値
SELECT m.*
FROM members m
LEFT JOIN departments d ON d.dept_id = m.dept_id
WHERE m.dept_id IS NOT NULL AND d.dept_id IS NULL;

MySQL 8.0.46 では、重複の2クエリが 1 / 2件 と sato@example.com / 2件 を返し、残りの3クエリはいずれも先ほど入れた1行を返しました。

+-----------+------------------+------+------+--------+---------+
| member_id | email            | name | age  | status | dept_id |
+-----------+------------------+------+------+--------+---------+
|         1 | sato@example.com | NULL |   -5 | x      |      99 |
+-----------+------------------+------+------+--------+---------+

手順2:データを直してから追加する

違反の種類 直し方の例
重複 残す行を決めて、ほかを削除または値を変更する
NULL UPDATE members SET name = '(未設定)' WHERE name IS NULL; のように仮の値を入れる
CHECK 条件に合う値へ更新する、または条件そのものを見直す
外部キー 親テーブルに行を追加する、子の値を NULL や正しい値に更新する

今回は違反行を削除してから追加し直し、MySQL・PostgreSQL ともに5つすべてがエラーなく追加できることを確認しました。

DELETE FROM members WHERE name IS NULL;
ALTER TABLE members ADD CONSTRAINT pk_members PRIMARY KEY (member_id);
ALTER TABLE members ADD CONSTRAINT uq_members_email UNIQUE (email);
-- 以下、NOT NULL・CHECK・FOREIGN KEY・DEFAULT も同様に追加

手順3:すぐに直せないときは「新しい行だけ先に検査する」

データの修正に時間がかかるときは、これから入るデータにだけ先に制約を効かせ、既存データはあとで確認する という進め方があります。

PostgreSQL:NOT VALID で追加し、あとから VALIDATE CONSTRAINT(CHECK と FOREIGN KEY が対象)。

ALTER TABLE members ADD CONSTRAINT ck_members_age CHECK (age >= 0) NOT VALID;
ALTER TABLE members ADD CONSTRAINT fk_members_dept
  FOREIGN KEY (dept_id) REFERENCES departments (dept_id) NOT VALID;

-- 新しい行はすぐに検査される
INSERT INTO members (member_id, email, name, age, status, dept_id)
VALUES (3, 'kato@example.com', '加藤', -1, 'active', 10);
ERROR:  new row for relation "members" violates check constraint "ck_members_age"
DETAIL:  Failing row contains (3, kato@example.com, 加藤, -1, active, 10).
SELECT conname, convalidated FROM pg_constraint WHERE conrelid = 'members'::regclass;
     conname     | convalidated 
-----------------+--------------
 ck_members_age  | f
 fk_members_dept | f

convalidated が f(未検証)になっています。既存データを直してから検証すると t に変わりました。違反行が残っている間は、検証もエラーになります。

ALTER TABLE members VALIDATE CONSTRAINT ck_members_age;
-- 違反行があるとき → ERROR:  check constraint "ck_members_age" of relation "members" is violated by some row
-- 直したあと     → ALTER TABLE

MySQL:CHECK を NOT ENFORCED(検査しない状態)で追加し、あとから ENFORCED に切り替える。

ALTER TABLE members ADD CONSTRAINT ck_members_age CHECK (age >= 0) NOT ENFORCED;
-- データを直したあとで有効にする
ALTER TABLE members ALTER CHECK ck_members_age ENFORCED;

MySQL 8.0.46 では、違反行が残ったまま ENFORCED にしようとすると ERROR 3819 (HY000): Check constraint 'ck_members_age' is violated. になり、違反行を直したあとは切り替えられました。NOT ENFORCED の間は新しい行も検査されない点が、PostgreSQL の NOT VALID との違いです。

DB 既存データの検査をあと回しにする書き方 新しい行の検査
PostgreSQL ADD CONSTRAINT ... NOT VALID → VALIDATE CONSTRAINT すぐに行われる(今回確認)
MySQL ADD CONSTRAINT ... CHECK (...) NOT ENFORCED → ALTER CHECK ... ENFORCED ENFORCED にするまで行われない
SQL Server ALTER TABLE ... WITH NOCHECK ADD CONSTRAINT ...(CHECK と FOREIGN KEY) 行われる(未実行。Microsoft Learn で WITH NOCHECK の説明を確認)
Oracle ADD CONSTRAINT ... ENABLE NOVALIDATE 行われる(未実行)

主キーと UNIQUE には、この「あと回し」は基本的に使えません(重複があると一意インデックスを作れないため)。先に重複を解消してから追加します。

外部キーの親を削除・更新したときの動き(ON DELETE)

外部キーは、親の行を消そうとしたときの動きも指定できます。指定しないときは、今回確認したとおり削除がエラーになります。

指定 親の行を削除したとき
指定なし / NO ACTION / RESTRICT 子が残っていればエラー
ON DELETE CASCADE 子の行も一緒に削除される
ON DELETE SET NULL 子の外部キー列が NULL になる

PostgreSQL 16.15 で ON DELETE SET NULL に付け替えて確かめました。1つの ALTER TABLE に DROP と ADD を並べると、制約の入れ替えを1文で行えます。

ALTER TABLE members
  DROP CONSTRAINT fk_members_dept,
  ADD CONSTRAINT fk_members_dept FOREIGN KEY (dept_id)
      REFERENCES departments (dept_id) ON DELETE SET NULL;

DELETE FROM departments WHERE dept_id = 10;
SELECT member_id, name, dept_id FROM members ORDER BY 1;
 member_id | name | dept_id 
-----------+------+---------
         1 | 佐藤 |        
         2 | 鈴木 |        

CASCADE は便利な一方、親を1行消すと関連する子がまとめて消えます。どのテーブルまで影響するかを確かめてから指定しましょう。

MySQLのsql_modeとNOT NULL

MySQL では、sql_mode に STRICT_TRANS_TABLES が含まれているかどうかで、NOT NULL 列を省略したときの動きが変わります。8.0.46 の既定値には含まれていました。試しに sql_mode を空にして実行した結果です。

SET SESSION sql_mode = '';
INSERT INTO members (member_id, email, age) VALUES (50, 'x@example.com', 20);
SHOW WARNINGS;
+---------+------+-------------------------------------------+
| Level   | Code | Message                                   |
+---------+------+-------------------------------------------+
| Warning | 1364 | Field 'name' doesn't have a default value |
+---------+------+-------------------------------------------+

エラーではなく警告になり、name には空文字が入りました。 NOT NULL を付けているのに空の値が入っているときは、SELECT @@sql_mode; で設定を確かめてみてください。

制約の設計チェックリスト

確認すること 目安
すべてのテーブルに主キーがあるか 行を特定できる列(または列の組み合わせ)を1つ決める
重複させたくない列に UNIQUE があるか メールアドレス、コード、番号など
必ず入力される列に NOT NULL があるか 迷ったら「NULL のとき何を意味するか」を説明できるかで判断する
値の範囲や組み合わせに CHECK があるか quantity > 0、start_date <= end_date、status IN ('active','inactive') など
別テーブルの ID を持つ列に外部キーがあるか 親を消したときの動き(ON DELETE)も決める
既定値が決まっている列に DEFAULT があるか 登録日時、状態の初期値など
制約に名前を付けているか pk_・uq_・ck_・fk_ の接頭辞+テーブル名+列名
CHECK と外部キーの列に NULL が入ってよいか 入れたくないなら NOT NULL も付ける

よくある質問

Q. 主キーと UNIQUE はどう使い分けますか? A. 行を特定するための ID には主キー(1テーブルに1つ、NULL 不可)、ID ではないけれど重複させたくない列には UNIQUE(いくつでも、NULL 可)を使います。UNIQUE の列で未入力も受け付けたくないときは NOT NULL を合わせて付けます。

Q. 制約を一時的に無効にできますか? A. DB によって方法があります。MySQL は CHECK を ALTER CHECK 制約名 NOT ENFORCED で検査しない状態にできます。PostgreSQL は NOT VALID で既存データの検査をあと回しにできます。大量データの取り込みなどで外す場合は、取り込み後に違反行がないことを確認してから元に戻す流れにしましょう。

Q. 制約名がわからないときはどうすればよいですか? A. MySQL は SHOW CREATE TABLE テーブル名;、PostgreSQL は psql の \d テーブル名 が一番手軽です。SQL で調べるなら、MySQL は information_schema.TABLE_CONSTRAINTS、PostgreSQL は pg_constraint を参照します。

Q. NOT NULL を追加しようとしたらエラーになりました。 A. その列にすでに NULL の行があります。MySQL では ERROR 1138: Invalid use of NULL value、PostgreSQL では column "…" contains null values と表示されました。SELECT * FROM テーブル WHERE 列 IS NULL; で対象を確認し、UPDATE で値を入れてから追加し直します。

まとめ

  • 制約は6種類:PRIMARY KEY・UNIQUE・NOT NULL・CHECK・FOREIGN KEY・DEFAULT。主キーは1テーブルに1つ、ほかは複数付けられる。
  • PRIMARY KEY・UNIQUE・CHECK・FOREIGN KEY の追加は ALTER TABLE ... ADD CONSTRAINT 名前 ... で4つの DB 共通。
  • NOT NULL と DEFAULT は DB で書き方が違う(MySQL・Oracle は MODIFY、PostgreSQL・SQL Server は ALTER COLUMN)。
  • 削除は DROP CONSTRAINT 制約名 が基本。MySQL は DROP PRIMARY KEY・DROP INDEX・DROP FOREIGN KEY・DROP CHECK も使う。
  • UNIQUE・CHECK・外部キーは NULL を通す。NULL も受け付けたくない列には NOT NULL を合わせて付ける。
  • 追加できないときは、違反行を SELECT で探して直してから追加する。PostgreSQL の NOT VALID、MySQL の NOT ENFORCED で段階的に進める方法もある。
  • 制約には名前を付ける。エラーメッセージが読みやすくなり、削除用の SQL も環境を問わず同じになる。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL制約一覧|主キー・UNIQUE・CHECK・NOT NULLの追加と削除」

株式会社RJCでは、SQLの制約やテーブル設計の考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?