工作笔记-数据库表记录删除操作的思考和处理

61 阅读9分钟

概述

在日常的业务系统中,数据库表记录的删除,是一个非常常见的业务操作。

基本上,对于这个操作,业界通常有两种技术实现方案,一个是确实执行执行数据库记录的删除,就是执行delete语句,删除满足条件的表记录;一种是实现一种逻辑删除,就是在数据库表设计的时候,设计一个“删除状态”字段,删除的时候并不是真正的删除这个记录,而是给当前这条记录做一个删除的标识, 比如执行Update ... set is_delete = 1 ...。

两种方式各有优势。前者操作简单直接,业务记录删除了之后,就可以直接反映到后续的操作和查询当中,相关删除的信息就无效了。而后者保留了一定的“余地”,特别是如果这个记录还关联到其他信息,可以保证查询的时候不会有关联错误。当然后者需要在开发的时候,都要考虑到这个删除标识,给程序和实现带来了一些额外的工作。此外,未删除的记录,显然还会占用一定的计算和存储资源,特别是在比较大的表中,需要为删除状态增加相关的过滤机制,可能对性能造成一定的影响。

选择何种实现方案,取决于具体的业务需求和技术条件,没有绝对的好坏。本文所讨论的场景,是建立在笔者正在维护的一个应用系统,它使用了逻辑删除的方案,但笔者觉得当前的实现并不是很好,有一些改进和优化的空间。

对删除业务的思考

笔者认为,之所以引入“逻辑删除”的技术处理方案,是考虑到以下需求和好处:

  • 可追溯性和恢复性

在任何系统中,信息的删除或者清除,也许有它的业务合理之处,但大多数都是一种敏感的操作,需要系统对这个操作保持某种可追溯性和可恢复性。

但要考虑到,删除数据的恢复,在业务系统中,一般不会是一个标准的业务需求,而是一种比较非常规的技术处理。所以只需要考虑到技术上的可行性就可以了, 一般都不需要直接体现在业务应用系统当中。

  • 关联数据

对于数据间有逻辑关联关系的数据,删除数据可能会破坏数据间的关系和数据完整性,如果程序设计不当,甚至会影响到程序的正常运行。而逻辑删除,就不会在数据结构上造成这个问题。

显然,逻辑删除也会带来一些负面的影响,这个在笔者现在维护的系统上,体现的就非常明显。后面有一个示例。

  • 应用开发的复杂性

逻辑删除,需要在数据库表设计时,保留一个逻辑删除状态的字段,并且在很多查询和关联查询的场合,要考虑到这个逻辑删除状态,这些都需要体现在相关的实现操作SQL和代码当中,为程序的维护带来的复杂性。后续的开发人员,如果不熟悉或者遗漏了这个情况,会造成程序执行的错误或者数据遗漏(笔者就遇到了这个情况)。

  • 信息重复冲突

还有一个更严重的情况,这是笔者业务系统中,遇到的现实的问题。用户由于某种原因,删除了一些信息记录;随后又增加了相同的记录。当然在数据库中,使用了自增的主键,这个操作不会产生数据层面的冲突。但后面业务操作中,使用一些业务字段进行操作的时候,由于数据库中出现了实际业务层面的“重复”数据的时候,就出了问题,某些业务操作就无法继续。特别让人无语的是,由于表面上旧数据已经被删除了,用户并不知道这个问题造成的原因,最后只能由运维人员来排查这个问题,给运维工作造成了很大的负担和额外的工作,实在是得不偿失。

  • 性能影响

逻辑删除,对于应用处理性能的影响,不仅仅是增加了一个记录字段的信息的问题。而是在所有相关操作和查询过程中,都需要考虑这个删除的状态,基于此状态的查询和数据过滤,是不是也应该加上索引? 笔者觉得,这个代价还是比较高昂的,超过了它带来的好处。

优化方案

针对前面提出的问题和不便,笔者提出可以使用下面的技术处理方案:

1 在数据库中创建一个通用的“删除信息”的数据表,所有需要记录删除信息的业务数据,都可以用这个表保存删除的信息

2 在所有的信息删除操作时,将删除的信息都记录在这个表当中

3 这个表平时不参与任何实际的业务操作,但在需要时,技术人员可以从这个表中查询可能的删除信息,并且在需要时可以方便的将特定数据进行恢复。

4 如果需要增加一种需要进行删除操作存档的业务,只需要在业务删除代码中,增加针对此业务的部分,就可以了,这可能体现在后面示例中的业务前缀、ID和数据库表

下面探讨一下相关的技术细节:

  • 删除信息记录表

借助Postgres强大的数据类型的支持,这个数据库表的结构可以设计成以下的样子:


CREATE TABLE public.sy_deleted (
	rid varchar PRIMARY KEY ,
	"content" jsonb NULL,
	dtime int4 NULL,
);

为了保证通用性,这个表可以只设计成为三个字段。

第一个字段 rid, 就是删除记录的标识,可以使用前者+业务ID的形式,或者其他的什么方式,保证这个信息是唯一的,这在业务系统代码中非常容易实现。

第二个字段 content ,就是删除记录的内容,这里使用jsonb来存储任意表的行记录,就可以提高一种通用性。当然代价是需要付出一定的存储资源。但考虑到这个表不会直接参与业务,这个影响其实是非常小的。

第三个字段 dtime,标识删除操作的时间,这是方便后续查询和追溯,当然也可以直接用默认的时间戳。

笔者应用系统和本文示例中,使用的是:

PostgreSQL 13.10 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44), 64-bit

但实际上,这个处理方案和思维方式是通用的,应该和所使用的数据库系统和编程语言关系不大,只需要做相应的调整,就可以适配不同的系统。

  • 删除操作

如果使用了删除信息记录表,在业务数据进行删除的时候,就需要对删除语句进行一定的改造,在删除的同时,可以将删除的数据,插入到删除数据表中。


with D as ( delete from "B_Student" where "IsDeleted" = 1 
returning  'STU_'||"StudentID", row_to_json("B_Student"), round(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) / 60))
insert into sy_deleted select * from D

改进的实现方案是合并了删除和插入的操作,让记录的删除,和删除备份使用一个语句来执行。这里使用了一个前缀,结合业务ID来区别不同类型的业务数据。业务数据表的行,可以使用row_to_json,直接将整个数据行转换成为一个JSON对象,存储在删除数据表的字段中。这里还使用了一个时间转换函数,将时间转换成为一个整数进行存储。

  • 触发器

有些读者可能考虑到,是不是可以使用触发器,来在记录删除的时候,自动进行处理。

这听起来很酷,但笔者并不建议这样操作。理由是程序维护、迁移和一致性的困难。而在代码中实现,只需要维护代码就可以了,不需要考虑数据库的情况,也只跟当前的业务模块相关。另外,删除记录这种敏感的操作,也不会有很多使用的场景,所以这并不是一个严重的问题。

  • 数据查询和恢复

一般的删除数据查询,和数据恢复的流程和操作如下:

// 查询已删除的记录
select * from "B_Student" where "StudentID" = 158547;
select * from sy_deleted where rid = 'STU_158547';

// 从删除记录表中恢复数据
INSERT INTO "B_Student" 
SELECT (jsonb_populate_record(NULL::"B_Student", content)).*
FROM sy_deleted where rid = 'STU_158547' ;

// 检查已恢复的记录
select * from "B_Student" where "StudentID" = 158547;

注意这里数据恢复的核心是使用和数据库表相同结构的数据库字段内容,当然如果这个内容是从数据库表生成的,这是可以保证的。 然后使用jsonb_populate_record方法,可以映射这个表结构, 来构造恢复并插入原表的信息。

当然,这里的数据查询,就和一般的数据库查询一样,也可以基于时间,或者基于jsonb字段中的内容来进行搜索。

使用非删除标记视图

这个稍微补充一些相关的信息。就是如果确实需要使用逻辑删除模式的话,除了引用原始数据表,增加并使用删除标记来进行查询之外,还可以考虑,基于删除标记的查询,创建一个非标记为删除的视图,然后使用视图进行工作,这一可以简化代码的编写和管理。笔者认为应当优先考虑这种技术处理方式。

小结

本文分析了笔者在业务应用系统中,遇到的由于数据逻辑删除标识带来的一些问题,并展开讨论了各种数据删除处理和跟踪的技术方案。随后,笔者提出了一种通用和优化的技术处理方案,应当能够更好的满足删除数据业务所需要的数据完整性、性能影响和维护方便等方面的要求。