MySQL の CHECK 制約

CHECK 制約は、カラムに格納される値が特定の条件を満たすことをデータベースレベルで保証する仕組みです。MySQL 8.0.16 以降で正式にサポートされ、それ以前のバージョンでは構文は受け付けるものの実際には無視されていました。

基本的な使い方

CHECK 制約を使うと、INSERT や UPDATE の際に条件を満たさない値を拒否できます。

CREATE TABLE products (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
  stock INT NOT NULL CHECK (stock >= 0),
  discount_rate DECIMAL(3,2) CHECK (discount_rate BETWEEN 0 AND 1)
);

-- 負の価格はエラーになる
INSERT INTO products (name, price, stock) VALUES ('Widget', -100, 10);
-- ERROR: Check constraint 'products_chk_1' is violated.

よく使う CHECK 制約のパターン

数値の範囲制限

価格や在庫数が負にならないように CHECK (column >= 0) を設定するのが最も一般的な使い方です。年齢なら CHECK (age BETWEEN 0 AND 150) のように上限も指定できます。

文字列の長さ制限

VARCHAR の最大長とは別に、最小長を強制したい場合に使います。CHECK (CHAR_LENGTH(code) = 6) で固定長を強制することも可能です。

ENUM 的な値制限

CHECK (status IN (‘active’, ‘inactive’, ‘pending’)) のように、許容される値のリストを定義できます。ENUM 型の代替として使えます。

名前付き CHECK 制約

制約に名前を付けると、エラーメッセージが分かりやすくなり、後から制約を削除・変更する際にも便利です。

CREATE TABLE employees (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  salary DECIMAL(12,2) NOT NULL,
  hire_date DATE NOT NULL,
  CONSTRAINT chk_salary_positive CHECK (salary > 0),
  CONSTRAINT chk_hire_date_range CHECK (hire_date >= '2000-01-01')
);

-- 制約の削除
ALTER TABLE employees DROP CHECK chk_salary_positive;

名前を付けない場合は MySQL が自動的に テーブル名_chk_N という名前を生成しますが、管理しにくくなるため、意味のある名前を明示的に付ける習慣をつけましょう。

CHECK 制約の制限事項

CHECK 制約にはいくつかの制限があります。

できること

同じ行内のカラムを参照した条件式、リテラル値との比較、組み込み関数の一部(CHAR_LENGTH など)の使用。

できないこと

サブクエリの使用、他のテーブルの参照、ストアドファンクションの呼び出し、変数の参照。テーブルをまたいだ制約はトリガーで実装する必要があります。

ENUM 型との使い分け

値のリストを制限する方法として、CHECK 制約のほかに ENUM 型もあります。それぞれ特徴が異なるため、用途に応じて使い分けるのがよいでしょう。

ENUM 型はテーブル定義に値のリストが埋め込まれるため、値を追加するには ALTER TABLE が必要になります。一方 CHECK 制約は、制約の削除・追加で柔軟に変更できます。ただし、ENUM は内部的に整数で格納されるためストレージ効率がよく、大量のデータを扱う場合に有利な面もあります。

CHECK 制約はアプリケーションのバリデーションを補完するものであり、置き換えるものではありません。アプリ側でもバリデーションを行いつつ、最終防衛線としてデータベースに CHECK 制約を設定しておくのが堅牢な設計です。