在数据库中维护记录历史 table

Maintaining record history in a database table

我现在正在设计 MySQL 数据库。客户要求之一是维护某些记录的记录历史 tables。我参考了互联网上的一些文章,这些文章建议我维护单独的历史记录 table,但我不喜欢这个想法。我在堆栈溢出 Is there a MySQL option/feature to track history of changes to records? 中得到了一个绝妙的主意,并对我的数据库进行了更改。我寻求使用“valid_date_from”和“valid_date_to”标志在同一 table 上维护记录历史记录的解决方案,而不是维护单独的历史记录 table.

例如我有两个 tables s_tbl_bill 有账单信息和 s_def_department 有 deparment 的定义。使用 s_tbl_bill 中的键 billing_department 关联的两个 table。

CREATE TABLE `s_tbl_bill` (
  `id` int NOT NULL AUTO_INCREMENT,
  `billing_department` int,
  `customer_id` mediumtext NOT NULL,
  `billed_date` datetime DEFAULT NULL,
  `is_active` enum('Y','N') DEFAULT 'Y',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `s_def_department` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name_eng` varchar(100) NOT NULL,
  `parent_id` int DEFAULT NULL,
  `phone` varchar(50) DEFAULT NULL,
  `is_active` varchar(50) DEFAULT 'Y',
  `created_timestamp` datetime DEFAULT CURRENT_TIMESTAMP,
  `valid_from` datetime DEFAULT CURRENT_TIMESTAMP,
  `valid_until` datetime DEFAULT NULL,
  `author_id` int DEFAULT NULL,
  `id_first` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

现在我遇到的问题是自动递增的主键。当我修改记录时,旧记录被设置为非活动状态,并使用新的主键添加了新记录,并且我使用主键将记录加入查询中。由于新记录是同一个旧元组的新版本,新主键在加入时给我带来了问题。为了解决这个问题,我在 table “id_first” 中添加了另一个字段,该字段包含首次创建新记录时记录的主键。

对于场景,

INSERT INTO `s_tbl_bill` (`id`, `billing_department`, `customer_id`, `billed_date`, `is_active`) 
VALUES ('10', '2', '5', '2018-06-19 13:00:00', 'Y');

INSERT INTO `s_def_department` (`id`, `name_eng`, `phone`, `is_active`, `created_timestamp`, `valid_from`, `valid_until`, `id_first`) 
VALUES ('2', 'DVD Store', '014231232', 'N', '2018-01-01', '2018-01-01 ', '2019-01-01', '2');

INSERT INTO `s_def_department` (`id`,`name_eng`, `phone`, `is_active`, `created_timestamp`, `valid_from`, `id_first`) 
VALUES ('14','Video Store', '012321223', 'Y', '2019-01-02', '2019-01-2', '2');

我有 2018-06-19 打印的 id 10 的账单。现在,在今天的日期,正在进行审计,并希望找出从哪个部门打印账单 10。但印制账单的部门将其名称从 DVD Store 更改为 Video Store。为了找出我 运行 以下查询。

select name_eng as dept_name
from s_tbl_bill b join s_def_department d on b.billing_department = d.id_first
where b.id = '10' and d.valid_from <= b.billed_date and d.valid_until >= b.billed_date

我的方法有什么可以改进的地方吗?任何建议都将非常有价值。

在链接的问题中,一条评论提到:

The combination of customer_id and the dates are the primary key.

所以你的s_tbl_bill.id不应该改变。
此外,您不需要保存 first_id,因为您可以轻松计算它。

s_def_department` (

  // ...

  PRIMARY KEY (`id`, `valid_from`)
}

INSERT INTO `s_def_department` (`id`,`name_eng`, `phone`, `is_active`, `created_timestamp`, `valid_from`) 
VALUES ('2','Video Store', '012321223', 'Y', '2019-01-02', '2019-01-2');

select (
  select name_eng 
  from s_def_department d
  where b.billing_department=d.id 
  order by valid_from desc 
  limit 1
) as dept_name
from s_tbl_bill b

# if you want only 1 record
where b.id = 10

所以我们所做的是,我们 select 另一个 table 的字段,具有相应的 ID。要获取最新的,我们使用 order by valid_from desc limit 1.

所以如果你想要第一个条目,你可以只使用order by valid_from asc limit 1

考虑在 is_active 上使用 table partitioning。由于大多数查询将需要 where is_active = 'Y',这将通过仅将活动行放在一个 table 来避免一些索引和性能问题。如果您还按 valid_until 进行分区,则可以控制非活动分区的大小,并通过简单地删除分区来有效截断旧历史记录。

因为几乎所有对此 table 的查询和连接都需要 is_active = 'Y' 强烈考虑使用可以一致地应用此范围的 ORM。

一个大的性能问题和复杂性是必须进行多次查询,而不是一次 update 来写入更改。这些需要在事务中以避免竞争条件。例如,假设您要更新 id 42 和 id_first 23.

begin

-- copy yourself
insert into s_def_department
select * from s_def_department where id = 42 and is_active = 'Y';

-- apply the changes to the new active row and set its tracking columns
update s_def_department
set
  name_eng = 'Something Else',
  valid_until = NULL,
  valid_from = CURRENT_TIMESTAMP
where id = last_insert_id();

-- deactivate yourself
update s_def_department
set is_active = 'N', valid_until = CURRENT_TIMESTAMP
where id = 42;

commit

EDIT 另一种方法是使用两个 table。一个用于存储项目的 ID,一个用于保存数据。

create table s_def_department_ptr (
  id bigint primary key auto_increment,
  data_id bigint not null references s_def_department_data(id)
);

CREATE TABLE `s_def_department_data` (
  `id` bigint not null primary key auto_increment,
  `ptr_id` bigint not null references s_def_department_ptr(id),
  ... and the rest of the data rows plus valid_from and valid_until ...
);

数据变化时,s_def_department_data增加一行,s_def_department_ptr.data_id引用它

这消除了对 is_active 的需要,活动行是 data_id 指向的行,通过将 is_active 关闭查询并提高引用完整性来避免错误。

它还简化了密钥并提高了参照完整性。表格引用 s_def_department_ptr.id.

缺点是它为每个查询添加了一个连接。什么应该是一个简单的 update 仍然需要几个查询。


这两种方法都会增加许多 wide-spread 性能损失和生产复杂性,因为该功能可能只会在少数地方使用。我会推荐历史 table。它使生产 table 和代码保持不变。数据可以存储在 JSON 中,避免重新创建 table 结构。考虑像 paper-trail.

这样的东西