如何使用Task Parallel库批量读取SQL表中的大量记录

时间:2017-05-28 20:24:12

标签: c# sql multithreading parallel-processing task-parallel-library

我在SQL server中有一个名为 Accounts 的sql表,其中数据按日期排序。该表有20列,包括AccountId 我想阅读每天的记录(约200K记录)。这样我就必须每天读取6个月的数据。

我需要做的是从Accounts Table中获取6个月的数据记录。所以我计划了我的代码,以便在每天的do while循环中从SQL Server Accounts表中获取数据。

现在,每天都包含从数据库中提取200K记录。因此,我将这200K记录分成一天进行批量处理(让我们在一次读取中记录10000或20000条记录,这使得一天内有大约10批记录)。一旦我得到这些10k或20k记录,我想从数据库中获取这些值并将其转换为csv文件并将csv文件导出到某个位置。

现在,我的问题是这个程序耗费了太多时间(大约需要50分钟才能获取一天的记录,我需要获取6个月数据的记录。所以你可以想象需要花多少时间)

我正在考虑使用TPL来破解代码并处理成任务,但不知道如何去做。

请建议如何使用Task并行库来提高性能,以便我可以轻松获得6个月的数据。

我的C#代码如下所示:

public void Main()
{
    do
    {
        done = true;
        var accountsTableRecors = ReadsDatabaseForADay(lastId);
        foreach (var accountsHistory in accountsTableRecors)
        {
            if (accountsHistory.accountsId != null) lastId = (long)accountsHistory.accountsId;
            done = false;
            recordCount++;
        }
        var flatFileDataList = ProcessRecords();
    } while (!done);
}

ProcessRecords上面的Main()方法解析了一些xml并将获取的数据转换为csv。

private IEnumerable<AccountsTable> ReadsDatabaseForADay(long lastId)
{
    var transactionDataRecords = DatabaseHelper.GetTransactions(lastId, 10000);
    var accountsTableData = transactionDataRecords as IList<AccountsTable> ?? transactionDataRecords.ToList();
    ListAccountsTable.AddRange(accountsTableData);
    return accountsTableData;
}

DatabaseHelperClass

internal static IEnumerable<AccountsTable> GetTransactions(long lastTransactionId, int count)
{
    const string sql = "SELECT TOP(@count) [accounts_id],[some_columns],[some_other_columns]. .....[all_other_columns] " 
                    + "FROM AccountsTable WHERE [accounts_id] > @LastTransactionId AND [creation_dt] > DATEADD(DAY,-1, GETDATE())" +
                   " ORDER BY [accounts_id]";
    return GetData(connection =>
    {
        var command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@count", count);
        command.Parameters.AddWithValue("@LastTransactionId", lastTransactionId);
        return command;
    }, DataRecordToTransactionHistory);
}

private static IEnumerable<T> GetData<T>(Func<SqlConnection, SqlCommand> commandBuilder, Func<IDataRecord, T> dataFunc)
{
    using (var connection = GetOpenConnection())
    {
        using (var command = commandBuilder(connection))
        {
            command.CommandTimeout = 0;
            using (IDataReader reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var record = dataFunc(reader);
                    yield return record;
                }
            }
        }
    }
}

1 个答案:

答案 0 :(得分:0)

您的代码中存在一些问题,这可能会导致性能下降。首先,每次获得另一批帐户表时,您都会从IEnumerable创建一个列表。 List<T>在内部使用一个数组,默认情况下会转到大对象堆,并且该堆不经常GC,所以到时候你会遇到内存问题和性能缺陷。

第二个问题是您的代码是高度面向I / O的,但是您不使用async方法,因此等待数据库响应会阻止您的线程。您应该切换到async版本并仅在Main入口点内使用屏蔽方法,但在这种情况下,您将无法yield您的结果,{{1} }不能是迭代器,您需要编辑async Task<IEnumerable>函数或在DataRecordToTransactionHistory函数内本地创建列表:

GetData

然而,这两种选择都有其缺点。至于我,你应该尝试使用这样的处理管道TPL Dataflow库:

private static async Task GetData(Func<SqlConnection, SqlCommand> commandBuilder,
    Action<IDataRecord> dataFunc)
{
    using (var connection = new SqlConnection())
    {
        await connection.OpenAsync();
        using (var command = commandBuilder(connection))
        {
            command.CommandTimeout = 0;
            using (var reader = await command.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    dataFunc(reader);
                }
            }
        }
    }
}
// OR
private static async Task<IEnumerable<T>> GetData<T>(Func<SqlConnection, SqlCommand> commandBuilder,
    Func<IDataRecord, T> dataFunc)
{
    using (var connection = new SqlConnection())
    {
        await connection.OpenAsync();
        using (var command = commandBuilder(connection))
        {
            command.CommandTimeout = 0;
            using (var reader = await command.ExecuteReaderAsync())
            {
                // linked list to avoid allocation of an array
                var result = new LinkedList<T>();
                while (await reader.ReadAsync())
                {
                    result.AddLast(dataFunc(reader));
                }
                return result;
            }
        }
    }
}

// default linking options var options = new DataflowLinkOptions { PropagateCompletion = true }; // store all the ids you need to process var idBlock = new BufferBlock<long>(); // transform id to list of AccountsTable var transformBlock = new TransformBlock<long, Task<IEnumerable<AccountsTable>>>(async id => { return await ReadsDatabaseForADay(id); }); // connect the blocks between each other idBlock.LinkTo(transformBlock, options); // flatten the already completed task to enumerable var flattenBlock = new TransformManyBlock<Task<IEnumerable<AccountsTable>>, AccountsTable>(async tables => { return (await tables).Select(t => t); }); transformBlock.LinkTo(flattenBlock, options); // gather results in batches of 10000 var batchBlock = new BatchBlock<AccountsTable>(10000); flattenBlock.LinkTo(batchBlock); // export resulting array to csv file var processor = new ActionBlock<AccountsTable[]>(a => { ExportToCSV(a); }); // populate the pipeline with needed ids foreach (var id in GetAllIds()) { // await the adding for each id await idBlock.SendAsync(id); } // notify the pipeline that all the data has been sent idBlock.Complete(); // await whole ids to be processed await processor.Completion; 在默认线程池中执行并使用它的所有优点。您可以调整块with MaxDegreeOfParrallelism,以便它可以立即处理多个ID。

因此,您应该使用TPL Dataflow方法,并且不应仅为存储数据创建太多缓冲区列表/数组。组装管道以充分利用迭代器和async函数。