我有一个大型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;
}
}
}
答案 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选项。