【问题标题】:UNIQUE constraint vs checking before INSERTUNIQUE 约束与 INSERT 之前的检查
【发布时间】:2014-03-20 08:03:27
【问题描述】:

我有一个 SQL 服务器表 RealEstate,其中包含 Id、Property、Property_Value 列。该表大约有 5-1000 万行,将来可能会增加更多。仅当此表中不存在 Id、Property、Property_Value 的组合时,我才想插入一行。

示例表 -

1,Rooms,5
1,Bath,2
1,Address,New York
2,Rooms,2
2,Bath,1
2,Address,Miami

不应允许插入2,Address,Miami。但是,2,Price,2billion 没问题。我很想知道执行此操作的“最佳”方式以及为什么。为什么部分对我来说是最重要的。两种检查方式是 -

  1. 在应用程序级别 - 应用程序应在插入行之前检查行是否存在。
  2. 在数据库级别 - 在所有 3 列上设置唯一约束并让数据库 进行检查而不是人员/应用程序。

有没有一种情况会比另一种更好?

谢谢。

PS:我知道已经有一个类似的问题,但它没有回答我的问题 - Unique constraint vs pre checking 另外,我认为UNIQUE适用于所有数据库,所以我认为我不应该删除mysql和oracle标签。

【问题讨论】:

  • 总是选择选项 #2。
  • @wdosanjos - 请告诉我原因。
  • 第二种方法更好,因为它是保证工作的唯一一种,如果您使用插入前检查方法,则有可能出现race condition。无论检查和插入之间有多小,都会有一个时间间隔,在这个间隙中,记录可能已被另一个线程插入。我认为避免这种情况的唯一方法是使用MERGEHOLDLOCK(特定于sql server)。即使那样,也没有理由不受到约束。两者都可以吗?
  • 没有太多要说的了,如果您想要一个不特定于 DBMS 的答案,那么我不能添加比我已经说过的更多的东西。无论您如何进行插入,都使用约束,那么您可以永远违反表的完整性。如果您想详细了解如何避免 SQL Server (2008+) 中的竞争条件,请阅读 this article 关于将 MERGE 与 HOLDLOCK 结合使用。
  • 您根本无法在应用程序级别使用并发事务进行操作。如果在数据库级别没有阻止,两个并发事务仍然可以插入重复值。应用程序中之前的检查并不比数据库对 UNIQUE 约束所做的检查更昂贵 - 但在多用户环境中它不会是正确的。

标签: mysql sql sql-server oracle


【解决方案1】:

我认为在大多数情况下,这两者之间的差异将足够小,以至于选择应该主要通过选择最终对第一次查看代码的人来说最容易理解的实现来驱动。

不过,我认为异常处理有几个小的优点:

  • 异常处理避免了潜在的竞争条件。如果另一个进程在您的检查和插入之间插入一条记录,则“检查,然后插入”方法可能会失败。因此,即使您正在执行“检查然后插入”,您仍然希望对插入进行异常处理,如果您已经在进行异常处理,那么您最好取消初始检查。

  • 如果您的代码不是存储过程并且必须通过网络与数据库交互(即应用程序和数据库不在同一个盒子上),那么您希望避免有两个单独的网络调用(一个用于检查,另一个用于插入)并通过异常处理提供了一种通过单个网络调用处理整个事情的简单方法。现在,有很多方法可以执行“检查然后插入”方法,同时仍然避免第二次网络调用,但简单地捕获异常可能是最简单的方法。

另一方面,异常处理需要唯一约束(实际上是唯一索引),这带来了性能折衷:

  • 在非常大的表上创建唯一约束会很慢,并且会导致对该表的每次插入都会影响性能。在真正的大型数据库上,您还必须为用于强制约束的唯一索引消耗的额外磁盘空间进行预算。
  • 另一方面,如果您的查询可以利用该索引,则可以更快地从表中进行选择。

我还要注意,如果您处于实际想要做的是“更新其他插入”的情况(即,如果具有唯一值的记录已经存在,那么您想要更新该记录,否则您插入一条新记录),那么您真正想要使用的是您的特定数据库的 UPSERT 方法,如果它有的话。对于 SQL Server 和 Oracle,这将是一个 MERGE 语句。

【讨论】:

  • 谢谢。我不确定带有“预检查”的 UNIQUE 和 INSERT 的性能损失是否相同。会有所不同吗?如果是,有什么不同?
  • 在一种特殊情况下(使用 SAN 进行存储的集群 SQL Server),我的经验表明,检查然后插入的性能可能会明显变差。多跳似乎真的会影响重复呼叫的整体时间。我们有一个健谈的应用程序,并且对于使用该配置的几个客户来说,在我们应用程序的某些部分中,墙上运行时间明显更差。 (铜线与光纤,网卡速度慢或配置不当,谁知道)
  • 我不认为差异是“小”。区别是巨大的:您根本无法保证应用程序的唯一性,除非您对表执行完全排他性 write 锁定,这意味着应用程序内部的解决方案要么不可扩展,要么根本无法工作。两者都不是很好的选择。
  • @DaveE 我们有独立于数据库服务器的应用程序服务器(也在 SAN 上,并且装备精良,可以处理多个并发数据库请求),我的经验反映了您自己的经验。如果您正在处理一个缓慢的应用程序,该应用程序为每个页面加载执行多个单独的数据库查询,那么您应该尝试的第一件事是更改它,以便它并行或作为单个 jdbc 批处理执行所有内容。当您按顺序执行所有操作时,网络跃点的单个开销确实开始增加。
  • 唯一列出现错误“重复条目...”,如何避免不预先检查?
【解决方案2】:

取决于 #1(进行查找)的成本是否合理,我会两者都做。至少,在 Oracle 中,这是我最有经验的数据库。

理由:

  • 唯一/主键应该是您的数据模型设计的核心部分,我看不出有任何不实施它们的理由 - 如果您有太多数据以至于维护唯一索引会影响性能:
    • 这是很多数据
    • 从您的 OLTP 工作中对其进行分区或归档
  • 您拥有的约束越多,您的数据就越安全,不会出现应用程序逻辑错误。
  • 如果您首先检查某行是否存在,您可以轻松地从该行中提取其他信息以用作错误消息的一部分,或者以其他方式分叉应用程序逻辑来处理重复。
  • 在 Oracle 中,回滚 DML 语句相对昂贵,因为默认情况下 Oracle 期望成功(即 COMMIT 已写入的更改)。

【讨论】:

  • “取决于 #1(进行查找)的成本是否合理” - 我如何确定成本是否合理?我的表有 5 -1000 万行,并且以每月大约 10K 左右的速度增长。考虑到大量的行,我不确定是否应该同时进行预插入检查和唯一约束。
  • Oracle 中的两种方法,相信在其他数据库中也是类似的。 1. 试一试,看看需要多长时间。 2.您可以查看查询计划以了解它将如何执行。如果您有唯一索引,那么查找几乎肯定会使用它。我通常希望这是有效的。
  • 两种检查方式的成本可能相似,因为它基本上会做同样的事情。
  • 谢谢本。有没有办法比较两者的成本?万一我们遗漏了什么,或者如果 RDBMS 有一个怪癖,或者但是,这两种方法的成本可能会有所不同。
  • 我认为唯一真正的比较方法是运行基准测试并对结果计时。可以估计 SELECT 语句的成本 - Oracle 为此提供了解释计划功能,但我不确定它是否会包含检查唯一索引的成本。可能会。
【解决方案3】:

这并不能直接回答问题,但我认为在此处发布它可能会有所帮助,因为它比维基百科更好,并且链接可能有一天会失效。

链接 - http://www.celticwolf.com/blog/2010/04/27/what-is-a-race-condition/

维基百科对竞态条件有很好的描述,但如果您不了解编程的基础知识,就很难理解。我将尝试用不太专业的术语来解释它,使用上面描述的生成标识符的示例。我还将使用人类活动的类比来尝试传达这些想法。

竞态条件是两个或多个程序(或单个程序的独立部分)都尝试同时获取某些资源,从而导致错误答案或冲突。该资源可以是信息,例如下一个可用的约会时间,也可以是对某些内容的独占访问权,例如电子表格。如果您曾经使用 Microsoft Excel 编辑共享驱动器上的文档,您可能有过被 Excel 告知其他人已经在编辑电子表格的经历。此错误消息是 Excel 优雅地处理潜在竞争条件和防止错误的方式。

程序的一个常见任务是确定某种下一个可用值,然后分配它。这种技术用于发票号码、学生证等。这是一个以前解决过的老问题。最常见的解决方案之一是允许存储数据的数据库生成数字。还有其他解决方案,它们各有优缺点。

不幸的是,对这一领域一无所知或根本不擅长编程的程序员经常尝试自己动手。聪明的人很快发现这是一个比看起来复杂得多的问题,并寻找现有的解决方案。坏人永远不会看到问题,或者一旦看到问题,就会坚持在不修复错误的情况下使他们不可行的解决方案变得更加复杂。让我们以学生证为例。新手程序员说“要知道下一个学号应该是什么,我们只需获取最后一个学号并增加它。”以下是幕后发生的事情:

  1. 贝蒂,管理员。招生办公室的助理启动了学生管理程序。请注意,这实际上只是在她的 PC 上运行的程序的副本。它通过学校网络与数据库服务器通信,但无法与其他 PC 上运行的程序的其他副本通信。
  2. Betty 为 Bob Smith 创建一个新的学生记录,输入所有信息。
  3. 在 Betty 输入数据时,另一位管理员 George。助理,在他的 PC 上启动学生管理程序并开始为 Gina Verde 创建记录。
  4. George 的打字速度比较快,所以他和 Betty 在同一时间完成。他们都同时点击了“保存”按钮。
  5. Betty 的程序连接到数据库服务器并获取正在使用的最高学号 5012。
  6. George 的程序同时对同一个问题得到相同的答案。
  7. 两个程序都决定他们正在保存的记录的新学生 ID 应该是 5013。他们将该信息添加到记录中,然后将其保存在数据库中。
  8. 现在 Bob Smith(Betty 的学生)和 Gina Verde(George 的学生)拥有相同的学生证。

此学生证将附加到各种其他记录中,从成绩到食堂的用餐卡。最终这个问题会暴露出来,有人将不得不花费大量时间为其中一个人分配一个新的 ID 并整理出混乱的记录。

当我向人们描述这个问题时,通常的反应是“但这在实践中多久会发生一次?从来没有,对吧?”。错误的。首先,当您的员工进行数据输入时,通常每个人都在相对较短的时间内完成。这增加了重叠的机会。如果该应用程序是对公众开放的 Web 应用程序,那么两个人同时点击“保存”按钮的几率会更高。我最近在生产系统中看到了这一点。这是一个公开测试版的网络应用程序。使用率很低,每天只有几个人注册。尽管如此,六对人在几个月的时间内设法获得了相同的 ID。如果您想知道,不,我和我团队中的任何人都没有编写该代码。然而,我们对这个问题发生了多少次感到非常惊讶。事后看来,我们不应该这样做。这真的是墨菲定律的简单应用。

如何避免这个问题?最简单的方法是使用经过充分测试的问题的现有解决方案。所有主要数据库(MS SQL Server、Oracle、MySQL、PostgreSQL 等)都有一种方法可以增加数字而不会产生重复。 MS SQL server 称其为“identity”列,而 MySQL 称其为“auto number”列,但功能相同。每当您插入新记录时,都会自动创建一个新标识符并保证其唯一性。这将使上述情况发生如下变化:

  1. 贝蒂,管理员。招生办公室的助理启动了学生管理程序。请注意,这实际上只是在她的 PC 上运行的程序的副本。它通过学校网络与数据库服务器通信,但无法与其他 PC 上运行的程序的其他副本通信。
  2. Betty 为 Bob Smith 创建一个新的学生记录,输入所有信息。
  3. 在 Betty 输入数据时,另一位管理员 George。助理,在他的 PC 上启动学生管理程序并开始为 Gina Verde 创建记录。
  4. George 的打字速度比较快,所以他和 Betty 在同一时间完成。他们都同时点击了“保存”按钮。
  5. Betty 的程序连接到数据库服务器并将要保存的记录交给它。
  6. George 的程序同时交出另一条记录进行保存。
  7. 数据库服务器将两条记录放入队列中,一次保存一条,为它们分配下一个可用编号。
  8. 现在 Bob Smith(Betty 的学生)的 ID 为 5013,Gina Verde(George 的学生)的 ID 为 5014。

有了这个解决方案,复制就没有问题了。多年来,制造商和用户都反复测试了为每个数据库服务器执行此操作的代码。全世界数以百万计的应用程序都依赖它并每天继续对其进行压力测试。任何人都可以对他们的本土解决方案说同样的话吗?

至少有一种经过充分测试的方法可以在软件中而不是在数据库中创建标识符:uuids(通用唯一标识符)。但是,uuid 采用xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 的形式,其中“x”代表十六进制数字(0-9 和 a-f)。您想将其用于发票号、学生证或公众看到的其他标识符吗?应该不会吧。

总而言之,当两个程序或程序的两个独立部分尝试同时访问某些信息或访问资源时,就会发生竞争条件,从而导致错误,无论是计算错误还是标识符重复或对资源的访问冲突。竞争条件的类型比我在这里介绍的要多得多,它们会影响软件和硬件的许多其他领域。

【讨论】:

    【解决方案4】:

    您的问题的描述正是为什么主键可以是复合的,例如,它们由多个字段组成。这样,数据库将为您处理唯一性,您无需关心它。

    在您的情况下,表定义可能类似于以下内容:

     CREATE TABLE `real_estate` (
       `id` int(11) NOT NULL AUTO_INCREMENT,
       `property` varchar(255) DEFAULT NULL,
       `property_value` varchar(255) DEFAULT NULL,
       PRIMARY KEY (`id`),
       UNIQUE KEY `index_id_property_property_value` (`id`, `property`, `property_value`),
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
    

    【讨论】:

    • 您的唯一键是毫无意义的,id 根据定义是唯一的,因为它是主键,因为 id 是唯一的 idpropertyproperty_value 的任何组合都将也要独一无二。
    猜你喜欢
    • 1970-01-01
    • 2010-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多