【问题标题】:Database: Insert new rows or update existing ones?数据库:插入新行还是更新现有行?
【发布时间】:2011-02-12 22:12:32
【问题描述】:

面向对象的设计鼓励使用不可变对象来提高线程安全性和性能。我想知道这是否会延续到关系数据库。

我是更新现有行还是插入新行作为替代行更好?

  • 用例
    • 每位员工仅与一家公司相关联
    • 随着时间的推移,员工会更换公司。
    • 员工姓名应该是唯一的。
  • 架构
    • 员工[姓名,公司]

选项 1:每次员工更换公司时,插入一个新的 Employee[name, company] 行。应用程序被指示跳过较旧的行(随着时间的推移在后台线程中被修剪)。 选项 2:每次员工更换公司时,更新现有行。

选项 1 让我想起了不可变对象,因为它是线程安全的(不需要锁)。另一方面,每次员工更换公司时,我都必须克隆所有关联对象并将它们指向新记录。此外,目前尚不清楚如何防止错误创建重复的Employees。

选项 2 可以轻松防止重复员工,但缺点是在 READ_COMMITTED 事务隔离中返回可能不一致的关联。

【问题讨论】:

    标签: database-design


    【解决方案1】:

    我发布此内容是希望这对将来的其他人有所帮助。我个人在这条(错误的)道路上浪费了无数天。

    不可变对象适用于值类型(想想整数、时间戳、温度读数等)。它们是永远不会改变的类型。当你开始谈论修改不可变对象的值时,这强烈表明你走错了路。当您使用真正的不可变对象时,您永远不必更新关联对象的引用。

    因此,无论是面向对象编程还是数据库设计,正确的答案是就地更新可变对象。

    更新:marc_s 提到了一些系统需要不可变的审计跟踪这一事实。我建议将系统一分为二。主表在将副本插入单独的审计表时就地更新数据。这有两个好处:

    1. 主表可以利用完整性检查(即“员工姓名必须是唯一的”)。
    2. 主表的读取速度非常快,随着时间的推移,可以修剪更大/更慢的审计表。

    这让您享受两全其美。

    【讨论】:

    • 对于适当小的“不可变”值,修剪不可变审计跟踪很容易。但说真的,将审计数据存储在不同的表中很好。审计表中的数据与原始表中的实体无关;这是关于行的。
    【解决方案2】:

    那些不是选项。它们是完全不同的东西,它们需要完全不同的表。痛苦的部分是表格中的数据可能看起来完全相同。以下是区分它们的方法。

    关系数据库中的每个表都有一个且只有一个谓词。谓词确定表中行的含义。所以一个数据看起来像这样的表

    Name    Company
    --
    Gili    Microsoft
    Marc    Oracle
    

    可能意味着

    Person named "Gili" is currently an employee of company "Microsoft".
    Person named "Marc" is currently an employee of company "Oracle".
    

    这样的表格会排除顾问,因为他们不是员工。 (无论如何,在美国他们不是。)

    但这可能意味着

    Person named "Gili" once was an employee of company "Microsoft".
    Person named "Marc" once was an employee of company "Oracle".
    

    而且该表也允许

    Person named "Gili" once was an employee of company "Oracle".
    

    不同的谓词,不同的表。 (这意味着你必须以不同的方式构造表来捕捉谓词的含义。)

    你不能拥有的是一张表,意味着

    Person named "Gili" is currently an employee of company "Microsoft".
    Person named "Marc" once was an employee of company "Oracle".
    

    一张表中有两个不同的谓词。在关系系统中做不到。

    所以,如果你的谓词归结为这个

    Person named NAME is currently an employee of company COMPANY.
    

    那么您必须在此人更换雇主时更新 COMPANY。如果你的谓词归结为这个

    Person named NAME once was an employee of company COMPANY.
    

    那么您必须在此人更换雇主时插入一个新行。

    【讨论】:

    • 我不同意。如果指示应用程序代码仅使用最后一条记录(旧记录随着时间​​的推移而被修剪),则选项 1 可用于对员工的当前公司进行建模。它可能不是世界上最直观的东西,但它在技术上没有任何问题。
    • @Gili:不,它不能。为实现用例约束而构建的表不允许您为每个人插入多于一行。 (这将违反谓词。)如果没有“last”有意义的列,SQL 和关系表都不支持“最后一行”的任何概念。用例显然没有这样的列。这不是指示“该”应用程序代码做某事的情况;这是一个记住编写每个应用程序的代码来做完全相同的事情的例子——众所周知,这种开发实践往往失败多于成功。
    • 我的错。我的意思是说,如果您有 Employee[id, name, company],您可以为同一个员工姓名插入多个条目,并让应用程序只使用最新的条目。 “id”被定义为一个自动递增的列,它将标识“最新”行。不过你说对了一件事:可能有多个应用程序访问同一个数据库。
    • @Gili:如果你这样做了,你会如何处理来自其他表的外键?外键将指向“旧”ID。数据库设计人员永远不会同意您提出的建议是有原因的。这不是因为我们变得困难或愚蠢。
    • @Catcall,这就是为什么我的问题指定您必须克隆所有关联对象(并将新外键指向新记录)。我从来没有暗示选项 1 很棒,但它有它的用途。您如何看待我在stackoverflow.com/questions/4980963/… 发布的答案?
    【解决方案3】:

    一般来说,数据仓库倾向于遵循“仅插入”模式。原因是维度表中的过时行仍然需要将旧事实置于新事实时存在的上下文中。

    示例:宾夕法尼亚州在 1 月 1 日之前是东北销售区的一部分,当时它成为中大西洋销售区的一部分。去年 12 月进行的销售需要回溯地理维度表中将其置于东北地区的行。代替“状态”维度表的更新将使这个旧事实无效。

    OLTP 数据库的趋势是就地执行更新,并且只跟踪当前情况。但是,这可能会导致将一些数据复制到事务行中。例如,采购订单系统中的发票明细行可能包含从产品表中的行复制而来的订购项目的价格。这样,如果产品表中的价格得到更新,影响此发票的价格不会被破坏。

    【讨论】:

      【解决方案4】:

      基本区别是这样的:

      • 如果您为每个更改插入一个新行,例如通过设置“ValidTo”日期“停用”旧行,然后您就可以了解随着时间推移发生的变化 - 您将进入“临时”数据库区域

      • 如果您一遍又一遍地更新同一行,那么您始终拥有当前状态 - 但没有历史记录。

      所以我想,这确实是个大问题:您是否需要时间信息,例如能够“回到过去”,现在三个月前您的数据状态如何?如果是这样,选项 1(包括“软删除” - 仅将行标记为已删除,实际上并不删除它们)是您唯一的选择。缺点显然是增加了复杂性和更多的存储需求。

      【讨论】:

      • 这很好。可审计的系统需要不可变的历史。话虽如此,您不能将系统分为实时数据和历史数据吗?实时数据将就地更新(简化完整性检查)并将副本插入到审计表中。
      • 如果您经常使用时间信息并及时返回,您可能需要考虑一种新的文档数据库(来自 NoSQL 世界)。其中一些通过针对该用例优化其数据存储来明确支持鼓励仅插入设计。这使得插入大大的速度更快,并且可以改进报告。常见的关系数据库更擅长更改现有数据,并且在处理大型数据集时变得越来越慢(当该数据集仅包含死/过时的数据时,这几乎是不需要的)。
      • 我想我只是重复了你的回答 marc_s。无意抄袭。
      【解决方案5】:
      tblPerson
          PersonID 
          LastName
          FirstName
      
      tblCompany
          CompanyID
          CompanyName
      
      tblCompany_Employee
          PersonID
          CompanyID
          StartDate
          EndDate
      

      永远不会从 tblCompany_Employee 中删除行 - 在雇用人员时插入记录,并带有开始日期。在雇佣结束时,EndDate 从 NULL 更新到雇佣结束的日期。

      要查找特定公司的当前员工,请从 tblCompanyEmployee WHERE EndDate IS Null 中选择 PersonID。

      要查找特定公司的特定人员的就业状况:

      SELECT PersonID
      FROM tblCompany_Employee
      WHERE PersonID = @PersonID
      AND CompanyID = @CompanyID
      AND EndDate IS Null
      

      注意:以上内容可能应该包含在一个函数中,该函数只返回一个有效 EmployeeID 的 true - 如果该人从未在相关公司工作 该语句将不返回任何记录,因此是错误的。

      在这种情况下,维护了一个审计跟踪,并且有可能(显然,经过一些额外的改进 - 我在这里很粗略地说明了这一点)来确定:

      A.一个人在所有公司的工作经历 B. 受雇于特定公司的所有人员 C. 目前受雇于某家公司 D. 目前未受雇于某家公司的人员 D.等

      不会因为 UPDATES 覆盖历史记录而丢失数据。

      【讨论】:

        猜你喜欢
        • 2021-01-05
        • 1970-01-01
        • 1970-01-01
        • 2011-04-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多