Menu

SQLite CHECK制約の使い方 | テーブル単位でデータ検証

SQLiteのCHECK制約で、列の値に対するルールをDB側で強制する方法を解説。単一列・複数列のチェック、名前付き制約、NULL周りのハマりどころまで実例で紹介します。

このページのコードはエディタで実行できます — 編集してすぐに結果を確認できます。

CHECK制約とは「すべての行が満たすべきルール」のこと

SQLiteのCHECK制約は、テーブルに紐づけるブール式のことです。INSERTUPDATEが走るたびにSQLiteがこの式を評価し、結果が偽になれば処理は失敗します。「価格はマイナスにできない」「ステータスは決められた3つの値のどれかでなければならない」といった業務ルールを、スキーマそのものに埋め込めるのが特徴です。

最初の2行は問題なく入ります。3行目は CHECK constraint failed が出て弾かれ、テーブルには入りません。アプリ経由でも、マイグレーションスクリプトでも、CLIから手で叩く場合でも、この制約は書き込み元を問わず必ずチェックしてくれます。

列レベル vs テーブルレベル

CHECK制約の書き方は2通りあります。列定義の後に書く「列レベル」と、すべての列の後にまとめて書く「テーブルレベル」です。動作はどちらも同じで、違うのはどちらが自然に読めるかという点だけです。

最初の予約は問題なく挿入されますが、2つ目は終了時刻が開始時刻より前なので失敗します。1カラムだけのルールはカラムレベルに、2カラム以上を比較するルールはテーブルレベルに書くと読みやすくなります。

値を特定のリストに制限する

よくある使い方が、カラムの値をあらかじめ決めた選択肢に絞り込むパターンです。SQLiteにはenum型がないので、CHECK ... IN (...)を使うのが定番です。

3行目は失敗します。'pending' が許可リストに入っていないためです。あとからステータスを追加したくなった場合はテーブルを作り直す必要があるので(詳しくは後述)、リストを固定する前に少し考えておきましょう。とはいえ、ロール名や注文ステータスのように本当に値が固定されている語彙であれば、まさにこの制約がぴったりです。

SQLite 名前付き制約で分かりやすくする

デフォルトでは制約に名前は付きません。エラーメッセージには式と一緒に「CHECK constraint failed」とだけ表示されるので、CHECK が1つしかないテーブルなら問題ありませんが、5個もあると何が引っかかったのか分かりにくくなります。そんなときは CONSTRAINT で名前を付けておきましょう。

これでエラーメッセージに制約名が含まれるようになり、どのルールに引っかかったのかが一目で分かります。名前を付けるのに数文字余分にかかるだけですが、本番環境で何かが壊れたとき、その投資はすぐに回収できます。

CHECK制約とNULLの落とし穴

CHECK制約は、式が真の場合 または NULLの場合にパスします。明示的に偽になったときだけ失敗するんです。一見奇妙に思えますが、NULLとの比較はほぼすべてNULLを返し、真でも偽でもないことを思い出せば納得できます。

NULLの行はすんなり通ります。NULL >= 0 の評価結果は falseではなくNULLになるので、CHECK制約には引っかからないんですね。負の値もNULLもどちらも弾きたいなら、NOT NULL とCHECK制約を組み合わせて使いましょう。

これで NOT NULL 制約のほうが先に引っかかって、CHECK にたどり着く前に INSERT が失敗します。2つの制約は役割分担しているわけです。値が入っていないかどうかは NOT NULL が、値の中身が想定どおりかは CHECK が見てくれます。

CHECK制約の中で使える組み込み関数

CHECK制約の式には、SQLiteの組み込み関数のほとんどがそのまま使えます。実際によく登場するものをいくつか挙げておきます。

失敗する例は3つあります。メールの形式が不正なケース、ユーザー名が短すぎるケース、そして国コードが小文字になっているケースです。シンプルなパターンマッチには LIKE が使えますし、length()upper()lower()、四則演算なども自由に組み合わせられます。ただし、式は必ず決定的(deterministic)であることを意識してください。random()current_timestamp のようなものを CHECK の中で使うと、行ごとに結果が変わる「ブレるルール」ができてしまい、まず期待した動作になりません。

SQLite CHECK制約とトリガーの使い分け

CHECK制約とトリガーはどちらも不正なデータを弾けるので、最初のうちは「どっちを使えばいいの?」と迷いがちです。判断の目安はこうです。

  • CHECK制約:ルールが「いま書き込もうとしている行」だけで完結する場合。たとえば「この列とあの列を比べる」「値が一定の範囲内に収まっている」「文字列が特定のパターンに合致する」など。
  • トリガー(具体的には BEFORE INSERT/UPDATERAISE を呼び出すもの):ルールが 他の行他のテーブル に依存する場合、あるいは1つの真偽式では書ききれない複雑な処理が必要な場合。

CHECK制約のほうが高速でシンプルですし、何よりスキーマに直接書かれるので可視性が高い、という利点があります。CREATE TABLE を読んだ人がそのままルールを把握できるわけです。CHECK制約で表現しきれないときに初めてトリガーを検討する、くらいの感覚でちょうどいいでしょう。

ALTER TABLE では CHECK制約を削除できない

ここがちょっとした落とし穴です。SQLite には ALTER TABLE ... DROP CONSTRAINT が存在しません。CHECK制約を削除したり変更したりしたい場合は、テーブルを作り直す必要があります。

BEGIN;

CREATE TABLE products_new (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);

INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;

COMMIT;

全体をトランザクションで囲んでおけば、途中でコケてもデータベースはそのままの状態で残せます。もしリビルド対象のテーブルに対して外部キーで参照している別テーブルがある場合、手順はもう少し増えます。foreign_keys を一旦オフにして、リビルドして、戻して、最後に整合性を再チェック、という流れですね。これについてはカリキュラム後半のマイグレーションの章で詳しく取り上げます。

次のテーマ: UNIQUE制約

CHECK制約は、1行の中の値の「形」を検証するものでした。次に登場する UNIQUE 制約は、行同士の関係、つまり「複数の行をまたいだ」検証を担当します。特定の列、あるいは複数列の組み合わせで、同じ値を持つ行が2つ以上存在しないことを保証してくれる仕組みです。次回はこれを見ていきましょう。

よくある質問

SQLiteのCHECK制約とは?

CHECK制約は、テーブルに紐づけたブール式で、すべての行が満たさなければならない条件を表します。SQLiteはINSERTUPDATEのたびにこの式を評価して、結果が偽ならその変更を弾きます。「価格は正の数でなければならない」といったルールを、アプリ側にロジックを書かずにDB側だけで強制できる、もっともシンプルな仕組みです。

CHECK制約で複数の列を同時にチェックできますか?

できます。1つの列に紐づける書き方ではなく、テーブルレベルの制約として書くのがコツです。たとえば列の定義を全部書いた後ろに CHECK (start_date <= end_date) と置けば、両方の列を参照できます。列レベルのCHECKでも他の列を参照すること自体は可能ですが、複数列が絡むときはテーブルレベルで書いた方が圧倒的に読みやすくなります。

NULLが入るとCHECK制約が効かないのはなぜ?

CHECKは式の結果が 真またはNULL のときに通り、明確に偽 のときだけ失敗する仕様だからです。たとえば CHECK (age >= 0) の場合、ageがNULLだと NULL >= 0 の結果はNULL(偽ではない)なので、そのまま通ってしまいます。NULL自体も禁止したいなら、CHECKと一緒に NOT NULL 制約も付けてあげましょう。

あとからCHECK制約を削除・変更できる?

残念ながら直接はできません。SQLiteには ALTER TABLE ... DROP CONSTRAINT がないからです。変更したい場合は、PRAGMA writable_schema を有効にしてsqlite_schemaを直接書き換える(上級者向け・リスクあり)か、テーブルを作り直すことになります。具体的には、新しい制約を持つテーブルを別名で作成 → データをコピー → 旧テーブルをDROP → リネーム、という流れです。制約に名前を付けておくと、この再構築スクリプトがぐっと読みやすくなります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める