维护数据库表中的记录历史记录

时间:2020-09-19 05:44:31

标签: mysql sql rdbms

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

例如,我有两个表s_tbl_bill和帐单信息,其中s_tbl_bill具有帐单信息,而s_def_department具有单位的定义。使用s_tbl_bill中的键billing_department将两个表关联起来。

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;

现在我遇到的问题是自动递增的主键。当我修改记录时,旧记录被设置为非活动状态,并且新记录中添加了新的主键,并且我正在使用主键将记录加入查询中。由于新记录是同一个旧元组的新版本,所以新的主键在加入时给我带来了问题。为了解决这个问题,我在表“ 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');

我有一个ID为10的汇票,该汇票于2018-06-19印刷。现在,今天的审计正在进行中,希望找出从哪个部门帐单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

我的方法是否有需要改进的地方?任何建议都是非常有价值的。

2 个答案:

答案 0 :(得分:1)

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

customer_id和日期的组合是主键。

因此您的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

所以我们要做的是,选择具有相应ID的另一个表的字段。要使用最新版本,请使用order by valid_from desc limit 1

因此,如果您想要第一个条目,则只需使用order by valid_from asc limit 1

答案 1 :(得分:1)

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

因为对此表的几乎所有查询和联接都需要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

编辑 另一种方法是使用两个表。一种用于存储商品的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仍然需要几个查询。


这两种方法都会增加许多广泛的性能损失和生产复杂性,因为该功能可能只会在少数几个地方使用。我会推荐一个历史记录表。它使生产表和代码保持不变。数据可以存储在JSON中,而不必重新创建表结构。考虑类似paper-trail之类的东西。