从具有大量数据的 SQL 服务器 Sproc 批量读取结果集?

Batch Reading a Result Set from a SQL Server Sproc with a very large amount of data?

我正在尝试使用来自 SQL 服务器的 .NET 代码执行数据导出。我在存储过程中有大量的导出逻辑,它返回带有 FOR JSON AUTO 的结果集以导出数据。 Stored Proc 完全按照我的需要构建 JSON 结果,而我的 .NET 代码基本上只需要使用该 Stored Proc 并将结果放入 FTP 位置。这对我的大部分导出都非常有用。

问题是一些导出有大量数据,生成的 JSON 文件有数百兆字节(并且 为千兆字节)。我 运行 遇到了一些问题,由于我处理这些数据的方式,我开始 OutOfMemeoryExceptions 被抛出(即将它全部存储在 .NET 的内存中,既在我填充的数据表中,又在稍后成一个 StringBuilder)。虽然我暂时投入了更多 money/memory 来解决这个问题,但这不是一个可扩展的解决方案。

所以我显然需要改变策略,这很好,因为我当前的流程没有得到很好的优化。最明显的反策略是使用 SqlDataReader 并调用 while (reader.Read()) 并将其推送到磁盘上的临时文件(稍后流式传输到 FTP)。从技术上讲,这种方法有效,因为我真的不需要 "intelligently process" 记录集 - 只需存储它并发送它。但是 reader.Read() 一次只会获得 1 条记录(其中 FOR JSON AUTO 多于 1 条记录,但与数百兆字节相比仍然很小)。这种方法的问题在于,这里的数据有效载荷很小 太小了 并且这使得到数据库的往返次数过多,导致此导出太简单了减缓。我通常用 chunky over chatty 进行优化,这个选项太 chatty 但我今天做的方式太 粗壮.

这两个选项之间有什么区别吗?如果我可以执行 SqlDataReader,我会很高兴,但告诉它每次往返数据库时将其批处理为 1000 条记录,因为这些网络往返正是这种反策略让我丧命的原因。或者,如果有某种方法可以控制 FOR JSON AUTO 记录集的记录大小比当前大得多(8k?我忘记了),那也可能是一个选项。

我的解决方案中有 Entity Framework,尽管我的当前代码下降到 ADO.NET。如果有必要,我愿意包括其他框架。但一个简单的 ADO.NET 解决方案将是理想的。

虽然这可能不是一个完美的解决方案,但这是我采用的实现方式:

    /// <summary>
    ///     Calls a stored procedure that returns data as "FOR JSON AUTO" and stores the results into a temporary file.
    /// </summary>
    /// <param name="connection">SQLConnection to execute the stored procedure on.</param>
    /// <param name="commandTextForJsonQuery">CommandText for the execution.</param>
    /// <param name="parameters">All input and output parameters.</param>
    /// <param name="optionalFilePathAndName">
    ///     Optional file name and path to write to. If not specified, a Temp file will be created.
    ///     If specified but the file does not exist, the file will be created. If it exists, it will first be deleted and all
    ///     contents will be lost.
    /// </param>
    /// <param name="commandTimeoutInSeconds">CommandTimeout to use with execution.</param>
    /// <returns>Returns the filename and path with the data in it.</returns>
    private static async Task<string> CallJsonQueryAndSaveToFile(SqlConnection connection, string commandTextForJsonQuery, SqlParameter[] parameters, string optionalFilePathAndName, int commandTimeoutInSeconds)
    {
        var needsToCloseConnection = false;
        optionalFilePathAndName = string.IsNullOrWhiteSpace(optionalFilePathAndName)
                                      ? Path.GetTempFileName()
                                      : optionalFilePathAndName;

        if (File.Exists(optionalFilePathAndName))
        {
            File.Delete(optionalFilePathAndName);
        }

        using (var file = File.AppendText(optionalFilePathAndName))
        {
            using (var command = new SqlCommand(commandTextForJsonQuery, connection)
                                 {
                                     CommandTimeout = commandTimeoutInSeconds,
                                     CommandType = CommandType.Text
                                 })
            {
                command.Parameters.AddRange(parameters);

                try
                {
                    if (connection.State != ConnectionState.Open)
                    {
                        needsToCloseConnection = true;
                        connection.Open();
                    }

                    using (var reader = command.ExecuteReader())
                    {
                        while (await reader.ReadAsync())
                        {
                            await file.WriteAsync(reader.GetString(0));
                        }
                    }
                }
                finally
                {
                    if (needsToCloseConnection)
                    {
                        connection.Close();
                    }
                }
            }
        }

        return optionalFilePathAndName;
    }
}

在我的例子中的 commandTextForJsonQuery 示例:

var cmdText = "EXEC @procResult = [dbo].[spFoo] @Param1, @Param2, @Param3"

此实施似乎 运行 比我预期的要快得多。虽然在 Azure 中托管时我没有对其进行大量测试,但在本地测试时我能够使我的 200mbps ISP 连接饱和,这对我来说已经足够了。我不需要对它进行完美优化,只是 "optimized enough" 所以它不会永远执行。