【问题标题】:How does SQLDataReader handle really large queries?SQLDataReader 如何处理非常大的查询?
【发布时间】:2020-10-16 04:11:02
【问题描述】:

实际上我不确定标题是否准确地描述了这个问题,但我希望它足够接近。

我有一些代码从数据库表中执行 SELECT,我知道这将导致大约 150 万行被选中。每行中的数据并不大 - 每行可能 20 个字节。但这仍然是 30MB 的数据。每行包含一个客户编号,我需要对每个客户做一些事情。

我的代码如下所示:

SqlConnection conn = new SqlConnection(connString);
SqlCommand command = new SqlCommand("SELECT ... my select goes here", conn);
using (conn)
{
    conn.Open();
    using (SqlDataReader reader = command.ExecuteReader())
    {
        while(reader.Read())
        {
            ... process the customer number here
        }
    }
}

所以我只遍历 SELECT 返回的所有客户。

我的问题是,这会导致对数据库的多次读取,还是只读取一次?我假设网络缓冲区不足以容纳 30MB 的数据,那么 .NET 在这里做了什么?每次 Read() 推进指针时,SELECT 的结果是否被隐藏在某个地方,以便 SQLDataReader 蚕食一行?还是返回数据库?

我问的原因是代码的“...在此处处理客户编号”部分可能需要一些时间,因此对于 150 万客户而言,该代码(上面的 while 循环)将需要很多小时才能完成.当这种情况发生时,我是否需要担心其他人在数据库中阻止我,或者我知道我已经从数据库中完成了我的一次 SELECT 并且我不会再回去了,我是否安全?

【问题讨论】:

  • 所以您有 2 个问题? => 数据读取器如何工作以及它将如何影响我的查询和/或数据库上的其他用户
  • 在处理像这样的大量数据时,有时谨慎地更改代码的工作方式。对客户进行了哪些处理?在退货之前,是否可以使用其他条件排除任何客户?
  • @Schuere 好吧,是的,我想。后者是为了挠我眼前的痒,前者是为了通知我以后的事情。我将如何将它们分开?
  • @JeffR,与我们分享你想对 150 万客户做什么的过程,也许有更好的选择,比如 SPROCS

标签: c# sql


【解决方案1】:

选择将作为“单一的整体事务”执行。输出的余额缓存在 SQL Server 中,并在协议确定有缓冲区可用于接收它时传递到网络。但是,SQL Server 不会每次都返回数据表。原始SELECT 传递过来的数据状态将返回到您的应用程序。如果您指定了 (NOLOCK),您将不会对数据产生进一步影响。其他人可以读写;你不会看到他们的变化。但是,直到最后一行在您的应用服务器的缓冲区中,几个小时后,您还没有完成 SQL Server。每个“我现在有更多空间,拜托”都会有网络流量,但不会明显超过一次全部 30MB 的流量。

对于大型结果集和长时间运行的流程,您最好编写应用程序来批量处理数据,即使基础架构可以支持完整的查询输出。回答每个批处理查询所需的资源更少。在失败的情况下,您只需要处理剩余的行;您不必从头开始。您的应用程序最终会在整体上做更多的工作,但每个块对环境的破坏性会更小。

【讨论】:

  • 省略WITH (NOLOCK) 会导致SELECT 持有锁吗?如果结果集足够大,SQL Server 是否会在内部将输出假脱机到临时表?
  • @binki 一般情况下,省略 NOLOCK 会导致锁一直保持到事务结束。但是请注意交易的isolation level
  • @binki 优化器可以选择假脱机到 TempDB 作为所选计划的一部分。但是,由于网络缓冲区繁忙,我不相信它会这样做(但没有任何证据)。
  • 嗯,我明白了。我设法得到一个普通的SELECT 来锁定一个表,方法是在UPDATE 之后将其放入事务中并使客户端休眠。这让我非常担心,那些已经离开并且还没有让他们的会话超时的客户可能会因为不阅读他们的结果集而阻止所有用户。如果您可以强制临时表溢出,我认为这当然会以资源为代价来防止。
【解决方案2】:

请求发送一次,而不是每次读者前进。然后根据大小将结果通过几个结果集发送回客户端。

默认结果集是将结果传输到客户端的最有效方式。从客户端计算机发送到服务器的唯一数据包是带有要执行的语句的原始数据包。当结果被发送回客户端时,SQL Server 将尽可能多的结果集行放入每个数据包中,从而最大限度地减少发送到客户端的数据包数量。

参考http://msdn.microsoft.com/en-us/library/ms187602.aspx

当一个请求被提交执行时,SQL Server 通过以下方式将结果集发送回客户端:

  1. SQL Server 收到来自客户端的网络数据包,其中包含 Transact-SQL 语句或一批 Transact-SQL 语句 执行。
  2. SQL Server 编译并执行语句或批处理。
  3. SQL Server 开始放置结果集的行,或多个 来自批处理或存储过程的结果集、网络数据包和 将它们发送给客户。 SQL Server 放置尽可能多的结果集行 尽可能在每个数据包中。
  4. 包含结果集行的数据包缓存在网络中 客户端的缓冲区。当客户端应用程序获取行时, ODBC 驱动程序或 OLE DB 提供程序从 网络缓冲并将数据传输到客户端应用程序。 客户端在转发中一次检索一行结果 方向。

默认结果集不会在一个大块中提供给应用程序。结果集缓存在客户端的网络缓冲区中。应用程序一次通过结果集获取一行。在每次提取时,OLE DB 提供程序或 ODBC 驱动程序将数据从网络缓冲区中的下一行移动到应用程序中的变量中。 OLE DB、ODBC 和 ADO 应用程序使用相同的 API 函数来检索它们将用于从游标中获取行的行。 SqlClient 托管提供程序使用 SqlDataReader 类公开默认结果集。当 MultipleActiveResultSets 设置为 true 时,允许在给定时间打开多个 SqlDataReader。

参考:http://technet.microsoft.com/en-us/library/ms187602(v=sql.105).aspx

【讨论】:

  • 这告诉我数据是如何通过网络传输的,但它并没有告诉我是否在单个事务中从数据库中提取了所有数据。问题是,当 SELECT 被执行时,它是作为一个单一的、整体的事务完成的吗?如果是这样,那可能是 30MB 的数据,在 While(reader.Read()) 循环中以小块的形式提供给我。 30MB 的数据保存在哪里?它是真的从数据库中读取并存储在某处的缓冲区中,还是 While 循环实际上返回到数据库并导致那里的流量?我在 ExecuteReader() 之后完成了数据库吗?
  • 很明显,Transact SQL 查询只会发送一个请求。服务器执行查询。然后 SQL Server 将结果打包成网络数据包并继续发送回客户端,直到发送完整个结果。客户端将其存储在缓存中,以便您的应用程序可以逐行读取。这意味着您的 30MB 数据将保留在服务器上,直到发送整个结果。每次阅读器前进时都会有接收网络数据包的流量,而不是发送请求。我会在我的答案中添加一些额外的信息。请检查一下
【解决方案3】:

首先,我会将您重定向到关于 SO 的以下问题,其中描述了如何处理锁等:

Understanding SQL Server LOCKS on SELECT queries

我的第一个问题是,您将运行此查询多少次。如果是按天计算,请确保选择在数据库上工作的用户最少的时间。

第二个问题是,你打算如何处理这些数据?也许您应该记住,在处理 1M+ 记录时,存储过程会更快,因为它会处理数据库上的所有内容并保持低流量。

【讨论】:

  • SELECT 使用 (NOLOCK) 提示,并且每天至少运行一次。表中不会总是有 150 万行,但现在有。一旦流程成熟,可能只有 20,000-30,000 行需要处理。每个客户的处理是繁重的,并且涉及访问(读取和写入)其他数据库。存储过程不起作用。
  • @jeffR,您在选择、更新/插入记录吗?
  • 是的,但不在同一个表中。我正在更新记录并将其插入到多个数据库中的其他表中。我必须为每个客户进行的处理包括从多个数据源读取其他数据、操作数据,然后基于该处理更新表。这不是存储过程可以做到的。
【解决方案4】:

DataReader 没有在客户端缓存任何内容;每次您调用Read() 时,它都会尝试从服务器传输数据。它的方式是这样的(根据经验):

  1. ExecuteReader() 阻塞,直到第一个 Sql 语句产生数据返回给客户端。
  2. NextResult() 阻塞直到服务器
    1. 表示服务器上没有执行任何其他操作,“命令”实际上已完成。
    2. XOR 命令中的后续语句生成数据 以返回给客户端。
  3. Read() 阻塞,直到服务器能够将下一条记录流式传输到客户端。 (是的,这意味着无序选择几乎总是比有序选择更早地开始流式传输到客户端。)
    • 例如我已经看到 SqlServer 需要 15 秒才能开始流式传输结果(NextResult() 返回),然后,一段时间后,在调用Read() 时再阻塞 15 秒;这是在带有 ORDER BY 的 SELECT 上。 (从 SSMS 执行查询时的行为相同。)
  4. 整个命令文本将与DataReader 的运行方式同步执行。
    • 即如果您的命令中有 2 个 SELECT 语句都返回数据,则第二个 SELECT 将仅在调用 NextResult() 时在服务器上开始执行。但是如果第一个返回零结果,第二个将在ExecuteReader() 期间开始执行。 (无论如何,您总是需要调用NextResult() 来获取第二个 SELECT 的数据。)

仅供参考:我的经验是使用 (MS) Sql2019 和 .Net Framework,而 IIRC 这种行为在 2013 年仍然如此。

所以要明确回答你的问题

您的查询是一个单独的 SELECT 语句,它将在自己的隐式事务中执行。一旦服务器完成查找所有要返回的数据/行,它将释放它可能在表上获得的所有锁,此时,您的代码不会对其他查询产生任何直接影响。相同的表。

但是,在您完成所有Read() 调用之前,您仍然需要占用服务器上的资源,并从连接池中获得对该连接的独占访问权限。因此,在您的示例中,您希望更改 while(reader.Read()) 循环以将所有数据捕获到本地对象中。然后在关闭连接后编写一个后续循环,以针对该数据执行长时间运行的过程。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-20
    • 2013-05-30
    • 2021-10-19
    • 2012-07-03
    • 1970-01-01
    相关资源
    最近更新 更多