【问题标题】:What is the real cost of a SQL transaction?SQL 事务的实际成本是多少?
【发布时间】:2010-10-08 19:37:09
【问题描述】:

这需要一些背景知识。我正在创建一个 Web 应用程序(带有 SQL Server 后端的 ASP.NET/VB.NET),它将接受申请以接收资金,并且此表单包含三个人的联系信息字段。因为所有三个人的信息(姓名、电子邮件、电话)都相同,所以我选择将联系信息存储在与应用程序分开的表中。在应用程序表中,这三个联系人中的每一个都有一个 ID 字段,指向联系人表中的一个联系人。

但是,这在我的 CRUD 工作中提出了一个有趣的问题/问题。我能想到的在一个 SQL 事务中为应用程序和所有三个联系人创建、更新和检索信息的唯一方法需要非常复杂的存储过程。换句话说,(对我来说)通过多个事务检索这些信息会简单得多。然而,由于这些信息从来都不是独立需要的,所以我总是会进行多次交易来获取一个应用程序的信息。

所以我的问题:

  1. 这种设计是不是有点矫枉过正?每个应用程序的联系人不会超过三个,并且必须恰好三个。将此信息删除到单独的表格中是否过于复杂?
  2. 与编写复杂的存储过程并且只需要一个事务相比,执行多个 SQL 事务的实际成本是多少?
  3. 一般而言,使用 ADO.NET 对 Web 应用程序执行 SQL 事务的成本是多少?

感谢您接受冗长的解释。

*编辑*

在阅读了您的一些回复后,看来我使用“交易”一词是错误的。我实际上好奇的是跨单个连接执行多个查询而不是执行一个查询的成本。很抱歉造成误会。

【问题讨论】:

  • 这不是,我再说一遍不是“复杂的存储过程”。如果您有多个作为工作单元的数据库更改,则必须使用事务,“成本”是只进行了一些更改而没有进行了一些更改。

标签: sql sql-server ado.net


【解决方案1】:

需要事务将数据库从一种一致状态移动到另一种一致状态。这里的“一致”适用于数据库的应用程序视图。典型的例子是两个账户之间的转账:你必须从一个账户借记,另一个账户贷记。在这两个操作之间,数据库是不一致的,有一些钱已经“消失”(从一个帐户中扣除的金额无处)。但在事务结束时,数据库再次保持一致。您需要一个事务来跨越这两个操作,以防止读者查看不一致的数据库,并在发生崩溃时确保数据库一致。

您说,为了在单个事务中处理逻辑中的多个项目,您需要复杂的程序。这不是真的,事务范围与请求范围正交:客户端可以启动事务,通过在 3 次调用中调用 3 个过程来执行 3 次操作,然后提交事务。 要求所有操作都在一个存储过程中完成。

所以事务应该导致显着的程序开销。事实上,程序会忽略事务。一个编写良好的过程在事务内部调用以及在没有事务的情况下调用时都应该正确运行。请参阅Exception handling and nested transactions 以获取在出现事务异常时行为正确的过程模板。

最后,交易的真实成本是多少?事务写入 数据(读取并不真正需要事务),因此它们锁定 修改的行。当试图读取这些被阻塞的行时,读取通常会阻塞,然后事务越长,它锁定行的时间越长,它阻塞的读取就越多。但是有一个非常简单的解决方案:快照读取。快照读取确实是一种神奇的小精灵尘埃,它们允许应用程序不受阻塞行的阻碍,因为读取器始终可以读取阻塞更新之前的行版本行。见Using Row Versioning-based Isolation Levels

所以结论很简单:交易没有成本。仅仅是因为除了交易之外别无选择。不是“X 比 Y 慢”的问题,而是“X 是唯一正确的替代方案”的问题。

更新

编辑后:多个请求与一个请求的成本哦可能很大。到服务器的往返(即在打开的连接上发出请求)具有固定成本。如果您进行多次往返,那么您为每个请求支付此固定成本。如果您使用多个过程调用执行一个请求,则此固定成本只需支付一次。在非常热的系统上,这种差异是可以测量的,并且会影响整体性能。但我说的是真正很热的系统,每秒有数千个请求。解决方案通常不是使过程复杂化,而是在一个请求中发出多个过程调用:

SqlCommand cmd = new SqlCommand(@"
exec usp_firstProc @arg1, @arg2;
exec usp_secondProc @arg2, @arg3;
exec usp_thirdProc @arg1, @arg3;", connection);
cmd.Parameters.Add("@arg1", someValue);
...
cmd.ExecuteNonQuery();

但我必须说,除非您有一个 非常热 的工作负载,否则这个解决方案并不是真正需要的。作为一个粗略的规则,对于任何低于 1000 个请求/秒的东西,我会认为清晰代码的好处大于性能好处。

如果您必须为每个请求打开一个新连接,则有一点不同。登录握手真的很昂贵,可以用数百毫秒来衡量。但解决方案很简单:使用连接池(在 ADO.Net 中默认启用)并且不要过早丢弃应用程序中的连接,保持连接并重用它,直到整个工作单元完成。

【讨论】:

  • +1 并接受!这是一个很好的答案!你清楚地知道你的东西:)
【解决方案2】:
  1. 这并不过分。这是一个很好的规范化,也是对 Don't-Repeat-Yourself 原则的一个很好的应用。系统的要求可能会发生变化,您将来可能需要 2 或 4 个联系人,而不是 3 个。
  2. 您不需要执行任何复杂的存储过程。你在这里没有做任何不寻常的事情。您根本不需要存储过程。
  3. 不知道。

当您说“交易”时,我认为您的意思是“查询”。当我想获取一个应用程序的数据时,我会使用两个单独的简单查询:

SELECT *
  FROM application

SELECT *
  FROM contact

这是两个独立的查询,但很重要。更重要的部分是以有意义的方式设计数据库。

【讨论】:

【解决方案3】:

您在这里使用了“交易”一词有点不正确。我认为你指的是一个简单的数据库查询,ACID 事务助手。

这就是说,很难说。默认情况下启用的连接池将无需打开三个/四个单独的连接。

您甚至可能不需要多个查询来获取数据,并且可以连接两个表来检索它,例如:

SELECT app.field1, app,field2, contact1.name, contact2.name, contact3.name
FROM Applications AS app
INNER JOIN Contacts AS contact1 ON app.contact1ID = contact1.id
INNER JOIN Contacts AS contact2 ON app.contact2ID = contact2.id
INNER JOIN Contacts AS contact3 ON app.contact3ID = contact3.id

或类似的东西,考虑到您的情况(从应用程序向选择列表添加新字段,从每个联系人表返回更多数据等。如果表被正确索引,查找速度非常快,并且可能不会影响性能完全没有。

如果您要插入/更新数据,那么是的,您需要多个查询才能做到这一点,但数据的选择可以一次完成。

当然,另一种选择是将数据放回原始应用程序表中,首先不需要第二个表。此时,不需要额外的连接或其他任何东西。

【讨论】:

    【解决方案4】:

    给你一些想法......

    如果您当前的设计是两个表,第一个表(应用程序)为

    ID、contact_id1、contact_id2、contact_id3、其他信息

    作为联系人的第二张表很简单

    contact_id,信息

    这种格式的最大优势是,如果您有 1 个联系人在多个应用程序上...Conatact 'Bob' is contact_1 on 8 个不同的应用程序意味着 bob 的信息只记录一次,并且只有他的 ID 记录在应用程序表中。最大的限制是每个应用程序的 3 个联系人是永久编码的

    Alternatively..application 可以定义为 application_ID,信息 并作为 application_ID、id_sequence、信息

    这种设置的优点是可以为每个应用程序存储无限数量的联系人。

    虽然一切都可以满足您的需求...如果您总是有 3 个联系人,那么这里的第二个选项有点不必要。如果您不打算拥有超过 1000 条记录,那么将所有信息存储在一个表中并不是最糟糕的主意(选项一主要消除冗余数据)。回答您的问题 1……在某些情况下,这太过分了,但答案将取决于您当前的要求和未来的要求。从“最佳实践”设计来看,2 表设置是首选。

    只是对问题 #2 的评论 - 这将是一个更简单的存储过程...我看不出它对您的设置来说太复杂了。您可能可以创建一个可更新的视图(但不确定那里可能会出现什么限制或问题)。

    【讨论】:

      【解决方案5】:

      除了纯处理成本外,每个查询都会花费您一次客户端-数据库-服务器往返时间。因此,如果您设法批量处理简单的单个查询,则可以节省往返时间。

      【讨论】:

        【解决方案6】:

        使用 1 个存储过程完成所有操作,因此您只需要打开 1 个 sql 连接。

        Web 表单中的每个变量都映射到存储过程中的一个参数(包括隐藏的联系人和应用程序密钥,对于新应用,您默认为 0)

        在过程中,如果记录键为 0,则过程将记录添加到数据库并使用 scope_identity() 快速检索键。如果您向程序发送非零键,它会更新记录。然后该过程返回所有数据,并将所有存储过程变量声明为输出。

        然后,您可以将过程包装在 sql 开始/结束事务中,使其成为全有或全无的 sql 命令。

        【讨论】:

        • 我不明白 - 为什么我们需要将存储过程带入其中?
        猜你喜欢
        • 2016-08-11
        • 1970-01-01
        • 2015-05-25
        • 2015-04-21
        • 2015-03-28
        • 2018-08-02
        • 2016-10-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多