こんにちは、小林です。
今回は、MySQLのNOT NULL制約とDEFAULT制約について話をしたいと思います。
私はNOT NULL制約に関して、勘違いをして覚えてしまっており、レビュー時にそれを元に指摘をしてしまうことがあります。
それがついに3回目になってしまったので、ブログとしてまとめ、自分の中に定着させようと思い、筆を取りました。
勘違いしていたこと
「MySQLにおいて、NULLが格納されている既存のカラムに新しくNOT NULL制約を付与してもNOT NULL制約違反にはならない」という勘違いをしていました。
思考としては、以下のような感じです。
- 更新用のDML(INSERT、UPDATE、DELETE)を実行したときに制約は適用される
- 制約の追加(ALTER)はDDLのため、制約は適用されない
※但し、制約の追加後の最初の登録、更新時に該当カラムにNULL以外の値を入れないといけない
勘違いをしてしまっていた理由は、今回二つあることがわかりました。
制約が適用されるタイミングを誤認していた
まず、制約の適用されるタイミングを誤認していました。
過去のプロジェクトで、DMLの実行時にトリガーを発火させるということをしていたことがあり、「トリガーはDMLの実行時に発火する」と覚えてしまっていました。
※トリガーはDDL実行時にも発火させることが可能です。
そこから、いつの間にかトリガーと制約を取り違えてしまい、「制約がチェックされるのはDMLの実行時のみ」と思い込んでいました。
これにより、DDLの実行時には影響がないと勘違いをしていました。
MySQLとPostgreSQLにてNOT NULL制約の扱いが異なっていた
こちらの方が本題で詳細は後述しますが、以下によって勘違いをしていました。
- PostgreSQLでは、DEFAULT制約なしのNOT NULL制約を付与するとエラーとなる
- MySQLでは、DEFAULT制約なしのNOT NULL制約を付与してもエラーとならない
※条件によります - MySQLではエラーにならないのだから、制約は動いていない
実際のところ、どうなのか?
検証
検証に使用したSQLはこちらのリポジトリに用意してあります。
検証手順は、以下の通りです。
- テーブルを作成する
- テーブルにレコードを登録する
- 新しいカラムを追加する
- カラムの追加と同時にNOT NULL制約を付与する・・・パターン1
- カラムを追加後にNOT NULL制約を付与する・・・パターン2
MySQL
パターン1
使用したSQLは、こちら。
mysql> SELECT * FROM not_null_when_add_columns1; +----+-----------+ | id | name | +----+-----------+ | 1 | nobunaga | | 2 | hideyoshi | +----+-----------+ 2 rows in set (0.00 sec) mysql> mysql> ALTER TABLE not_null_when_add_columns1 ADD COLUMN added_column VARCHAR(255) NOT NULL; Query OK, 0 rows affected (0.05 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM not_null_when_add_columns1; +----+-----------+--------------+ | id | name | added_column | +----+-----------+--------------+ | 1 | nobunaga | | | 2 | hideyoshi | | +----+-----------+--------------+ 2 rows in set (0.00 sec) mysql>
カラムの追加に成功しました。※NUllが格納されている場合は、NULLと表示されます。
パターン2
使用したSQLは、こちら。
mysql> SELECT * FROM not_null_when_add_columns2; +----+-----------+ | id | name | +----+-----------+ | 1 | nobunaga | | 2 | hideyoshi | +----+-----------+ 2 rows in set (0.00 sec) mysql> ALTER TABLE not_null_when_add_columns2 ADD COLUMN added_column VARCHAR(255); Query OK, 0 rows affected (0.05 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM not_null_when_add_columns2; +----+-----------+--------------+ | id | name | added_column | +----+-----------+--------------+ | 1 | nobunaga | NULL | | 2 | hideyoshi | NULL | +----+-----------+--------------+ 2 rows in set (0.00 sec) mysql> ALTER TABLE not_null_when_add_columns2 MODIFY added_column VARCHAR(255) NOT NULL; ERROR 1138 (22004): Invalid use of NULL value mysql>
カラムを追加した際に、カラムにNULLが設定されています。とエラーが出力されました。
PostgreSQL
パターン1
使用したSQLは、こちら。
(結果一部抜粋) play_with_sql=# SELECT * FROM not_null_when_add_columns1; id | name ----+----------- 1 | nobunaga 2 | hideyoshi (2 rows) play_with_sql=# play_with_sql=# ALTER TABLE not_null_when_add_columns1 ADD COLUMN added_column VARCHAR(32) NOT NULL; ERROR: column "added_column" of relation "not_null_when_add_columns1" contains null values play_with_sql=#
カラムを追加した際に、カラムにNULLが設定されています。とエラーが出力されました。
パターン2
使用したSQLは、こちら。
(結果一部抜粋) play_with_sql=# SELECT * FROM not_null_when_add_columns2; id | name ----+----------- 1 | nobunaga 2 | hideyoshi (2 rows) play_with_sql=# play_with_sql=# ALTER TABLE not_null_when_add_columns2 ADD COLUMN added_column VARCHAR(32); ALTER TABLE play_with_sql=# SELECT * FROM not_null_when_add_columns2; id | name | added_column ----+-----------+-------------- 1 | nobunaga | (null) 2 | hideyoshi | (null) (2 rows) play_with_sql=# play_with_sql=# ALTER TABLE not_null_when_add_columns2 ALTER COLUMN added_column SET NOT NULL; ERROR: column "added_column" of relation "not_null_when_add_columns2" contains null values play_with_sql=#
カラム追加時にはエラーになりませんでしたが、NOT NULL制約を付与した際にエラーが出力されました。
結論
MySQL
- NOT NULL制約のあるカラムを追加する際に、DEFAULT制約が設定されていない場合、(SQLモードによるが)その型のDEFAULT値が設定されるため、NOT NULL制約を回避する
- しかし、カラムを追加後にNOT NULL制約を追加する場合には先にNULLが設定され、そのカラムに対してNOT NULL制約を設定されることとなり、エラーとなる
明示的な DEFAULT 句のない NOT NULL カラムに対するデータエントリでは、INSERT または REPLACE ステートメントにカラムの値を含まれていない場合、または UPDATE ステートメントがカラムを NULL に設定する場合、MySQL はその時点で有効な SQL モードに従ってカラムを処理します。
参考:https://dev.mysql.com/doc/refman/8.0/ja/data-type-defaults.html
PostgreSQL
- DEFAULT制約が指定されていないとNULLが設定されるため、エラーとなる
新しい列にはデフォルト値が初期値として入ります(DEFAULT句を指定しない場合はNULL値が入ります)。
参考:https://www.postgresql.jp/document/14/html/ddl-alter.html#DDL-ALTER-ADDING-A-COLUMN
さいごに
毎回、調査して間違っていることに気づくのですが、しばらく経つと忘れてしまい、また指摘しまうという悪循環に陥っていました。
しかし、それもこれで終わりです。おそらく。。。きっと。。。多分。。。