役に立つ2つの制約
雑なスキーマ設計から生まれるバグの大半は、突き詰めると2つに集約されます。「想定外に NULL が入っているカラム」か、「アプリ側があるはずだと思っていた値が入っていないカラム」のどちらかです。SQLite の NOT NULL と DEFAULT は、この両方を一気に解決してくれます。しかも、追加のコストはほぼゼロです。
必須カラムが1つ、フォールバックを持つカラムが2つ。INSERT で渡したのは email だけで、残りは SQLite が自動で埋めてくれます。これがこの機能のすべてと言ってもよく、ここから先は細かい挙動の話になります。
NOT NULL は「NULL を絶対に許さない」制約
SQLite の NOT NULL 制約は文字通りの動きをします。デフォルト値のないカラムを INSERT で省略しても、明示的に NULL を書いても、いずれの場合も書き込みは失敗します。
エラーメッセージは次のように表示されます。
Runtime error: NOT NULL constraint failed: posts.title
NULL を直接渡しても結果は同じです。
INSERT INTO posts (id, title) VALUES (1, NULL);
-- 実行時エラー: NOT NULL制約違反: posts.title
これがルールです。論理的に必須となるカラムには NOT NULL を付けておけば、それだけで一群のバグを未然に防げます。アプリ側のコードがどう頑張っても、データベースに NULL を忍び込ませることはできません。
DEFAULT は呼び出し側が値を指定しなかったときに効く
DEFAULT が発動するのは、INSERT でそのカラムにまったく触れなかった場合だけです。明示的に NULL を渡したときには_救済してくれません_:
最初のINSERTはデフォルト値に任せ、2つ目は明示的に上書きしています。もし INSERT INTO tasks (title, status) VALUES ('x', NULL) と書いていたら、NOT NULL constraint failed エラーになります。カラム名を明示した時点でデフォルトは発動しないからです。
ここで押さえておきたい考え方はこうです。DEFAULT は 値が指定されていない カラムを埋めるためのもの。NOT NULL は どんな経路で来たNULLでも 拒否するもの。この2つは独立した機能で、組み合わせるとうまく噛み合います。
DEFAULTには式も書ける
リテラルを指定するのが一般的なケース(DEFAULT 0、DEFAULT ''、DEFAULT 'pending' など)ですが、SQLiteではカッコで囲んだ式もデフォルト値として使えます。これを使えば、レコード作成時刻を自動で記録したり、ランダムなIDを生成したりできます。
いくつか押さえておきたいポイントがあります。
- 式は 挿入のたびに評価されます。テーブル作成時に一度だけ評価されるわけではありません。各行ごとに、それぞれのタイムスタンプとトークンが割り当てられます。
- 括弧なしで書ける特殊なキーワードは
CURRENT_TIMESTAMP、CURRENT_DATE、CURRENT_TIMEの3つだけです。それ以外の式には括弧が必要です。 - 式の中で他のカラムやサブクエリを参照することはできません。完全に自己完結している必要があります。
カラムを任意にしつつ、値が入る場合は自動でスタンプしたいなら、NOT NULL を外してデフォルトだけ残します。必須に しつつ 自動スタンプも効かせたいなら、両方を指定します。
DEFAULT NULL も有効(あえて書く場面もある)
DEFAULT NULL と書くのは、デフォルトを指定しないのと実質同じです。値を渡さなければカラムは NULL になります。それでも書く意味があるのは、「初期状態として値なしを意図している」とスキーマ上で明示したいときです。
bio と avatar は、ここでは同じ挙動になります。bio の DEFAULT NULL は、いわばコード形式のコメントのようなもの。スキーマを読む人に対して「bio が空でも異常ではなく、想定内の状態ですよ」と伝える役割を果たします。
既存テーブルへの NOT NULL 追加
ここがちょっと厄介なところです。SQLite の ALTER TABLE はあえて機能が絞られていて、Postgres のように ALTER COLUMN ... SET NOT NULL を実行することはできません。何ができるかは、対象のカラムがすでに存在するかどうかで変わってきます。
新規追加するカラムであれば ADD COLUMN ... NOT NULL は使えますが、その場合はデフォルト値を必ず指定する必要があります。指定しないと、既存の行で NOT NULL カラムにいきなり NULL が入ることになり、それは原理的にあり得ないからです。
同じことを default なしで試すと、エラーになります。
ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- 実行時エラー: デフォルト値が NULL の NOT NULL カラムは追加できません
既存のカラムに対しては、その場で制約を変えることはできません。定番のやり方は、いわゆる「テーブル再構築の儀式」です。望む制約を持った新しいテーブルを作り、データをコピーして、古いテーブルを削除してから、新しいテーブルをリネームする — この流れですね。詳しい手順は drop-and-alter-table のページで扱いますが、今の段階では「この制限は実在するので、最初のスキーマ設計でちゃんと考えておこう」と覚えておいてください。
実戦でよくある組み合わせ
本番運用のテーブルでは、「アプリケーションが当然そうあるべきだと考えていること」をスキーマに落とし込むために、両方の制約を組み合わせて使うのが一般的です:
上から下までスキーマを眺めるだけで、コードを一行も見なくてもこのアプリが何をするか想像できます。customer は必須でフォールバックなし——呼び出し側は注文の相手が誰なのかを必ず知っていなければなりません。金額・通貨・ステータスにはどれも妥当なデフォルトが設定されているので、最小限の INSERT でも筋の通った行が出来上がります。notes は任意。created_at はデータベース側で埋められ、本来そこ_以外_で埋めるべきではない値です。
これがこうした制約の真価です。暗黙の前提を、データベース自身が守ってくれるルールに変えてくれます。
よくある落とし穴
ハマりやすいポイントを簡単にまとめておきます。
- 明示的な
NULLはDEFAULTを打ち消す。INSERT INTO t (col) VALUES (NULL)ではデフォルト値は使われません。デフォルトを効かせたいなら、その列を列リストから 外す 必要があります。 - 式のデフォルトには括弧が必要。
DEFAULT CURRENT_TIMESTAMPは特別なキーワード(全部で3つ)なのでそのまま動きますが、DEFAULT lower(hex(randomblob(8)))はNG。DEFAULT (lower(hex(randomblob(8))))のように括弧で囲んでください。 NOT NULLと空文字列は別物。''はTEXTとして正当な値なので、NOT NULL制約には引っかかりません。空文字列も弾きたい場合はCHECKの出番です(次のページで扱います)。ADD COLUMN ... NOT NULLにはNULL以外のDEFAULTが必須。 デフォルトを指定しないと SQLite はそのカラム追加を拒否します。
次は CHECK 制約
NOT NULL と DEFAULT は、「必ず値があること」と「無ければ埋める」をカバーしてくれます。さらに踏み込んだバリデーション——「正の数であること」「決められた値のいずれかであること」「終了日が開始日より後であること」——を表現したいときは、SQLite の CHECK 制約の出番です。各行が満たすべき任意のブール式を書けるので、次のページで詳しく見ていきましょう。
よくある質問
SQLiteでカラムを必須にするには?
カラム定義に NOT NULL を付けるだけです。たとえば email TEXT NOT NULL のように書きます。INSERT や UPDATE でこのカラムを NULL にしようとすると NOT NULL constraint failed で弾かれます。値が指定されなかったときのフォールバックを用意したい場合は、DEFAULT と組み合わせるのが定石です。
SQLiteのDEFAULT(デフォルト値)はどう動く?
DEFAULT <値> は、INSERT でそのカラムが指定されなかったときに使われる値を決めます。リテラル(DEFAULT 0 や DEFAULT 'pending')、NULL、あるいはカッコで囲んだ式(DEFAULT (CURRENT_TIMESTAMP) や DEFAULT (lower(hex(randomblob(8)))) など)が使えます。式の場合は挿入のたびに評価されるのがポイントです。
INSERT時に「NOT NULL constraint failed」が出るのはなぜ?
NOT NULL 指定があるのに DEFAULT もないカラムを、値なしで INSERT しようとしているのが原因です。対処は3択で、①そのカラムを INSERT 文に含める、②DEFAULT を付ける、③制約自体を外す、のいずれかです。明示的に NULL を渡しても同じエラーになります。NOT NULL は出どころに関係なくNULLを拒否します。
既存カラムに後からNOT NULLを付けられる?
直接は付けられません。SQLiteには ALTER TABLE ... ALTER COLUMN がないからです。回避策は2つで、NOT NULL DEFAULT <値> 付きの新しいカラムを追加するか(既存行のためにDEFAULTは必須)、テーブルごと作り直す方法です。後者は、新テーブルを制約付きで作成 → データをコピー → 旧テーブルを DROP → 新テーブルを RENAME、という流れになります。