MySQL主键有两个ID字段


MySQL primary keys with two ID fields

我有一个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字段添加到这些表中吗?

当然,我们可以。但这意味着外键不是指一个特定的员工,而是指一个具体员工的一个特定版本。正如他们所说,不是名义上的。

已经实施了许多方案来解决这个问题(请参阅维基百科的"缓慢变化的维度"条目,了解一些更流行的方案(。

这里有一个简单的解决方案,它允许您对数据进行版本化,而不使用外键引用。

首先,我们意识到,并非所有数据都会发生变化,因此永远不会更新。在我们的示例元组中,这个静态数据是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 );

不要让子查询吓到你。仔细检查可以发现,它使用索引搜索而不是大多数其他方法将生成的扫描来定位所需的版本行。试试吧——它很快(当然,不同的数据库管理系统的里程可能会有所不同(。

但这是它真正好的地方。假设您想查看某个特定日期的数据。这个查询会是什么样子?只需接受上面的查询并进行一个小的添加:

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是当前系统时间,则返回当前数据。这意味着,查看当前数据的查询和查看过去数据的查询实际上是同一个查询。

只要该列上有一个索引(不是唯一索引(,这其实并不重要。比快得多