唯一(多列)并在一列中为null

Mar*_*łek 18 mysql sql database unique-constraint

我有简单的类别表.类别可以具有父类别(par_cat列),如果它是主类别,则为null,并且具有相同的父类别,不应该有2个或更多具有相同名称或URL的类别.

该表的代码:

CREATE TABLE IF NOT EXISTS `categories` (
`id` int(10) unsigned NOT NULL,
  `par_cat` int(10) unsigned DEFAULT NULL,
  `lang` varchar(2) COLLATE utf8_unicode_ci NOT NULL DEFAULT 'pl',
  `name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
  `url` varchar(120) COLLATE utf8_unicode_ci NOT NULL,
  `active` tinyint(3) unsigned NOT NULL DEFAULT '1',
  `accepted` tinyint(3) unsigned NOT NULL DEFAULT '1',
  `priority` int(10) unsigned NOT NULL DEFAULT '1000',
  `entries` int(10) unsigned NOT NULL DEFAULT '0',
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=3 ;



ALTER TABLE `categories`
  ADD PRIMARY KEY (`id`), 
  ADD UNIQUE KEY `categories_name_par_cat_unique` (`name`,`par_cat`), 
  ADD UNIQUE KEY `categories_url_par_cat_unique` (`url`,`par_cat`), 
  ADD KEY `categories_par_cat_foreign` (`par_cat`);


ALTER TABLE `categories`
  MODIFY `id` int(10) unsigned NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=3;

ALTER TABLE `categories`ADD CONSTRAINT `categories_par_cat_foreign` 
  FOREIGN KEY (`par_cat`) REFERENCES `categories` (`id`);
Run Code Online (Sandbox Code Playgroud)

问题是,即使我有唯一的密钥,它也不起作用.如果我尝试在数据库2中插入已par_cat设置为null相同名称和URL的类别,则可以将这两个类别插入到数据库中而不会出现问题(并且它们不应该).但是,如果我选择其他类别par_cat(例如1假设存在id为1的类别),则只插入第一条记录(这是所需的行为).

问题 - 如何处理这种情况?我读到了:

UNIQUE索引创建一个约束,使索引中的所有值必须是不同的.如果您尝试添加具有与现有行匹配的键值的新行,则会发生错误.此约束不适用于除BDB存储引擎之外的NULL值.对于其他引擎,UNIQUE索引允许包含NULL的列的多个NULL值.如果为UNIQUE索引中的列指定前缀值,则列值在前缀中必须是唯一的.

但是如果我有多个列我预期的唯一它不是的情况下(仅par_cat可以为空,nameurl不能为null).因为par_catid同一个表的引用但某些类别没有父类别,所以它应该允许null值.

Tho*_*ner 29

这符合SQL标准的定义.NULL表示未知.如果您有两个par_cat = NULL和name ='X'的记录,那么这两个NULL不会被视为保持相同的值.因此,它们不违反唯一键约束.(好吧,有人可能会争辩说NULL仍然可能意味着相同的值,但是应用这个规则会使得使用唯一索引和可空字段几乎不可能,因为NULL也可以意味着1,2或其他任何值.所以他们做得很好如他们所做的那样定义它.)

由于MySQL不支持您可以使用索引的功能索引,因此ISNULL(par_cat,-1), name您唯一的选择是使par_cat为0或-1的NOT NULL列或"无父"的任何内容,如果您希望约束起作用.

  • 或者将id为0的虚拟记录放入类别中. (5认同)
  • 你是对的,没有完美的解决方案.我将该虚拟记录放在表中,因此数据库完整性由外键约束保护.你是对的,但是每次排除类别0都要记得很烦人.为了方便起见,你可以在该表上写一个排除记录的视图.至于惯例:我不知道.我的anwer中提到的功能索引会很棒.如果它在MySQL中可用,那就是我选择的道路. (4认同)
  • 好的,我明白了.但是如果我不使它为NULL,我将无法插入`par_cat`值0,因为外键约束`FOREIGN KEY(`par_cat`)REFERENCES`category`(`id`);`.那么这是否意味着我必须在2个约束之间进行选择 - 使用外键(使用null)并且不使用unique或使用unique但在`par_cat`上删除外键,因为这两个约束不能一起工作? (2认同)
  • 好的,但是在创建外键之前我需要将其放在首位,否则无法将其插入数据库中,对吗?在这种情况下,通常的做法是什么?放置0条记录(如您建议的那样)还是不使用外键或唯一键?使用“ 0记录”使万一我想从数据库中获取所有内容,则需要始终从结果中删除那些“ 0记录” (2认同)

Yos*_*eph 12

我看到这是在 2014 年被问到的。但是它经常从 MySQL 请求:https : //bugs.mysql.com/bug.php ? id =8173https://bugs.mysql.com/bug.php?id =17825例如。人们可以点击影响我来尝试从 MySQL 中获得关注。

从 MySQL 5.7 开始,我们现在可以使用以下解决方法:

ALTER TABLE categories 
ADD generated_par_cat INT UNSIGNED AS (ifNull(par_cat, 0)) NOT NULL,
ADD UNIQUE INDEX categories_name_generated_par_cat (name, generated_par_cat), 
ADD UNIQUE INDEX categories_url_generated_par_cat (url, generated_par_cat); 
Run Code Online (Sandbox Code Playgroud)

generate_par_cat 是一个虚拟的生成列,所以它没有存储空间。当用户插入(或更新)时,唯一索引会导致生成的 generate_par_cat 的值即时生成,这是一个非常快速的操作。

  • 谢谢你!得益于 MySQL 5.7 的[“生成列”功能](https://dev.mysql.com/doc/refman/5.7/en/create-table- generated-columns.html),可以确认这是有效的。我们遇到了唯一索引日期时间(软删除:deleted_at 列)的 NULL 问题,因此我们不是将 NULL 转为零,而是将 `\`stringified_deleted_at\` char(19) GENERATED ALWAYS AS (CONVERT(IFNULL(deleted_at, " "),char)) 已存储` (2认同)

Kid*_*ang 7

以防万一你来自 Laravel...

这是 Laravel 的 Virtual Column 迁移版本,用于解决UNIQUE其中一列NULL有价值时的问题

$table->integer('generated_par_cat')->virtualAs('ifNull(par_cat, 0)');

$table->unique(['name', 'generated_par_cat'], 'name_par_cat_unique');
Run Code Online (Sandbox Code Playgroud)