NOT NULL・UNIQUE・CHECK制約で不正データを防ぐ

木の柵が続く草原 SQL

メールアドレスが空の会員や、年齢がマイナスの会員がテーブルに入ってしまうと、あとから探して直すのは手間がかかります。制約(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 で名前を付けておくと、エラーの原因がすぐわかります。主キーと外部キーの制約は主キー・外部キーの設計の記事で扱っています。

もっと学ぶなら【PR】

※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

参考リンク

アイキャッチ画像: Photo by Annie Spratt on Unsplash

タイトルとURLをコピーしました