处理已删除的用户-单独还是相同的表?


19

场景是我的用户数量在不断扩大,随着时间的流逝,用户将取消他们的帐户,这些帐户目前在同一表中被我们标记为“已删除”(带有标记)。

如果具有相同电子邮件地址的用户(这就是用户登录的方式)希望创建一个新帐户,则可以再次注册,但是会创建一个新帐户。(我们为每个帐户提供唯一的ID,因此可以在实时和已删除的电子邮件地址之间复制电子邮件地址)。

我注意到的是,在整个系统中,正常情况下,我们会不断查询user表,以检查用户是否被删除,而我在想的是,我们根本不需要这样做。 ![澄清1:通过'不断查询',我的意思是我们有这样的查询:'... FROM users WHERE isdeleted =“ 0” AND ...'。例如,我们可能需要提取特定日期所有会议的所有注册用户,因此在该查询中,我们具有FROM用户WHERE isdeleted =“ 0”-这使我的观点更清楚了吗?]

(1) continue keeping deleted users in the 'main' users table
(2) keep deleted users in a separate table (mostly required for historical
    book-keeping)

两种方法的优缺点是什么?


您出于什么原因保留用户?
keppla 2011年

2
这称为软删除。另请参见删除数据库记录unpermenantley(软删除)
Sjoerd

@keppla-他提到:“历史记录”。
克里斯·

@ChrisF:我对范围感兴趣:他是否只想保留用户的书,还是附加了一些数据(例如,评论,付款等)
keppla 2011年

这可能有助于停止将其视为已删除(这是不正确的),并开始将其帐户视为已取消(这正确的)。
Mike Sherrill'Cat Recall'11

Answers:


13

(1)继续将已删除的用户保留在“主要”用户表中

  • 优点:在所有情况下查询都更简单
  • 缺点:如果用户数量很多,则随着时间的流逝,性能可能会下降

(2)将已删除的用户保留在单独的表中(大多数用于历史记录)

您可以使用例如触发器将删除的用户自动移动到历史记录表。

  • 优点:对活动用户表的维护更简单,性能稳定
  • 缺点:需要对历史表进行不同的查询;但是,由于大多数应用程序都不应该对此感兴趣,因此这种负面影响可能有限

11
分区表(位于IsDeleted上)将消除使用单个表的性能问题。
伊恩

1
@Ian除非每个查询都提供IsDeleted作为查询条件(这似乎不是原始问题),否则分区甚至可能导致性能下降。
阿德里安·舒姆

1
@Adrian,我假设最常见的查询将在登录时进行,并且只有删除的用户都不能登录。–
Ian

1
如果isdeleted成为性能问题,并且您希望使用单个表,请对它使用索引视图。
JeffO 2011年

10

我强烈建议使用同一张表。主要原因是数据完整性。很可能将有许多表,它们之间的关系取决于用户。删除用户后,您不想将这些记录孤立。
具有孤立记录既使执行约束更加困难,又使查找历史信息更加困难。如果您希望用户恢复所有旧记录,则当用户提供使用的电子邮件时要考虑的另一种行为。通过使用软删除,这将自动工作。就编码而言,例如在我当前的c#linq应用程序中,其中delete = 0子句会自动附加到所有查询的末尾


7

“我注意到的是,在整个系统中,在正常情况下,我们会不断查询user表,以检查用户是否被删除”

这给了我不好的设计气味。您应该隐藏这种逻辑。例如,您应该UserService提供一种isValidUser(userId)“在整个系统中”使用的方法,而不是像这样:

“获取用户记录,检查用户是否标记为已删除”。

您存储已删除用户的方式不应影响业务逻辑。

通过这种封装,上述参数将不再影响您的持久性方法。然后,您可以更多地关注与持久性本身有关的利弊。

要考虑的事项包括:

  • 删除的记录应实际清除多长时间?
  • 删除记录的比例是多少?
  • 如果您实际上将引用从表中删除,引用完整性是否会存在问题(例如,从其他表中引用了用户)?
  • 您是否考虑重新打开用户?

通常我会采取一种综合的方式:

  1. 将记录标记为已删除(以使其符合功能要求,例如重新打开交流电或检查最近关闭的交流电)。
  2. 在预定义的时间段之后,将已删除的记录移至存档表(用于簿记)。
  3. 在预定的存档期后清除它。

1
[澄清1:通过'不断查询',我的意思是我们有这样的查询:'... FROM users WHERE isdeleted =“ 0” AND ...'。例如,我们可能需要提取特定日期所有会议的所有注册用户,因此在该查询中,我们拥有FROM用户WHERE isdeleted =“ 0”-这使我的观点更清楚了吗?] @Adrian
Alan Beats

是的,清晰得多。:)如果这样做,我宁愿将其作为用户状态更改,而不是将其视为物理/逻辑删除。尽管代码量不会减少(“ and isDeleted ='0'”与'和“ state <>'TERMINATED'”),但所有内容看起来都更加合理,并且具有不同的用户状态也是正常的。也可以执行定期清除TERMINATED用户的操作,正如我先前的回答所建议的那样)
Adrian Shum

5

为了正确回答这个问题,您首先需要确定:在此系统/应用程序的上下文中,“删除”是什么意思?

要回答这个问题,您需要回答另一个问题:为什么删除记录?

有许多充分的理由说明用户可能需要删除数据的原因。通常,我发现(每张表)有一个原因可能导致需要删除。一些例子是:

  • 回收磁盘空间;
  • 根据保留/隐私权政策要求硬删除;
  • 损坏/绝望的错误数据,比修复更容易删除和重新生成。
  • 大多数行被删除,例如日志表仅限于X个记录/天。

硬删除还有一些很差的理由(稍后会详细说明):

  • 更正小错误。这通常强调了开发人员的懒惰和敌意的UI。
  • 为了“避免”交易(例如,本不应该开票的发票)。
  • 因为可以

您为什么问,这真的有什么大不了的?好孩子DELETE怎么了?

  • 在任何与金钱紧密联系的系统中,硬删除都违反了各种会计期望,即使移至档案/墓碑表也是如此。解决此问题的正确方法是追溯事件
  • 存档表倾向于偏离实时模式。如果您忘记了一个新添加的列或级联,那么您将永久丢失该数据。
  • 硬删除可能是非常昂贵的操作,尤其是对于级联。许多人没有意识到级联不止一个级别(或者在某些情况下,任何级联,取决于DBMS)将导致记录级操作,而不是设置操作。
  • 重复,频繁的硬删除可加快索引碎片的过程。

所以,软删除更好,对吗?不,不是真的:

  • 设置级联变得非常困难。您几乎总是以客户机显示为孤立行的结果结束。
  • 您只能跟踪一个删除。如果该行被多次删除和取消删除怎么办?
  • 尽管可以通过分区,视图和/或过滤后的索引在某种程度上减轻读取性能,但读取性能会受到影响。
  • 如前所述,在某些情况/管辖范围内,它实际上可能是非法的。

事实是这两种方法都是错误的。删除是错误的。 如果您实际上是在问这个问题,则意味着您正在建模当前状态而不是事务。在数据库领域,这是一种不好的做法。

乌迪·达汉(Udi Dahan)在“ 不要删除-就是不要”中写道。有总是某种任务,交易,活动,或(我的首选术语)事件实际上代表了“删除”。这是确定的,如果你随后又希望将非规范化的表现“当前状态”表,但做到这一点后,你已经确定下来的交易模式,而不是之前。

在这种情况下,您有“用户”。用户本质上是客户。客户与您有业务关系。这种关系不会简单地消失在空中,因为他们取消了帐户。真正发生的是:

  • 客户创建帐户
  • 客户取消帐户
  • 客户续订帐户
  • 客户取消帐户
  • ...

在每种情况下,都是一位客户,并且可能是同一帐户(即,每个帐户续订都是一项新的服务协议)。那为什么要删除行呢?这很容易建模:

+-----------+       +-------------+       +-----------------+
| Account   | --->* | Agreement   | --->* | AgreementStatus |
+-----------+       +-------------+       +----------------+
| Id        |       | Id          |       | AgreementId     |
| Name      |       | AccountId   |       | EffectiveDate   |
| Email     |       | ...         |       | StatusCode      |
+-----------+       +-------------+       +-----------------+

而已。这里的所有都是它的。您无需删除任何内容。上面是一个相当普通的设计,具有很高的灵活性,但是您可以稍微简化一下;您可能会决定不需要“协议”级别,而只需将“帐户”转到“ AccountStatus”表即可。

如果您的应用程序中经常需要获取有效协议/帐户的列表,那么这是一个(稍微)棘手的查询,但这就是视图的用途:

CREATE VIEW ActiveAgreements AS
SELECT agg.Id, agg.AccountId, acc.Name, acc.Email, s.EffectiveDate, ...
FROM AgreementStatus s
INNER JOIN Agreement agg
    ON agg.Id = s.AgreementId
INNER JOIN Account acc
    ON acc.Id = agg.AccountId
WHERE s.StatusCode = 'ACTIVE'
AND NOT EXISTS
(
    SELECT 1
    FROM AgreementStatus so
    WHERE so.AgreementId = s.AgreementId
    AND so.EffectiveDate > s.EffectiveDate
)

这样就完成了。现在,您拥有了软删除的所有优点,但没有缺点:

  • 孤立记录是非发行记录,因为所有记录始终可见。您仅在必要时从其他视图中进行选择。
  • “删除”通常是一种非常便宜的操作-只需在事件表中插入一行即可。
  • 无论您多么糟糕,永远不会失去任何历史。
  • 如果需要(例如出于隐私原因),您仍然可以硬删除帐户,并且对删除将干净进行且不会干扰应用程序/数据库的任何其他部分的知识感到满意。

剩下要解决的唯一问题是性能问题。在许多情况下,由于聚集索引的AgreementStatus (AgreementId, EffectiveDate)存在,实际上实际上是没有问题的-很少有I / O寻求进行。但是,如果有问题,可以使用触发器,索引视图/物化视图,应用程序级事件等方法来解决。

不过,不要太早担心性能-正确进行设计更为重要,在这种情况下,“正确”意味着以一种将数据库用作事务处理系统的方式使用数据库。


1

我目前正在使用一个系统,其中每个表都有一个用于软删除的Deleted标志。 这是一切存在的祸根。 当用户可以从一个表中“删除”一条记录,而返回到该表的FK的子记录没有被级联软删除时,这完全破坏了关系完整性。随着时间的流逝,真正产生垃圾数据。

因此,我建议使用单独的历史记录表。


当然,如果没有级联的历史转移,您是否会遇到完全相同的问题?
glenatron 2011年

不在活动记录表中,否。
Jesse C. Slicer

那么,在将用户委托给历史记录表之后,从用户表中删除FK的子记录会怎样?
glenatron 2011年

您的触发器(或业务逻辑)也会将子记录分配到各自的历史记录表中。关键是,如果数据库不告诉您破坏了RI,则无法物理删除父记录(用于移至历史记录)。因此,您不得不对其进行设计。Deleted标志不会强制级联软删除。
Jesse C. Slicer

3
取决于软删除的真正含义。如果这只是停用它们的一种方式,则无需调整与已停用帐户相关的记录。似乎对我来说只是数据。是的,我也必须在未设计的系统中处理它。并不意味着您必须喜欢它。
JeffO 2011年

1

将表一分为二将是可想而知的。

这是我建议的两个非常简单的步骤:

  1. 将“用户”表重命名为“所有用户”。
  2. 创建一个名为“用户”的视图,作为“从所有用户中选择*,其中delete = false”。

PS对不起,延迟了几个月的回答!


0

如果当有人使用相同的电子邮件地址回来时您一直在恢复已删除的帐户,那么我将所有用户都放在同一张表中。这将使帐户恢复过程变得微不足道。

但是,在创建新帐户时,将删除的帐户移到单独的表中可能会更简单。实时系统不需要此信息,因此请不要公开它。如您所说,它使查询在大型数据集上更简单,甚至可能更快。更简单的代码也更易于维护。


0

您没有提及正在使用的DBMS。如果您拥有具有适当许可证的Oracle,则可以考虑将users表划分为两个分区:活动用户和已删除用户。


然后,在删除用户时,必须将行从一个分区移动到另一个分区,这绝对不是打算使用分区的方式。
彼得Török

@Péter:嗯?您可以根据需要的任何条件进行分区,包括删除标志。
2011年

@Aaronaught,好,我说错了。DBMS可以为您完成工作,但这仍然是额外的工作(因为必须将行从一个位置物理移动到另一个位置,可能移动到另一个文件),并且这可能会降低数据的物理分布。
彼得Török
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.