我有一个 MySQL 表,people
,它看起来像这样:
id | object_id | name | sex | published
----------------------------------------------
1 | 1 | fred | male | [timestamp]
2 | 2 | john | male | [timestamp]
我有两个 id
的原因是,在我的 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 来跟踪草稿/已发布的行时,如何引用主键?
最佳答案
您似乎想要保留数据的版本,并且遇到了如何维护指向版本化数据的外键指针的老问题。解决方法其实很简单,事实证明它是第二范式的特例。
获取以下员工数据:
EmpNo FirstName LastName Birthdate HireDate Payrate DeptNo
现在您的任务是在数据发生变化时维护数据版本。然后您可以添加一个日期字段来显示数据更改的时间:
EmpNo EffDate FirstName LastName Birthdate HireDate Payrate DeptNo
生效日期字段显示每个特定行生效的日期。
但问题是 EmpNo,它是表的完美主键,不能再满足这个目的。现在每个员工可以有很多条目,除非我们想在每次更新员工数据时分配一个新的员工编号,否则我们必须找到另一个或多个关键字段。
一个明显的解决方案是将 EmpNo 和新的 EffDate 字段组合为主键。
好的,这解决了 PK 问题,但是现在其他表中引用特定员工的任何外键呢?我们也可以将 EffDate 字段添加到这些表中吗?
当然可以。但这意味着外键不再指代某个特定员工,而是指代某个特定员工的一个特定版本。正如他们所说,不是名义上的。
已经实现了许多方案来解决这个问题(请参阅“Slowly Changing Dimension”的维基百科条目以获取一些更受欢迎的列表)。
这是一个简单的解决方案,可让您对数据进行版本控制并单独保留外键引用。
首先,我们意识到并非所有数据都会发生变化,因此永远不会更新。在我们的示例元组中,此静态数据是 EmpNo、FirstName、Birthdate、HireDate。可能会更改的数据是 LastName、Payrate、DeptNo。
但这意味着像 FirstName 这样的静态数据依赖于 EmpNo——原始 PK。可变或动态数据,如 LastName(可能因婚姻或收养而改变)取决于 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 是当前系统时间,它返回当前数据。这意味着查看当前数据的查询和查看过去数据的查询实际上是同一个查询。
关于php - 具有两个 ID 字段的 MySQL 主键,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32617128/