将大型CSV文件并行导出到SQL Server

时间:2014-10-22 04:51:52

标签: c# sql sql-server multithreading csv

我有一个大型CSV文件... 10列,1亿行,我的硬盘大小约为6 GB。 我想逐行读取此CSV文件,然后使用SQL批量复制将数据加载到Microsoft SQL Server数据库中。 我已经在这里以及互联网上阅读了几个主题。大多数人认为并行读取CSV文件在效率方面并不多,因为任务/线程争用磁盘访问。

我要做的是,从CSV逐行读取并将其添加到大小为100K行的阻止集合中。一旦这个集合完全启动,就会使用SQLBuckCopy API将数据写入SQL服务器。

我已经编写了这段代码,但是在运行时遇到错误,上面写着“尝试在具有挂起操作的对象上调用批量复制”。这个场景看起来像是可以使用.NET 4.0 TPL轻松解决的,但我无法让它工作。关于我做错了什么的建议?

    public static void LoadCsvDataInParalleToSqlServer(string fileName, string connectionString, string table, DataColumn[] columns, bool truncate)
    {
        const int inputCollectionBufferSize = 1000000;
        const int bulkInsertBufferCapacity = 100000;
        const int bulkInsertConcurrency = 8;

        var sqlConnection = new SqlConnection(connectionString);
        sqlConnection.Open();

        var sqlBulkCopy = new SqlBulkCopy(sqlConnection.ConnectionString, SqlBulkCopyOptions.TableLock)
        {
            EnableStreaming = true,
            BatchSize = bulkInsertBufferCapacity,
            DestinationTableName = table,
            BulkCopyTimeout = (24 * 60 * 60),
        };

        BlockingCollection<DataRow> rows = new BlockingCollection<DataRow>(inputCollectionBufferSize);
        DataTable dataTable = new DataTable(table);
        dataTable.Columns.AddRange(columns);

        Task loadTask = Task.Factory.StartNew(() =>
            {
                foreach (DataRow row in ReadRows(fileName, dataTable))
                {
                    rows.Add(row);
                }

                rows.CompleteAdding();
            });

        List<Task> insertTasks = new List<Task>(bulkInsertConcurrency);

        for (int i = 0; i < bulkInsertConcurrency; i++)
        {
            insertTasks.Add(Task.Factory.StartNew((x) =>
                {
                    List<DataRow> bulkInsertBuffer = new List<DataRow>(bulkInsertBufferCapacity);

                    foreach (DataRow row in rows.GetConsumingEnumerable())
                    {
                        if (bulkInsertBuffer.Count == bulkInsertBufferCapacity)
                        {
                            SqlBulkCopy bulkCopy = x as SqlBulkCopy;
                            var dataRows = bulkInsertBuffer.ToArray();
                            bulkCopy.WriteToServer(dataRows);
                            Console.WriteLine("Inserted rows " + bulkInsertBuffer.Count);
                            bulkInsertBuffer.Clear();
                        }

                        bulkInsertBuffer.Add(row);
                    }

                },
                sqlBulkCopy));
        }

        loadTask.Wait();
        Task.WaitAll(insertTasks.ToArray());
    }

    private static IEnumerable<DataRow> ReadRows(string fileName, DataTable dataTable)
    {
        using (var textFieldParser = new TextFieldParser(fileName))
        {
            textFieldParser.TextFieldType = FieldType.Delimited;
            textFieldParser.Delimiters = new[] { "," };
            textFieldParser.HasFieldsEnclosedInQuotes = true;

            while (!textFieldParser.EndOfData)
            {
                string[] cols = textFieldParser.ReadFields();

                DataRow row = dataTable.NewRow();

                for (int i = 0; i < cols.Length; i++)
                {
                    if (string.IsNullOrEmpty(cols[i]))
                    {
                        row[i] = DBNull.Value;
                    }
                    else
                    {
                        row[i] = cols[i];
                    }
                }

                yield return row;
            }
        }
    }

3 个答案:

答案 0 :(得分:6)

别。

并行访问可能会或可能不会让您更快地读取文件(它不会,但我不打算 战斗......)但是对于某些并行写入它赢了不会给你更快的批量插入。这是因为最小记录的批量插入(即非常快批量插入)需要表锁。见Prerequisites for Minimal Logging in Bulk Import

  

最小日志记录要求目标表满足以下条件:

     

...
   - 指定表锁定(使用TABLOCK)   ......

根据定义,并行插入无法获取并发表锁。 QED。你正在咆哮错误的树。

停止在互联网上随机查找您的来源。阅读The Data Loading Performance Guide,是 指南,以便...执行高性能数据。

我建议你停止发明轮子。使用SSIS,这完全旨在处理的内容。

答案 1 :(得分:5)

http://joshclose.github.io/CsvHelper/

https://efbulkinsert.codeplex.com/

如果可能,我建议您将文件读入List&lt; T&gt;使用前面提到的csvhelper并使用批量插入来写你的数据库,或者你使用的efbulkinsert并且速度非常快。

using CsvHelper;

public static List<T> CSVImport<T,TClassMap>(string csvData, bool hasHeaderRow, char delimiter, out string errorMsg) where TClassMap : CsvHelper.Configuration.CsvClassMap
    {
        errorMsg = string.Empty;
        var result = Enumerable.Empty<T>();

        MemoryStream memStream = new MemoryStream(Encoding.UTF8.GetBytes(csvData));
        StreamReader streamReader = new StreamReader(memStream);
        var csvReader = new CsvReader(streamReader);

        csvReader.Configuration.RegisterClassMap<TClassMap>();
        csvReader.Configuration.DetectColumnCountChanges = true;
        csvReader.Configuration.IsHeaderCaseSensitive = false;
        csvReader.Configuration.TrimHeaders = true;
        csvReader.Configuration.Delimiter = delimiter.ToString();
        csvReader.Configuration.SkipEmptyRecords = true;
        List<T> items = new List<T>();

        try
        {
            items = csvReader.GetRecords<T>().ToList();
        }
        catch (Exception ex)
        {
            while (ex != null)
            {
                errorMsg += ex.Message + Environment.NewLine;

                foreach (var val in ex.Data.Values)
                    errorMsg += val.ToString() + Environment.NewLine;

                ex = ex.InnerException;
            }
        }
        return items;
    }
}

编辑 - 我不明白你在批量插入中做了什么。您希望批量插入整个列表或数据数据表,而不是逐行插入。

答案 2 :(得分:3)

您可以创建存储过程并传递文件位置,如下所示

CREATE PROCEDURE [dbo].[CSVReaderTransaction]
    @Filepath varchar(100)=''
AS
-- STEP 1: Start the transaction
BEGIN TRANSACTION

-- STEP 2 & 3: checking @@ERROR after each statement
EXEC ('BULK INSERT Employee FROM ''' +@Filepath
        +''' WITH (FIELDTERMINATOR = '','', ROWTERMINATOR = ''\n'' )')

-- Rollback the transaction if there were any errors
IF @@ERROR <> 0
 BEGIN
    -- Rollback the transaction
    ROLLBACK

    -- Raise an error and return
    RAISERROR ('Error in inserting data into employee Table.', 16, 1)
    RETURN
 END

COMMIT TRANSACTION

你也可以像FIELDTERMINATOR和ROWTERMINATOR一样添加BATCHSIZE选项。