前言

Soft Delete 中文叫 "逻辑删", "软删除". 对比的自然就是 Hard Delete.

这篇想聊一聊它的好与坏, 什么时候可以考虑用它.

Hard Delete

在说 soft delete 之前, 我们先来看看 hard delete.

Hard Delete 其实就是普通的 delete 操作. 它是为了与 soft delete 做出区分才刻意叫 hard delete 的.

在和 soft delete 对比下, hard delete 的一些特性被放大了.

1. 一旦执行了 delete, 数据就真的被删除了 (只能通过 SQL Backup, Log 才能回复)

2. Concurrency delete, 执行 delete 操作时, 通过 0 rows affected 判断是否并发.

3. Cascade delete, SQL Server 有自带的 cascade delete 功能, principal 被删除, foreign 也一起被删除

4. Restrict delete, 除了 cascade delete, 也可以设置约束, 一旦有 foreign, principal 就不能被删除. (这个是默认行为)

5. Foreign constraint, 当输入一个不存在的 foreign key, SQL Server 会报错.

除了第一个, 数据不容易被还原以外, 其余的都是好的 feature.

而 soft delete 正是为了解决第一个问题而被提出的.

Step 1: Column Deleted

既然说, 一旦执行了 delete, 数据就真的被删除了, 那就不要执行 delete 咯.

通过增加一个 column 名为 deleted, 然后通过 update deleted = 1 来表示这个 row 已被删除. 这样不就 ok 了吗.

Step 2: Where Deleted = 0

单有表达还不够, 还得有人去理解, SQL Server 自然不可能理解我们的 deleted = 1, 所以接下来, 需要在几乎每一个 table 加上 where 语句

where deleted = 0, 过滤掉已删除的数据.

提醒: 在 inner join 的时候也要过滤哦.

Step 3: Restore Deleted Row

只要把 update deleted = 0 就可以马上还原数据了.

Step 4: Unique Problem

理想很丰满, 现实很骨感. 如果你以为只要付出一点点努力就可以轻松的完成 soft delete, 那就 too young too simple 了.

把"已删除"的数据和普通数据放在一个表内, 第一个会遇到的问题就是 Unique.

因为 SQL Server 并不能智能区分什么数据是已删除的.

第一个想到的方法自然是加一个 filter 在这个 unique 上, filter: deleted = 0

然后你就会发现, 当出现 2 个 deleted and duplicated data 的时候, unique 又报错了.

显然 bool 是没办法解决这个问题的. 使用 DateDeleted 就可以规避这个 unique 的问题了 (总不可能同一个删除时间还能有 duplicated data 了吧)

把 filter 去掉, 在所有 unique 加入 DateDeleted 这个 column. 这样 unique 就不会在撞了.

Step 5: Concurrency Delete

在 hard delete 的情况下, 通过 0 rows affected 来判断是否并发.

在 soft delete 的情况下就不同了, 我们得实现一个 concurrency 的机制, 比如 row version.

Step 6: Cascade Delete

上面说 hard delete 的时候, 有提到一些 SQL Server build-in 的好东西.

但是这些好东西都是基于 hard delete 的, 一旦我们改用 soft delete, 这些 build-in 的机制也跟着没了.

要实现一个 cascade soft delete, 可以使用 trigger.

监听 principal table 的 after update, 如果 DateDeleted 从 null update to not null 那么就表示, 这个 update 是一个 soft delete 操作.

然后跟着 update foreign table 的 DateDeleted 就可以了.

另外, 与 hard delete 不同的是, soft delete 需要被 restore, 如果要实现 cascade delete, 也需要一起实现 cascade restore 才行.

通过判断 principal DateDeleted 从 not null update to null, 可以知道这个 update 是一个 restore 操作.

这里需要注意, 不能单纯的 restore 所有 foreign row 哦, 要考虑到, 可能 foreign row 本来就已经被删除, 而不是被 cascade delete 的.

可以通过时间判断, 如果是相同时间, 那么就是 cascade delete 的. 那么就需要 restore.

GO
CREATE OR ALTER TRIGGER [TR_Contract_AfterUpdate_ForCascadeSoftDelete_Trade] ON [Contract]
AFTER UPDATE
AS
IF (ROWCOUNT_BIG() = 0) RETURN;
SET NOCOUNT ON; UPDATE [Trade]
SET [DeletedBy] =
CASE
-- Delete
WHEN deleted.[DateDeleted] IS NULL AND inserted.[DateDeleted] IS NOT NULL
THEN
CASE
WHEN [Trade].[DateDeleted] IS NOT NULL THEN [Trade].[DeletedBy]
ELSE inserted.[DeletedBy]
END
-- Restore
ELSE
CASE
-- DeletedBy also need to be same
WHEN [Trade].[DateDeleted] = deleted.[DateDeleted] AND [Trade].[DeletedBy] = deleted.[DeletedBy] THEN NULL
ELSE [Trade].[DeletedBy]
END
END,
DateDeleted =
CASE
WHEN deleted.[DateDeleted] IS NULL AND inserted.[DateDeleted] IS NOT NULL
THEN
CASE
WHEN [Trade].[DateDeleted] IS NOT NULL THEN [Trade].[DateDeleted]
ELSE inserted.[DateDeleted]
END
ELSE
CASE
WHEN [Trade].[DateDeleted] = deleted.[DateDeleted] AND [Trade].[DeletedBy] = deleted.[DeletedBy] THEN NULL
ELSE [Trade].[DateDeleted]
END
END
FROM deleted
INNER JOIN inserted
ON deleted.[ContractId] = inserted.[ContractId]
INNER JOIN [Trade] ON inserted.[ContractId] = [Trade].[ContractId]
WHERE (
(deleted.[DateDeleted] <> inserted.[DateDeleted]) or (deleted.[DateDeleted] is null or inserted.[DateDeleted] is null)
)
AND (deleted.[DateDeleted] is not null or inserted.[DateDeleted] is not null)
GO

如果有多个 foreign table, 那么就写多个 trigger.

Step 7: Restrict Delete

Restrict delete 就是当有 foreign 的时候不允许删除 principal. 这个也是 build-in 功能, soft delete 就没了.

同样可以用 trigger 来做. 监听 principal 的 update, 发现是 delete 操作, 先查看是否有相关的 foreign row. 有的话就报错.

GO
CREATE OR ALTER TRIGGER [TR_Trade_AfterUpdate_ForRestrictSoftDelete_TradeItem] ON [Trade]
AFTER UPDATE
AS
IF (ROWCOUNT_BIG() = 0) RETURN;
SET NOCOUNT ON;
-- Check have non-deleted children
IF EXISTS (
-- 这里不需要锁表, 是因为 foreign insert/update 会锁
SELECT 1 FROM deleted INNER JOIN inserted ON deleted.[TradeId] = inserted.[TradeId]
INNER JOIN [TradeItem] ON inserted.[TradeId] = [TradeItem].[TradeId]
WHERE (
(deleted.[DateDeleted] <> inserted.[DateDeleted]) or (deleted.[DateDeleted] is null or inserted.[DateDeleted] is null)
)
AND (deleted.[DateDeleted] is not null or inserted.[DateDeleted] is not null)
AND [TradeItem].[DateDeleted] IS NULL
)
BEGIN
;THROW 50001, 'Restrict Soft Delete', 0;
END
GO

如果有多个 foreign table, 那么就写多个 trigger

Step 8: Foreign constraint

Foreign constraint 是站在 foreign 的角度, 当 insert/update 时, 需要确保 foreign key relate to principal row 必须存在于 database.

这也是一个 build-in 功能, 由于 soft delete 并不会真的删除数据, 意味着 build-in 的 foreign constraint 是不需要的, 可以关掉它, 节约性能.

但是 foreign constraint 这个概念可不能关掉哦, soft delete 依然需要有这个机制, 我们得自己实现一个.

当 foreign insert/update 时, 通过 trigger 去检查确保 principal row 不是 deleted 状态, 如果是 deleted 就报错.

注: 这过程还需要提升隔离等级哦, 需要 repeatable read (防止并发问题)

-- Foreign constraint check when foreign insert
GO
CREATE OR ALTER TRIGGER [TR_TradeItem_AfterUpdate_ForRestrictSoftDelete_TradeItem] ON [TradeItem]
AFTER UPDATE
AS
IF (ROWCOUNT_BIG() = 0) RETURN;
SET NOCOUNT ON; -- Check current isolation level and keep it, if later set then need to reset back.
DECLARE @currentIsolationLevel nvarchar(64); SELECT @currentIsolationLevel =
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'READ UNCOMMITTED'
WHEN 2 THEN 'READ COMMITTED'
WHEN 3 THEN 'REPEATABLE READ'
WHEN 4 THEN 'SERIALIZABLE'
WHEN 5 THEN 'SNAPSHOT'
END
FROM sys.dm_exec_sessions
WHERE session_id = @@SPID; DECLARE @isolationChanged bit = 0; -- record whether change isolation
IF(@currentIsolationLevel <> 'REPEATABLE READ' AND @currentIsolationLevel <> 'SERIALIZABLE')
BEGIN
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET @isolationChanged = 1;
END -- 1 foreign key 1 @count, so can be more than 1
DECLARE @count int; SELECT @count = SUM(CASE WHEN [Trade].[DateDeleted] IS NOT NULL THEN 1 ELSE 0 END)
FROM deleted INNER JOIN inserted ON deleted.[TradeItemId] = inserted.[TradeItemId]
LEFT JOIN [Trade] ON inserted.[TradeId] = [Trade].[TradeId]
WHERE (
(deleted.[TradeId] <> inserted.[TradeId]) or (deleted.[TradeId] is null or inserted.[TradeId] is null)
)
AND (deleted.[TradeId] is not null or inserted.[TradeId] is not null); IF(@isolationChanged = 1) -- reset back isolation level
BEGIN
EXEC('SET TRANSACTION ISOLATION LEVEL ' + @currentIsolationLevel);
END IF(@count > 0)
BEGIN
;THROW 50001, 'Restrict Soft Delete', 0;
END
GO -- Foreign constraint check when foreign update foreign key
GO
CREATE OR ALTER TRIGGER [TR_TradeItem_AfterInsert_ForRestrictSoftDelete_TradeItem] ON [TradeItem]
AFTER INSERT
AS
IF (ROWCOUNT_BIG() = 0) RETURN;
SET NOCOUNT ON; DECLARE @currentIsolationLevel nvarchar(64);
SELECT @currentIsolationLevel =
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'READ UNCOMMITTED'
WHEN 2 THEN 'READ COMMITTED'
WHEN 3 THEN 'REPEATABLE READ'
WHEN 4 THEN 'SERIALIZABLE'
WHEN 5 THEN 'SNAPSHOT'
END
FROM sys.dm_exec_sessions
WHERE session_id = @@SPID; DECLARE @isolationChanged bit = 0;
IF(@currentIsolationLevel <> 'REPEATABLE READ' AND @currentIsolationLevel <> 'SERIALIZABLE')
BEGIN
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET @isolationChanged = 1;
END DECLARE @count int; SELECT @count = SUM(CASE WHEN [Trade].[DateDeleted] IS NOT NULL THEN 1 ELSE 0 END) FROM inserted
LEFT JOIN [Trade] ON inserted.[TradeId] = [Trade].[TradeId]; IF(@isolationChanged = 1)
BEGIN
EXEC('SET TRANSACTION ISOLATION LEVEL ' + @currentIsolationLevel);
END IF(@count > 0)
BEGIN
;THROW 50001, 'Restrict Soft Delete', 0;
END
GO

Step 9: Table Structure

当某个 column 从 nullable 变成 not nullable 的时候, soft deleted 的 row 就会成为麻烦.

因为需要设置 default value 给它们. 另外如果某个 column 要删除掉, 也意味着 soft deleted 的资料也需要被删除掉.

所以你的历史记录并不能完全的被保存起来. 这个基本上是无解的.

We really need soft delete?

从上面的 Step 可以看出来要实现一个完整的 soft delete 代价还是挺大的, 不管是开发, 维护, 性能等等.

所以让我们回到需求的本质.

需求 1, 我们希望数据永远不要真的 delete 掉, 因为你不知道会不会有那么一天, 你突然后悔了.

需求 2, 还原数据的速度. 如果满足了第一个需求, 那么可能进一步希望能快速还原.

一般上第 1 个需求会比第 2 个重要很多.

从这 2 个点看的话, 上面的 Soft Delete 对第 2 个需求满足的很好, 但是对第 1 个需求就不那么理想了.

所以 Soft Delete 并不是一个很划算的方案.

Soft Delete Alternative

Temporal Table

要完整的保留所有数据. 那么自然不能放过 update 的数据. 不只是 delete 可以销毁数据, update 也可以丫.

SQL Server 的 Temporal Table 也是一个为了满足上面 2 个需求而诞生的.

在数据保护上, 它比 soft delete 要强, 在恢复数据上, 它弱于 soft delete 一些, 在开发和维护上它是 build-in solution 更好一些. 性能都差不多.

但是它算是一个比较重的方案. 所以如果没有充分利用到它最大的特性 (time travel) 我认为依然不算是合适的方案.

Archive Table or Data Log Table

这个方案基本上是放弃了快速还原的需求, 把重心放到数据保护上. 它比 soft delete 简单很多.

做一个表, 做 trigger 监听每个表的 delete/update, 把 deleted 的 row to json 然后存入 archive table 中.

与 soft delete 和 temporal table 相比, 把所有 history 都放入一个表中的好处就是不需要担心 table structure change.

当然坏处可能就是需要 query by json, 性能可能慢一些.

总体来看这个方案依然是比较合适的.

总结

不要真的删除数据, 这个是对的, 如何实现这个需求则有许多地方需要考虑.

soft delete 的简单是站在前面 3 个 Step 说的, 越往后问题越多.

我观察 Microsoft Azure, Google Cloud 它们在实现 Soft Delete 的时候是很有控制的.

比如,

unique 依然存在, 你需要另外取名字, 或者关闭 soft delete 去 hard delete 才可以用回同一个名字

soft delete 只保留 30-90 days, 这样就不会因为数据太多而影响性能, 也不会因为 table structure change 而一直被影响着 (有个周期, 最多也是一个时间内比较混乱)

只有少数 table 有 soft delete, 只有真的需要 delete 和快速 restore 的地方, 它们才会考虑使用 soft delete.

所以我个人的建议是, 要用 soft delete 就要清楚它的利与弊, 要避短扬长. 千万不要认为世界上所有问题都有银弹. 想一招打天下. 越往后你会越痛苦的.

SQL Server – Soft Delete的更多相关文章

  1. SQL Server中DELETE和TRUNCATE的区别

    ​DELETE和TRUNCATE语句之间的区别是求职面试中最常见的问题之一.这两条语句都可以从表中删除数据.然而,也有不同之处. 本文将重点讨论这些差异,并通过实例加以说明. TRUNCATE DEL ...

  2. SQL Server中Delete语句表名不能用别名

    delete from TABLEA A where A.FIELD1=10        (ORACLE适用)delete TABLEA from TABLEA A where A.FIELD1=1 ...

  3. 追踪SQL Server执行delete操作时候不同锁申请与释放的过程

    一直以为很了解sqlserver的加锁过程,在分析一些特殊情况下的死锁之后,尤其是并发单表操作发生的死锁,对于加解锁的过程,有了一些重新的认识,之前的知识还是有一些盲区在里面的.delete加锁与解锁 ...

  4. SQL Server 后悔药 delete drop update

    国庆假期终于有时间做点事情 因为平常工作会做些数据库操作 可能会有所操作失误  参考一下 方法一 ApexSql 2016一个软件 http://www.cnblogs.com/gsyifan/p/A ...

  5. sql server 测试delete后数据空间情况

    总结结论: [1]如果是索引组织表,删除的数据空间是会被文件设置为可用状态,其他表都可以使用. [2]如果是堆表,删除数据空间也会设置为可用状态,但是只能给被删除数据的表使用. [3]truncate ...

  6. SQL Server温故系列(1):SQL 数据操作 CRUD 之增删改合

    1.插入语句 INSERT INTO 1.1.用 INSERT 插入单行数据 1.2.用 INSERT 插入多行数据 1.3.用 INSERT 插入子查询结果行 1.4.INSERT 小结及特殊字段插 ...

  7. SQL SERVER 2005删除维护作业报错:The DELETE statement conflicted with the REFERENCE constraint "FK_subplan_job_id"

    案例环境: 数据库版本: Microsoft SQL Server 2005 (Microsoft SQL Server 2005 - 9.00.5000.00 (X64) ) 案例介绍: 对一个数据 ...

  8. SQL Server DML(UPDATE、INSERT、DELETE)常见用法(一)

    1.引言 T-SQL(Transact Structured Query Language)是标准的SQL的扩展,是程序和SQL Server沟通的主要语言. T-SQL语言主要由以下几部分组成: 数 ...

  9. SQL Server中UPDATE和DELETE语句结合INNER/LEFT/RIGHT/FULL JOIN的用法

    在SQL Server中,UPDATE和DELETE语句是可以结合INNER/LEFT/RIGHT/FULL JOIN来使用的. 我们首先在数据库中新建两张表: [T_A] CREATE TABLE ...

  10. SQL Server INSET/UPDATE/DELETE的执行计划

    DML操作符包括增删改查等操作方式. insert into Person.Address (AddressLine1, AddressLine2, City, StateProvinceID, Po ...

随机推荐

  1. WebGL加载本地模型

    前言 大部分的webgl框架,比如threejs和babylon等,都可以加载obj和gltf模型. 我们的引擎,基于three封装,同样有加载模型的loader,因此加载obj和gltf模型也是很简 ...

  2. 基于 Vagrant 手动部署多个 Redis Server

    环境准备 宿主机环境:Windows 10 虚拟机环境:Vagrant + VirtualBox Vagrantfile 配置 首先,我们需要编写一个 Vagrantfile 来定义我们的虚拟机配置. ...

  3. 如何在 XAMPP 中使用 不同的 PHP 版本?

    你有没有碰到这种情况,你工作的项目需要的是PHP8,而你自己的项目需要的是PHP7,而你又特别钟爱于XAMPP,奈何它却不能自由切换PHP版本,下面就讲下本人在用的方法将PHP7更新到PHP8,可以通 ...

  4. JavaScript小面试~~JavaScript实现图片懒加载,多方式解决加载过多问题

    图片懒加载,就是滚动页面时,图片未出现在可视局域时不加载图片,只有图片出现在可视区域才加载. 思路:通过上面一段话,实现图片懒加载需要知道: 绑定滚动事件 可视窗口高度(VH) 图片元素距离可视局域顶 ...

  5. PHP转Go系列 | 推荐一个强大的Go语言工具函数库

    大家好,我是码农先森. 从 PHP 转到 Go 的朋友,常常会因为没有便捷的工具函数而感到苦恼.PHP 写的多了就会形成路径依赖,在写 Go 的时候时不时就会想到 PHP 强大的数组函数.当然写 Go ...

  6. java一些位运算的方法

    记录一些java位运算的方法 Integer.bitCount:用于记录一个int输转化成二进制之后里面包含了多少个1. 使用例题为leetcode2859 切记下面规则要先转二进制 &相同位 ...

  7. Jmeter函数助手37-setProperty

    setProperty函数用于修改jmeter属性值. 属性名称:填入需要修改的属性名 Value of property:填入需要修改的属性值 Return Original Value of pr ...

  8. 【Java】 WebService 校验机制

    测试环境域名 不可见 正式环境域名 不可见 1.2.安全校验凭证 accessId(授权ID) 测试/正式待定 securityKey(加密密钥) 测试/正式待定 1.3.安全校验机制 1.3.1.在 ...

  9. 【DataBase】XueSQL Training

    地址: http://xuesql.cn/ Lesson0 -- 认识SQL -- [初体验]这是第一题,请你先将左侧的输入框里的内容清空,然后请输入下面的SQL,您将看到所有电影标题: SELECT ...

  10. 【Vue】Re01 理论概念和入门上手

    一.Vue概述 什么是渐进式?1.把Vue作应用的一部分嵌套项目中2.如果完全抛弃其他组件和框架,Vue又具有丰富的生态和库莱支持3.Core + Router + VueX 满足项目绝大多数的需求- ...