MySQL历史表设计和查询

时间:2018-01-30 22:02:46

标签: mysql join database-design

TL; DR:这个设计是否正确,我应该如何查询?

我们假设我们有 city 地址的历史记录表,其设计如下:

CREATE TABLE city_history (
  id         BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  name       VARCHAR(128)    NOT NULL,
  history_at DATETIME        NOT NULL,
  obj_id     INT UNSIGNED    NOT NULL
);
CREATE TABLE address_history (
  id           BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  city_id      INT             NULL,
  building_no  VARCHAR(10)     NULL,
  history_at   DATETIME        NOT NULL,
  obj_id       INT UNSIGNED    NOT NULL
);

原始表几乎相同,除了 history_id obj_id city:id,name; address:id,city_id,building_no )。 city 地址 city_id )之间也存在外键关系。

历史记录表会在原始条目(创建,更新,删除)的每次更改时填充,并在给定时间显示条目的确切状态。

obj_id 保存原始对象的id - 没有外键,因为原始条目可以删除,历史记录条目也不能。 history_at 是创建历史记录条目的时间。

为每个表单独创建历史记录条目 - 更改城市名称会创建 city_history 条目,但不会创建 address_history 条目。

  1. 因此,要查看任何 T1 时间点的城市整个地址的状态(例如打印文档),我们会从两个历史记录表中获取给定的最新条目 obj_id T1 之前创建,对吗? 理论上我们应该能够在任何给定的时间点使用 city 来查看signle 地址的状态。任何人都可以帮助我为给定的地址ID和时间创建这样的查询吗?请注意,可能有多个记录具有相同的确切时间戳。

  2. 还需要创建一个报告,用于显示给定时间段内给定地址的每个状态变化,其中包含" city_name,building_no,changed_at"等条目。是否可以使用SQL查询创建?性能在这里并不重要,这些报告不会经常产生。

  3. 在用户可以过滤结果的交互式版本中可能需要上述报告,例如按城市名称或建筑物编号。是否仍然可以在SQL中执行?

  4. 实际上地址表和 address_history 表中还有4个外键应该在报告中加入(街道,邮政编码等)。查询不会长达十页,以提供所有必需的功能吗?

  5. 我试图建立一些查询,与每组最大的玩家一起玩,但我不认为我可以随处获得。这个设计对我的用例来说真的很好吗(如果是这样,请你提供一些问题让我一起玩,以便得到我想要的地方?)?或者我应该重新考虑整个设计?

    任何帮助表示感谢。

2 个答案:

答案 0 :(得分:0)

这是一个非常“传统”的问题,当涉及对某一行的更改进行版本控制(或监控)时。

有各种“解决方案”,每种解决方案都有其自身的缺点和优势。

以下“陈述”是我的经验的结果,它们既不完美,也不声称它们是“唯一的”!

1。)创建一个“历史表”:这是最糟糕的想法。您始终需要考虑需要查询的表,具体取决于应查询的DATA。这是一个“鸡蛋”问题......

2。)使用 ONE 表与 ONE (增加)“修订版”编号:这是一种更好的方法,但查询“难以”:确定无论使用哪种方法,“最近一行”每“id”都是非常昂贵的。

我的个人表现是,遵循“双链表”的模式最好解决这个问题,当涉及数百万条记录时:

3。)在每个实体中保留两列,比如prev_version_idnext_version_id。如果没有以前的版本,则prev_version_id指向NULL。如果没有更高版本,则next_version_id指向NULL

这种方法要求您在更新时始终执行两项操作:

  • 创建新行
  • 将旧行引用(next_version_id)更新为刚刚执行的行。

但是,当您的数据库增长到100万行时,您会非常高兴您选择了这条路径:

  • 查询“最旧”版本就像查询where ISNULL(prev_version_id) and entity_id = 5
  • 一样简单
  • 查询“最新”版本就像查询where ISNULL(next_version_id) and entity_id = 5
  • 一样简单
  • 获取完整版本的历史记录只会定位到数据表的entity_id=5,可以按prev_version_idnext_version_id进行排序。

经常被忽视的事实:前两个查询也可用于获取实体的ALL first versionsALL recent versions列表 - 大约在任何时间! (不要低估它可以确定实体的最新版本的“代价高昂”!相信我,当“测试”一切看起来都很好,但真正的斗争开始于使用数百万条记录的实时数据时。)

欢呼声, dognose

答案 1 :(得分:0)

(我的答案是从here复制的,因为该问题从未将答案标记为已接受。)

我的正常模式"在(非常)伪代码中:

  • 表A:a_id(PK),a_stuff
  • 表A_history:a_history_id(PK),a_id(FK引用A.a_id),valid_from,valid_to,a_stuff

触发A:

  • 插入时:使用valid_from = now和valid_to = null将值插入A_history。
  • 更新时:为a_id的最后一条历史记录设置valid_to = now;并在"插入"上执行相同的插入触发该行的更新值。
  • 关于删除:为a_id的最后一个历史记录设置valid_to = now。

在这种情况下,您使用" x> = from和x<来查询历史记录到" ( BETWEEN作为之前的记录' s"来自"值应该匹配下一个""值")。

此外,这种模式还可以改变日志"报告更容易。

  • 如果没有专门用于更改日志记录的表,则只能通过{{1}}找到相关记录。
  • 如果存在中央更改日志表,则只需修改触发器以包含日志条目插入。 (除非日志条目包含"元"数据,例如更改原因,更改者等等......显然)。

注意:此模式可以在没有触发器的情况下实现。使用存储过程,甚至代码中的多个查询,实际上可以否定对非历史表的需求。

  • 历史表" a_id"虽然通常需要替换任何唯一标识记录的内容;它仍然可以是一个id值,但这些值需要在插入时合成,并在更新/删除时知道。
  • 查询:
    • (如果不是新的话)更新最近的条目{{1}}。
    • (如果没有删除)INSERT新条目