I am looking for how to implement unique constraints with NULL check.
MySQL shouldn't allow multiple null value.
Employee:
id | name
---|-----
1 | null
2 | null -> should give error during inserting 2nd row.
No, MySQL is doing the right thing, according to the SQL-99 specification.
https://mariadb.com/kb/en/sql-99/constraint_type-unique-constraint/
A UNIQUE Constraint makes it impossible to COMMIT any operation that would cause the unique key to contain any non-null duplicate values. (Multiple null values are allowed, since the null value is never equal to anything, even another null value.)
If you use a UNIQUE constraint but don't want multiple rows with NULL, declare the columns as NOT NULL
and prohibit any row from having NULL.
MySQL 5.7 does allow for a workaround:
mysql> CREATE TABLE `null_test` (
-> `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
-> `const` varchar(255) NOT NULL DEFAULT '',
-> `deleted_at` datetime DEFAULT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.03 sec)
With soft deletes, it would be nice if you could have just one row with a with a deleted_at = NULL
per constraint.
mysql> ALTER TABLE `null_test` ADD `vconst` int(1) GENERATED ALWAYS AS (((NULL = `deleted_at`) or (NULL <=> `deleted_at`))) VIRTUAL;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
So I created a virtual column that will flip from 1
to null
when deleted_at
gets set.
mysql> ALTER TABLE `null_test` ADD UNIQUE KEY `nullable_index` (`const`,`vconst`);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
Instead of including deleted_at
to the unique constraint add the virtual column, vconst
.
mysql> INSERT INTO `null_test` SET `const` = 'Ghost';
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM `null_test` WHERE `const` = 'Ghost';
+--------+-------+------------+--------+
| id | const | deleted_at | vconst |
+--------+-------+------------+--------+
| 999901 | Ghost | NULL | 1 |
+--------+-------+------------+--------+
1 row in set (0.01 sec)
No need to insert the vconst
(but you cannot, anyhow).
mysql> INSERT INTO `null_test` SET `const` = 'Ghost';
ERROR 1062 (23000): Duplicate entry 'Ghost-1' for key 'nullable_index'
Inserting it again throws the Duplicate entry error.
mysql> UPDATE `null_test` SET `deleted_at` = NOW() WHERE `const` = 'Ghost';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Same with setting delete_at
, no need to touch vconst
, it will flip automatically.
mysql> SELECT * FROM `null_test` WHERE `const` = 'Ghost';
+--------+-------+---------------------+--------+
| id | const | deleted_at | vconst |
+--------+-------+---------------------+--------+
| 999901 | Ghost | 2017-02-16 22:07:45 | NULL |
+--------+-------+---------------------+--------+
1 row in set (0.00 sec)
mysql> INSERT INTO `null_test` SET `const` = 'Ghost';
Query OK, 1 row affected (0.00 sec)
Now you are free to insert a new row with the same constraints!
mysql> SELECT * FROM `null_test` WHERE `const` = 'Ghost';
+--------+-------+---------------------+--------+
| id | const | deleted_at | vconst |
+--------+-------+---------------------+--------+
| 999901 | Ghost | 2017-02-16 22:07:45 | NULL |
| 999903 | Ghost | NULL | 1 |
+--------+-------+---------------------+--------+
2 rows in set (0.01 sec)
In this case, depending on how much you soft delete, setting deleted_at
, you might want to include deleted_at
to the index, or a new index with it, but I will let my load tests decide.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With