NULL的唯一键

时间:2010-11-02 20:24:59

标签: mysql database null relational-model

这个问题需要一些假设的背景。让我们考虑使用MySQL作为RDBMS的employee表,其中包含namedate_of_birthtitlesalary列。因为如果任何一个人的姓名和出生日期与另一个人相同,那么根据定义,他们是同一个人(除非我们有两个人在1809年2月12日出生的亚伯拉罕·林肯出生的惊人巧合),我们将放一个namedate_of_birth上的唯一键,表示“不要将同一个人存储两次”。现在考虑这个数据:

id name        date_of_birth title          salary
 1 John Smith  1960-10-02    President      500,000
 2 Jane Doe    1982-05-05    Accountant      80,000
 3 Jim Johnson NULL          Office Manager  40,000
 4 Tim Smith   1899-04-11    Janitor         95,000

如果我现在尝试运行以下语句,它应该会失败:

INSERT INTO employee (name, date_of_birth, title, salary)
VALUES ('Tim Smith', '1899-04-11', 'Janitor', '95,000')

如果我尝试这个,它会成功:

INSERT INTO employee (name, title, salary)
VALUES ('Jim Johnson', 'Office Manager', '40,000')

现在我的数据将如下所示:

id name        date_of_birth title          salary
 1 John Smith  1960-10-02    President      500,000
 2 Jane Doe    1982-05-05    Accountant      80,000
 3 Jim Johnson NULL          Office Manager  40,000
 4 Tim Smith   1899-04-11    Janitor         95,000
 5 Jim Johnson NULL          Office Manager  40,000

这不是我想要的,但我不能说我完全不同意发生的事情。如果我们谈论数学集,

{'Tim Smith', '1899-04-11'} = {'Tim Smith', '1899-04-11'} <-- TRUE
{'Tim Smith', '1899-04-11'} = {'Jane Doe', '1982-05-05'} <-- FALSE
{'Tim Smith', '1899-04-11'} = {'Jim Johnson', NULL} <-- UNKNOWN
{'Jim Johnson', NULL} = {'Jim Johnson', NULL} <-- UNKNOWN

我的猜测是,MySQL说,“因为我知道NULL出生日期的Jim Johnson尚未出现在此表中,我将添加他。 “

我的问题是:即使date_of_birth并不总是知道,我怎样才能防止重复?到目前为止,我提出的最好方法是将date_of_birth移至一张不同的桌子。然而,问题在于,我可能最终会选择两个同名,收视率和工资相同的出纳员,不同的出生日期,并且没有任何方法可以存储它们而没有重复。

10 个答案:

答案 0 :(得分:22)

唯一键的基本属性就是这样 它必须是独一无二的。制作该关键Nullable的一部分会破坏这个属性。

您的问题有两种可能的解决方案:

  • 一种方式,错误的方式,就是使用一些魔法日期来代表未知。这只是让你过去 DBMS“问题”但在逻辑意义上没有解决问题。 预计两个“约翰史密斯”条目的日期未知存在问题 出生。这些人是同一个人还是他们独一无二的人? 如果你知道它们不同,那么你又回到了同样的老问题 - 您的唯一钥匙不是唯一的。甚至不要考虑分配一系列魔术日期 代表“未知” - 这真是通往地狱的道路。

  • 更好的方法是将EmployeeId属性创建为代理键。这只是一个 您分配给知道的个人的任意标识符是唯一的。这个 标识符通常只是一个整数值。 然后创建一个Employee表来关联EmployeeId(唯一的,不可为空的 在这种情况下,你认为是依赖的属性 出生的姓名和出生日期(任何可以为空的)。在您的任何地方使用EmployeeId代理键 以前使用名称/出生日期。这会为您的系统添加一个新表 以健壮的方式解决了未知值的问题。

答案 1 :(得分:6)

我认为MySQL就是在这里做的。其他一些数据库(例如Microsoft SQL Server)将NULL视为一个只能插入UNIQUE列的值,但我个人认为这是一种奇怪且意外的行为。

然而,由于这是你想要的,你可以使用一些“魔术”值而不是NULL,例如过去很久的日期

答案 2 :(得分:5)

您没有基于名称的重复项的问题是无法解决的,因为您没有自然键。为出生日期未知的人提供假日期并不能解决您的问题。 1900年1月1日出生的约翰史密斯仍然是与1960/03/09出生的约翰史密斯不同的人。

我每天使用来自大型和小型组织的名称数据,我可以向您保证,他们总是有两个不同的名字。有时候有相同的职称。出生日并不能保证独特性,很多约翰史密斯出生于同一天。当我们与医生办公室数据合作时,我们经常有两名医生,他们的名字,地址和电话号码相同(父子组合)

如果您要插入员工数据以唯一地识别每个员工,最好的办法是拥有员工ID。然后检查用户界面中的uniquename以及是否有一个或多个匹配项,询问用户是否表示他们,如果他说不,请插入记录。然后构建一个deupping进程来解决问题,如果有人偶然分配了两个id。

答案 3 :(得分:3)

还有另一种方法可以做到这一点。添加列(不可为空)以表示date_of_birth列的String值。如果date_of_birth为null,则新列值将为“”(空字符串)。

我们将列命名为 date_of_birth_str ,并创建一个唯一约束员工(name,date_of_birth_str)。因此,当两个记录具有相同的名称和null date_of_birth值时,唯一约束仍然有效。

但是应该仔细考虑维护两个相同含义列的努力,以及新列的性能损害。

答案 4 :(得分:1)

我建议创建其他表格列checksum,其中包含namedate_of_birth的md5哈希值。删除唯一键(name, date_of_birth),因为它无法解决问题。在校验和上创建一个唯一键。

ALTER TABLE employee 
    ADD COLUMN checksum CHAR(32) NOT NULL;

UPDATE employee 
SET checksum = MD5(CONCAT(name, IFNULL(date_of_birth, '')));

ALTER TABLE employee 
    ADD UNIQUE (checksum);

此解决方案会产生很小的技术开销,导致您需要生成散列的每个插入对(每个搜索查询都相同)。为了进一步改进,您可以添加触发器,在每个插入中为您生成哈希:

CREATE TRIGGER before_insert_employee 
BEFORE INSERT ON employee
FOR EACH ROW
    IF new.checksum IS NULL THEN
      SET new.checksum = MD5(CONCAT(new.name, IFNULL(new.date_of_birth, '')));
    END IF;

答案 5 :(得分:0)

完美的解决方案是支持基于函数的UK,但随着mySQL也需要支持基于函数的索引,这变得更加复杂。这样可以避免使用“假”值代替NULL,同时还允许开发人员决定如何处理UK中的NULL值。不幸的是,mySQL目前不支持我所知道的这种功能,所以我们留下了解决方法。

CREATE TABLE employee( 
 name CHAR(50) NOT NULL, 
 date_of_birth DATE, 
 title CHAR(50), 
 UNIQUE KEY idx_name_dob (name, IFNULL(date_of_birth,'0000-00-00 00:00:00'))
);

(注意在唯一键定义中使用 IFNULL()功能)

答案 6 :(得分:0)

我遇到了类似的问题,但有一点麻烦。在您的情况下,每个员工都有一个生日,虽然可能是未知的。在这种情况下,系统为具有未知生日但其他方面相同信息的员工分配两个值具有逻辑意义。 NealB接受的答案非常准确。

但是,我遇到的问题是数据字段不一定有值的问题。例如,如果向表中添加了“name_of_spouse”字段,则表中的每一行都不一定有值。在这种情况下,NealB的第一个要点(“错误的方式”)实际上是有道理的。在这种情况下,对于没有已知配偶的每一行,应在列name_of_spouse中插入字符串“None”。

我遇到这个问题的情况是编写一个带有数据库的程序来对IP流量进行分类。目标是在专用网络上创建IP流量图。每个数据包都放入一个数据库表中,该数据库表具有基于其ip源和dest,端口源和dest,传输协议和应用程序协议的唯一连接索引。但是,许多数据包根本没有应用程序协议。例如,所有没有应用程序协议的TCP数据包都应归为一类,并且应该占用连接索引中的一个唯一条目。这是因为我希望这些数据包形成我的图形的单个边缘。在这种情况下,我从上面提出了自己的建议,并在应用程序协议字段中存储了一个字符串'None',以确保这些数据包形成一个唯一的组。

答案 7 :(得分:0)

您可以添加一个生成的列,其中NULL的值由未使用的常量代替,例如零。然后,您可以将唯一约束应用于此列:

CREATE TABLE employee ( 
  name VARCHAR(50) NOT NULL, 
  date_of_birth DATE, 
  uq_date_of_birth DATE AS (IFNULL(date_of_birth, '0000-00-00')) UNIQUE
);

答案 8 :(得分:0)

我正在寻找一种解决方案,建议的Alexander Yancharuk对我来说是个好主意。但是在我的情况下,我的列是外键,而employee_id可以为null。

我有这样的结构:


+----+---------+-------------+
| id | room_id | employee_id |
+----+---------+-------------+
|  1 |       1 | NULL        |
|  2 |       2 | 1           |
+----+---------+-------------+

并且不能复制具有employee_id NULL 的room_id

我解决了在插入之前添加触发器的问题,

DELIMITER $$
USE `db`$$
CREATE DEFINER=`root`@`%` TRIGGER `db`.`room_employee` BEFORE INSERT ON `room_employee` FOR EACH ROW
BEGIN
    IF EXISTS (
            SELECT room_id, employee_id
            FROM room_employee
            WHERE (NEW.room_id = room_employee.room_id AND NEW.employee_id IS NULL AND room_employee.employee_id IS NULL)
        ) THEN
        CALL `The room Can not be duplicated on room employee table`;
    END IF;
END$$
DELIMITER ;

我还为 room_id employee_id

添加了唯一的约束

答案 9 :(得分:-2)

简单来说,唯一约束的作用是创建字段或列。 null 会破坏此属性,因为数据库会将null视为 unknown

为了避免重复并允许null:

  

将唯一键设为主键