メールアドレスが空の会員や、年齢がマイナスの会員がテーブルに入ってしまうと、あとから探して直すのは手間がかかります。制約(constraint)をテーブルに付けておけば、そうした行は INSERT の時点ではじかれます。NOT NULL・UNIQUE・CHECK の3つについて、書き方と、違反したときに出るエラーを実例で並べました。CHECK 制約が実際に効くのは MySQL 8.0.16 からです。また、NULL は CHECK を素通りするので、必須の列は NOT NULL と組み合わせて使います。
以下の結果は MySQL 8.0 で実際に実行したものです。
会員テーブルで考える
例として、サービスの会員テーブルを使います。メールアドレスと名前は必須で、メールアドレスは重複させません。年齢は任意ですが、入れるなら0〜150の範囲にします。プランは free・standard・premium の3種類だけです。これをそのまま制約にすると、次の定義になります。
CREATE TABLE members (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(50) NOT NULL,
age INT,
plan VARCHAR(10) NOT NULL DEFAULT 'free',
CONSTRAINT uq_members_email UNIQUE (email),
CONSTRAINT chk_members_age CHECK (age BETWEEN 0 AND 150),
CONSTRAINT chk_members_plan CHECK (plan IN ('free', 'standard', 'premium'))
);
INSERT INTO members (email, name, age, plan) VALUES
('sato@example.com', '佐藤', 34, 'standard'),
('suzuki@example.com', '鈴木', NULL, 'free');
SELECT * FROM members;
+----+--------------------+--------+------+----------+
| id | email | name | age | plan |
+----+--------------------+--------+------+----------+
| 1 | sato@example.com | 佐藤 | 34 | standard |
| 2 | suzuki@example.com | 鈴木 | NULL | free |
+----+--------------------+--------+------+----------+
3つの制約の違いを先に表にしておきます。NULL の扱いがそれぞれ違うところに注目してください。
| 制約 | 防ぐもの | NULL を入れると | 違反したときのエラー |
|---|---|---|---|
| NOT NULL | 値が空の行 | エラー | 1048 / 1364 |
| UNIQUE | 同じ値の重複 | 何件でも入る | 1062 |
| CHECK | 条件に合わない値 | 通る | 3819 |
NOT NULL で値を必ず入れさせる
NOT NULL を付けた列には、NULL を入れられません。メールアドレスに NULL を指定すると、次のエラーになります。
INSERT INTO members (email, name, age) VALUES (NULL, '田中', 28);
ERROR 1048 (23000): Column 'email' cannot be null
列そのものを書き忘れた場合も止まります。こちらはエラーの番号と文が変わり、既定値(DEFAULT)がないと言われます。
INSERT INTO members (name, age) VALUES ('田中', 28);
ERROR 1364 (HY000): Field 'email' doesn't have a default value
NOT NULL と DEFAULT を組み合わせると、省略したときに決まった値が入ります。plan 列は DEFAULT ‘free’ にしてあるので、指定しなければ free になります。
INSERT INTO members (email, name, age) VALUES ('tanaka@example.com', '田中', 28);
SELECT id, email, name, age, plan FROM members;
+----+--------------------+--------+------+----------+
| id | email | name | age | plan |
+----+--------------------+--------+------+----------+
| 1 | sato@example.com | 佐藤 | 34 | standard |
| 2 | suzuki@example.com | 鈴木 | NULL | free |
| 3 | tanaka@example.com | 田中 | 28 | free |
+----+--------------------+--------+------+----------+
UNIQUE で同じ値を2回入れさせない
UNIQUE を付けた列には、すでにある値と同じ値を入れられません。佐藤さんと同じメールアドレスで登録しようとすると、こうなります。
INSERT INTO members (email, name, age) VALUES ('sato@example.com', '佐藤二郎', 40);
ERROR 1062 (23000): Duplicate entry 'sato@example.com' for key 'members.uq_members_email'
エラーに制約の名前 uq_members_email が出ています。CONSTRAINT で名前を付けておくと、どの決まりに引っかかったのかがエラーを見ただけでわかります。
複数の列の組み合わせで重複を禁じることもできます。お気に入りのテーブルで「同じ会員が同じ商品を2回登録しない」ようにする例です。
CREATE TABLE favorites (
member_id INT NOT NULL,
item_id INT NOT NULL,
CONSTRAINT uq_favorites UNIQUE (member_id, item_id)
);
INSERT INTO favorites VALUES (1, 100), (1, 200), (2, 100);
INSERT INTO favorites VALUES (1, 100);
ERROR 1062 (23000): Duplicate entry '1-100' for key 'favorites.uq_favorites'
(1, 200) や (2, 100) は、片方の値が同じでも組み合わせが違うので入ります。止まるのは (1, 100) がもう一度来たときだけです。
UNIQUE の列に NULL は何件でも入ります。NULL は「値がわからない」という意味なので、NULL 同士は同じ値として扱われません。
CREATE TABLE coupons (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) UNIQUE
);
INSERT INTO coupons (code) VALUES ('WELCOME'), (NULL), (NULL);
SELECT * FROM coupons;
+----+---------+
| id | code |
+----+---------+
| 2 | NULL |
| 3 | NULL |
| 1 | WELCOME |
+----+---------+
NULL の行が2件とも入りました。並び順が id 順でないのは、ORDER BY を書いていないためです。決まった順で見たいときは ORDER BY を付けます。重複を1件も許さない列は、UNIQUE と NOT NULL を両方付けておきます。
CHECK で値の範囲や種類を決める
CHECK は、列の値が条件式を満たすかどうかを INSERT・UPDATE のたびに確かめます。年齢にマイナスを入れたり、決めていないプランを指定したりすると止まります。
INSERT INTO members (email, name, age) VALUES ('ito@example.com', '伊藤', -5);
INSERT INTO members (email, name, age, plan) VALUES ('kato@example.com', '加藤', 22, 'gold');
ERROR 3819 (HY000): Check constraint 'chk_members_age' is violated.
ERROR 3819 (HY000): Check constraint 'chk_members_plan' is violated.
表の制約として書けば、複数の列を比べる条件も作れます。キャンペーンの開始日が終了日より後にならないようにする例です。
CREATE TABLE campaigns (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT chk_campaigns_period CHECK (start_date <= end_date)
);
INSERT INTO campaigns (name, start_date, end_date) VALUES ('冬のセール', '2026-12-20', '2026-12-01');
ERROR 3819 (HY000): Check constraint 'chk_campaigns_period' is violated.
付けた CHECK 制約の一覧は、information_schema.CHECK_CONSTRAINTS で確かめられます。
SELECT CONSTRAINT_NAME, CHECK_CLAUSE
FROM information_schema.CHECK_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = 'demo'
ORDER BY CONSTRAINT_NAME;
+----------------------+-------------------------------------------------------------------------+
| CONSTRAINT_NAME | CHECK_CLAUSE |
+----------------------+-------------------------------------------------------------------------+
| chk_campaigns_period | (`start_date` <= `end_date`) |
| chk_members_age | (`age` between 0 and 150) |
| chk_members_plan | (`plan` in (_utf8mb4\'free\',_utf8mb4\'standard\',_utf8mb4\'premium\')) |
+----------------------+-------------------------------------------------------------------------+
名前を付けずに CHECK を書くと、MySQL が「テーブル名_chk_1」のような名前を自動で付けます。あとで消したり直したりするときに名前が要るので、自分で付けておくほうが扱いやすくなります。
よくある落とし穴
MySQL 8.0.15 以前では CHECK が無視される
MySQL 8.0.16 より前は、CHECK を書いても構文として読むだけで、中身は確かめていませんでした。エラーも出ないので、効いているつもりで不正な値が入り続けます。古いバージョンを使っている場合は、SELECT VERSION(); で確かめてください。
NULL は CHECK を通る
CHECK は条件式が FALSE になったときだけ止めます。NULL を比べた結果は TRUE でも FALSE でもない UNKNOWN なので、通ってしまいます。最初の INSERT で鈴木さんの age が NULL のまま入ったのはこのためです。値を必須にしたい列には NOT NULL も付けます。
CURDATE() や NOW() は CHECK に書けない
「開催日は今日以降」のように、実行するたびに結果が変わる関数は使えません。テーブルを作る時点でエラーになります。
CREATE TABLE events (
id INT PRIMARY KEY,
held_on DATE,
CHECK (held_on >= CURDATE())
);
ERROR 3814 (HY000): An expression of a check constraint 'events_chk_1' contains disallowed function: curdate.
ほかに、サブクエリ・変数・ストアドファンクション・AUTO_INCREMENT の列も CHECK の中では使えません。日付のような条件は、アプリ側で確かめます。
あとから足す制約は、今のデータでも確かめられる
ALTER TABLE で CHECK を足すと、すでにある行もすべて確かめられます。1行でも守れていなければ、制約は追加されません。名前を3文字以上にする制約を足そうとすると、佐藤さんたちの2文字の名前で止まります。
ALTER TABLE members ADD CONSTRAINT chk_members_name CHECK (CHAR_LENGTH(name) >= 3);
ERROR 3819 (HY000): Check constraint 'chk_members_name' is violated.
本番のテーブルに足す前に、WHERE NOT (条件) で引っかかる行を SELECT して、先にデータを直しておきます。
INSERT IGNORE だと違反した行が黙って消える
INSERT IGNORE を使うと、CHECK の違反はエラーではなく警告になり、その行は入りません。
INSERT IGNORE INTO members (email, name, age) VALUES ('ito@example.com', '伊藤', -5);
SHOW WARNINGS;
+---------+------+-------------------------------------------------+
| Level | Code | Message |
+---------+------+-------------------------------------------------+
| Warning | 3819 | Check constraint 'chk_members_age' is violated. |
+---------+------+-------------------------------------------------+
アプリから流すと警告に気づきにくく、データが足りない原因になります。CSV の取り込みなどで IGNORE を使うときは、SHOW WARNINGS で飛ばされた行を必ず確かめます。
PostgreSQL も3つの制約を同じ書き方で使えます。PostgreSQL 15 からは UNIQUE NULLS NOT DISTINCT と書くと、NULL の重複も1件までにできます。
まとめ
必須の列には NOT NULL、重複させない列には UNIQUE、値の範囲や種類を決めたい列には CHECK を付けます。UNIQUE と CHECK は NULL を通すので、必須なら NOT NULL と組み合わせてください。CHECK が効くのは MySQL 8.0.16 からです。制約には CONSTRAINT で名前を付けておくと、エラーの原因がすぐわかります。主キーと外部キーの制約は主キー・外部キーの設計の記事で扱っています。
参考リンク
- MySQL 8.0 リファレンス: CHECK Constraints
- MySQL 8.0 リファレンス: CREATE TABLE Statement
- MySQL 8.0 リファレンス: The INFORMATION_SCHEMA CHECK_CONSTRAINTS Table
アイキャッチ画像: Photo by Annie Spratt on Unsplash

