MySQL主键有两个ID字段

时间:2015-09-16 19:30:49

标签: php mysql database-design primary-key

我有一个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

这是约翰的工作经历。我在object_id字段中引用了约翰的person_object_id,因为如果我提到他的id,如果我删除了其中一个约翰行,我将面临脱钩这两个表的风险,如上例所示

所以我的问题是:如上所述,使用非主键(object_id而非id)引用表格效率不高吗?当我需要一个非唯一ID来跟踪草稿/已发布的行时,如何引用主键?

2 个答案:

答案 0 :(得分:1)

看起来您希望保留数据的版本,并且您遇到了如何维护版本化数据的外键指针的古老问题。解决方案实际上很简单,事实证明它是第二范式的特殊情况。

获取以下员工数据:

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

最后一行可以及时回归&#34;查看过去任何特定时间的数据。并且,如果DateOfInterest是当前系统时间,则返回当前数据。这意味着查看当前数据的查询和查看过去数据的查询实际上是相同的查询。

答案 1 :(得分:0)

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