从文本文件导入到SQL Server数据库,是ADO.NET太慢了吗?

我的程序现在仍在运行,以将数据从日志文件导入远程SQL Server数据库。 日志文件大小约为80MB,包含大约470000行,包含大约25000行数据。 我的程序只能导入300行/秒,这真的很糟糕。 🙁

public static int ImportData(string strPath) { //NameValueCollection collection = ConfigurationManager.AppSettings; using (TextReader sr = new StreamReader(strPath)) { sr.ReadLine(); //ignore three first lines of log file sr.ReadLine(); sr.ReadLine(); string strLine; var cn = new SqlConnection(ConnectionString); cn.Open(); while ((strLine = sr.ReadLine()) != null) { { if (strLine.Trim() != "") //if not a blank line, then import into database { InsertData(strLine, cn); _count++; } } } cn.Close(); sr.Close(); return _count; } } 

InsertData只是使用ADO.NET的普通插入方法。 它使用解析方法:

 public Data(string strLine) { string[] list = strLine.Split(new[] {'\t'}); try { Senttime = DateTime.Parse(list[0] + " " + list[1]); } catch (Exception) { } Clientip = list[2]; Clienthostname = list[3]; Partnername = list[4]; Serverhostname = list[5]; Serverip = list[6]; Recipientaddress = list[7]; Eventid = Convert.ToInt16(list[8]); Msgid = list[9]; Priority = Convert.ToInt16(list[10]); Recipientreportstatus = Convert.ToByte(list[11]); Totalbytes = Convert.ToInt32(list[12]); Numberrecipient = Convert.ToInt16(list[13]); DateTime temp; if (DateTime.TryParse(list[14], out temp)) { OriginationTime = temp; } else { OriginationTime = null; } Encryption = list[15]; ServiceVersion = list[16]; LinkedMsgid = list[17]; MessageSubject = list[18]; SenderAddress = list[19]; } 

InsertData方法:

 private static void InsertData(string strLine, SqlConnection cn) { var dt = new Data(strLine); //parse the log line into proper fields const string cnnStr = "INSERT INTO LOGDATA ([SentTime]," + "[client-ip]," + "[Client-hostname]," + "[Partner-Name]," + "[Server-hostname]," + "[server-IP]," + "[Recipient-Address]," + "[Event-ID]," + "[MSGID]," + "[Priority]," + "[Recipient-Report-Status]," + "[total-bytes]," + "[Number-Recipients]," + "[Origination-Time]," + "[Encryption]," + "[service-Version]," + "[Linked-MSGID]," + "[Message-Subject]," + "[Sender-Address]) " + " VALUES ( " + "@Senttime," + "@Clientip," + "@Clienthostname," + "@Partnername," + "@Serverhostname," + "@Serverip," + "@Recipientaddress," + "@Eventid," + "@Msgid," + "@Priority," + "@Recipientreportstatus," + "@Totalbytes," + "@Numberrecipient," + "@OriginationTime," + "@Encryption," + "@ServiceVersion," + "@LinkedMsgid," + "@MessageSubject," + "@SenderAddress)"; var cmd = new SqlCommand(cnnStr, cn) {CommandType = CommandType.Text}; cmd.Parameters.AddWithValue("@Senttime", dt.Senttime); cmd.Parameters.AddWithValue("@Clientip", dt.Clientip); cmd.Parameters.AddWithValue("@Clienthostname", dt.Clienthostname); cmd.Parameters.AddWithValue("@Partnername", dt.Partnername); cmd.Parameters.AddWithValue("@Serverhostname", dt.Serverhostname); cmd.Parameters.AddWithValue("@Serverip", dt.Serverip); cmd.Parameters.AddWithValue("@Recipientaddress", dt.Recipientaddress); cmd.Parameters.AddWithValue("@Eventid", dt.Eventid); cmd.Parameters.AddWithValue("@Msgid", dt.Msgid); cmd.Parameters.AddWithValue("@Priority", dt.Priority); cmd.Parameters.AddWithValue("@Recipientreportstatus", dt.Recipientreportstatus); cmd.Parameters.AddWithValue("@Totalbytes", dt.Totalbytes); cmd.Parameters.AddWithValue("@Numberrecipient", dt.Numberrecipient); if (dt.OriginationTime != null) cmd.Parameters.AddWithValue("@OriginationTime", dt.OriginationTime); else cmd.Parameters.AddWithValue("@OriginationTime", DBNull.Value); //if OriginationTime was null, then insert with null value to this column cmd.Parameters.AddWithValue("@Encryption", dt.Encryption); cmd.Parameters.AddWithValue("@ServiceVersion", dt.ServiceVersion); cmd.Parameters.AddWithValue("@LinkedMsgid", dt.LinkedMsgid); cmd.Parameters.AddWithValue("@MessageSubject", dt.MessageSubject); cmd.Parameters.AddWithValue("@SenderAddress", dt.SenderAddress); cmd.ExecuteNonQuery(); } 

我的程序如何运行得更快? 非常感谢!

使用SqlBulkCopy 。

编辑:我创建了IDataReader的最小实现并创建了一个Batch类型,以便我可以使用SqlBulkCopy插入任意内存数据。 这是重要的一点:

 IDataReader dr = batch.GetDataReader(); using (SqlTransaction tx = _connection.BeginTransaction()) { try { using (SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(_connection, SqlBulkCopyOptions.Default, tx)) { sqlBulkCopy.DestinationTableName = TableName; SetColumnMappings(sqlBulkCopy.ColumnMappings); sqlBulkCopy.WriteToServer(dr); tx.Commit(); } } catch { tx.Rollback(); throw; } } 

其余的实现留给读者:)

提示:您需要实现的IDataReader的唯一位是ReadGetValueFieldCount

嗯,让我们稍微打破一下。

在伪代码中你所做的是ff:

  1. 打开文件
    • 打开连接
    • 对于每个有数据的行:
    • 解析字符串
    • 将数据保存在SQL Server中
    • 关闭连接
    • 关闭文件

现在这样做的根本问题是:

  • 您在等待行解析时保持SQL连接处于打开状态(非常容易受到超时和事物的影响)
  • 可能会逐行保存数据,每个数据都在自己的事务中。 在您向我们展示InsertData方法正在执行的操作之前,我们不会知道
  • 因此,您在等待SQL完成插入时保持文件处于打开状态

执行此操作的最佳方法是将文件整体解析,然后批量插入它们。 您可以使用SqlBulkCopy (由Matt Howells建议)或SQL Server Integration Services执行此操作。

如果你想坚持使用ADO.NET,你可以将INSERT语句汇集在一起​​,然后将它们传递给一个大的SQLCommand,而不是这样做,例如,为每个insert语句设置一个SQLCommand对象。

您为每行数据创建SqlCommand对象。 因此,最简单的改进就是创造一个

 private static SqlCommand cmdInsert 

并使用Parameters.Add()方法声明参数。 然后,对于每个数据行,使用设置参数值

 cmdInsert.Parameters["@paramXXX"].Value = valueXXX; 

第二个性能改进可能是跳过为每一行创建Data对象,并直接从list []数组中分配Parameter值。