【问题标题】:MySQL primary keys with two ID fields具有两个 ID 字段的 MySQL 主键
【发布时间】:2015-12-13 13:02:41
【问题描述】:

我有一个 MySQL 表 people,如下所示:

id | object_id | name | sex   | published
----------------------------------------------
 1 |     1     | fred | male  | [timestamp]
 2 |     2     | john | male  | [timestamp]

我有两个 ids 的原因是,在我的 CRUD 应用程序中,用户可能会编辑现有对象,在这种情况下它变成草稿,所以我有两行(草稿记录和已经存在的记录) 使用相同的object_id,如下所示:

id | object_id | name | sex      | published
----------------------------------------------
 2 |     2     | john | male     | [timestamp]
 3 |     2     | john | female   | NULL

这使我可以跟踪记录的草稿和发布状态。当id 为 3 的行发布时,其published 字段将被标记并删除已发布的行。

每个人也有工作经历,所以我有一张桌子history

id | person_object_id | job
----------------------------------
 1 |         2        | dev
 2 |         2        | accountant

这是约翰的工作经历。我在 person_object_id 字段中引用了 John 的 object_id,因为如果我引用他的 id,如果我删除了上面示例中的 John 行之一,我将冒着断开两个表的风险。

所以我的问题是:使用非主键(object_id 而不是id)来引用一个表,就像我上面所做的那样,不是效率低下吗?当我需要非唯一 id 来跟踪草稿/已发布行时,如何引用主键?

【问题讨论】:

  • 只要列有索引,不管是不是主键。
  • 嗯,好的。我对数据库设计的认识很模糊,但我认为主键的意义在于加快对特定行的访问。
  • 谢谢。 irudyak 的回答似乎完全符合我的用例?

标签: php mysql database-design primary-key


【解决方案1】:

您似乎想要保留数据的版本,但您遇到了如何维护指向版本化数据的外键指针的古老问题。解其实很简单,原来是第二范式的特例。

获取以下员工数据:

EmpNo FirstName LastName Birthdate HireDate Payrate DeptNo

现在,您的任务是在数据变化时维护数据版本。然后,您可以添加一个显示数据更改时间的日期字段:

EmpNo EffDate FirstName LastName Birthdate HireDate Payrate DeptNo

“生效日期”字段显示每个特定行生效的日期。

但问题在于,作为表的完美主键的 EmpNo 不能再用于此目的。现在每个员工可以有很多条目,除非我们想在每次更新员工数据时分配一个新的员工编号,否则我们必须找到另一个或多个关键字段。

一个明显的解决方案是让 EmpNo 和新的 EffDate 字段的组合成为主键。

好的,这解决了 PK 问题,但是现在其他表中引用特定员工的任何外键呢?我们也可以将 EffDate 字段添加到这些表吗?

嗯,当然,我们可以。但这意味着外键现在指的是一位特定员工的一个特定版本,而不是指一个特定的员工。不像他们说的那样,是名义上的。

已经实施了许多方案来解决这个问题(请参阅“Slowly Changing Dimension”的 Wikipedia 条目以获取一些更流行的列表)。

这是一个简单的解决方案,可让您对数据进行版本控制,并单独保留外键引用。

首先,我们意识到并非所有数据都会发生变化,因此永远不会更新。在我们的示例元组中,此静态数据是 EmpNo、FirstName、Birthdate、HireDate。然后可能会更改的数据是 LastName、Payrate、DeptNo。

但这意味着像 FirstName 这样的静态数据依赖于 EmpNo - 原始 PK。可变或动态数据,例如姓氏(可能因结婚或收养而改变)取决于 EmpNo 和 EffDate。我们的元组不再是第二范式!

所以我们标准化。我们知道该怎么做,对吧?我们闭着眼睛。关键是,当我们完成后,我们有一个主实体表,每个实体定义只有一行。所有外键都可以将此表引用到一个特定的员工——这与我们出于任何其他原因进行规范化时相同。但现在我们也有一个版本表,其中包含所有可能不时更改的数据。

现在我们有两个元组(至少两个 - 可能已经执行了其他规范化过程)来表示我们的员工实体。

EmpNo(PK) FirstName Birthdate  HireDate
=====     ========= ========== ==========
1001      Fred      1990-01-01 2010-01-01

EmpNo(PK) EffDate(PK)    LastName Payrate DeptNo
=====     ========       ======== ======= ======
1001      2010-01-01     Smith    15.00   Shipping
1001      2010-07-01     Smith    16.00   IT

用所有版本化数据重建原始元组的查询很简单:

select  e.EmpNo, e.FirstName, v.LastName, e.Birthdate, e.Hiredate, v.Payrate, v.DeptNo
from    Employees e
join    Emp_Versions v
    on  v.EmpNo = e.EmpNo;

仅使用最新数据重建原始元组的查询并不是非常复杂:

select  e.EmpNo, e.FirstName, v.LastName, e.Birthdate, e.Hiredate, v.Payrate, v.DeptNo
from    Employees e
join    Emp_Versions v
    on  v.EmpNo = e.EmpNo
    and v.EffDate =(
        select  Max( EffDate )
        from    Emp_Versions
        where   EmpNo = v.EmpNo );

不要让子查询吓到你。仔细检查表明,它使用索引查找而不是大多数其他方法将生成的扫描来定位所需的版本行。试试吧——它很快(当然,不同的 DBMS 的里程可能会有所不同)。

但这就是它变得非常好的地方。假设您想查看特定日期的数据是什么样的。该查询会是什么样子?只需使用上面的查询并做一个小补充:

select  e.EmpNo, e.FirstName, v.LastName, e.Birthdate, e.Hiredate, v.Payrate, v.DeptNo
from    Employees e
join    Emp_Versions v
    on  v.EmpNo = e.EmpNo
    and v.EffDate =(
        select  Max( EffDate )
        from    Emp_Versions
        where   EmpNo = v.EmpNo
            and EffDate <= :DateOfInterest ); --> Just this difference

最后一行可以“回到过去”以查看数据在过去任何特定时间的样子。并且,如果 DateOfInterest 是当前系统时间,则返回当前数据。这意味着查看当前数据的查询和查看过去数据的查询实际上是同一个查询。

【讨论】:

  • 一个漂亮的答案,谢谢!这需要一些时间才能完成,我觉得不够专业,无法将其标记为正确。如果其他人可以在这里权衡,我会相信社区。​​span>
  • 这是否意味着我的问题的其他答案是错误的?不是指责,而是因为我想了解这里的利害关系。
  • 很少有一个“正确”的答案而所有其他的都是“错误的”。通常有几个好的答案,也可能有一些不太好的答案。 “最佳”答案是一种主观评价,因为您的特定技术环境、能力、管理结构、用户需求和其他考虑因素的完整列表使最佳答案之一成为您的最佳答案。由于环境不同,其他人可能会认为不同的答案是最好的。恐怕这是我们无法为您做出的一项决定。
【解决方案2】:

只要您对该列有索引(非唯一索引),这并不重要。比它几乎一样快

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-10
    • 1970-01-01
    • 2011-03-20
    • 1970-01-01
    • 2013-01-21
    • 2012-01-31
    • 2022-08-20
    • 1970-01-01
    相关资源
    最近更新 更多