在.NET / C#中编写大量的logging(批量插入)
从.NET中执行批量插入到MS Access数据库的最佳方法是什么? 使用ADO.NET,花费一个多小时才能写出一个大的数据集。
请注意,在我“重构”之前,我的原始文章在问题部分既有问题也有答案。 我采取了伊戈尔·特曼的build议,并分两部分重写 – 上面的问题和我的答案。
我发现以特定的方式使用DAO大约比使用ADO.NET快30倍。 我在这个答案分享代码和结果。 作为背景,在下面,testing是写出20个列的100000个表的logging。
技术和时间的总结 – 从最好到最差:
- 02.8秒:使用DAO,使用
DAO.Field
来引用表列 - 02.8秒:写出文本文件,使用自动化将文本导入到Access中
- 11.0秒:使用DAO,使用列索引来引用表列。
- 17.0秒:使用DAO,请参考名称栏
- 79.0秒:使用ADO.NET,为每一行生成INSERT语句
- 86.0秒:使用ADO.NET,将DataTable用于“批量”插入的DataAdapter
作为背景,偶尔我需要分析合理的大量数据,而且我发现Access是最好的平台。 分析涉及许多查询,并经常有很多VBA代码。
出于各种原因,我想用C#代替VBA。 典型的方法是使用OleDB连接到Access。 我使用OleDbDataReader
来抓取数百万条logging,并且工作得很好。 但是当把结果输出到表格时,花费了很长时间。 一个多小时。
首先,我们来讨论一下从C#编写logging到Access的两种典型方法。 这两种方式涉及OleDB和ADO.NET。 首先是每次生成一条INSERT语句,然后执行它们,对于100 000条logging需要79秒。 代码是:
public static double TestADONET_Insert_TransferToAccess() { StringBuilder names = new StringBuilder(); for (int k = 0; k < 20; k++) { string fieldName = "Field" + (k + 1).ToString(); if (k > 0) { names.Append(","); } names.Append(fieldName); } DateTime start = DateTime.Now; using (OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB)) { conn.Open(); OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "DELETE FROM TEMP"; int numRowsDeleted = cmd.ExecuteNonQuery(); Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted); for (int i = 0; i < 100000; i++) { StringBuilder insertSQL = new StringBuilder("INSERT INTO TEMP (") .Append(names) .Append(") VALUES ("); for (int k = 0; k < 19; k++) { insertSQL.Append(i + k).Append(","); } insertSQL.Append(i + 19).Append(")"); cmd.CommandText = insertSQL.ToString(); cmd.ExecuteNonQuery(); } cmd.Dispose(); } double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds; Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds); return elapsedTimeInSeconds; }
请注意,我发现Access中没有允许批量插入的方法。
然后,我曾经想过,也许使用数据表与数据适配器将被certificate是有用的。 特别是因为我认为我可以使用数据适配器的UpdateBatchSize
属性进行批量插入。 但是,显然只有SQL Server和Oracle支持,而Access不支持。 最长的时间是86秒。 我使用的代码是:
public static double TestADONET_DataTable_TransferToAccess() { StringBuilder names = new StringBuilder(); StringBuilder values = new StringBuilder(); DataTable dt = new DataTable("TEMP"); for (int k = 0; k < 20; k++) { string fieldName = "Field" + (k + 1).ToString(); dt.Columns.Add(fieldName, typeof(int)); if (k > 0) { names.Append(","); values.Append(","); } names.Append(fieldName); values.Append("@" + fieldName); } DateTime start = DateTime.Now; OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB); conn.Open(); OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "DELETE FROM TEMP"; int numRowsDeleted = cmd.ExecuteNonQuery(); Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted); OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM TEMP", conn); da.InsertCommand = new OleDbCommand("INSERT INTO TEMP (" + names.ToString() + ") VALUES (" + values.ToString() + ")"); for (int k = 0; k < 20; k++) { string fieldName = "Field" + (k + 1).ToString(); da.InsertCommand.Parameters.Add("@" + fieldName, OleDbType.Integer, 4, fieldName); } da.InsertCommand.UpdatedRowSource = UpdateRowSource.None; da.InsertCommand.Connection = conn; //da.UpdateBatchSize = 0; for (int i = 0; i < 100000; i++) { DataRow dr = dt.NewRow(); for (int k = 0; k < 20; k++) { dr["Field" + (k + 1).ToString()] = i + k; } dt.Rows.Add(dr); } da.Update(dt); conn.Close(); double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds; Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds); return elapsedTimeInSeconds; }
然后我尝试了非标准的方式。 首先,我写了一个文本文件,然后使用自动化导入。这很快 – 2.8秒 – 并列第一名。 但是我认为这种脆弱的原因有很多:输出date字段很棘手。 我不得不格式化它们( someDate.ToString("yyyy-MM-dd HH:mm")
),然后设置一个特殊的“导入规范”,以这种格式编码。 导入规范也必须设置“引号”分隔符。 在下面的示例中,只有整数字段,不需要导入规范。
对于“国际化”的文本文件也是脆弱的,因为对于小数点分隔符,不同的date格式,可能使用unicode。
请注意,第一个logging包含字段名称,以便列顺序不依赖于表,并且我们使用自动化来执行文本文件的实际导入。
public static double TestTextTransferToAccess() { StringBuilder names = new StringBuilder(); for (int k = 0; k < 20; k++) { string fieldName = "Field" + (k + 1).ToString(); if (k > 0) { names.Append(","); } names.Append(fieldName); } DateTime start = DateTime.Now; StreamWriter sw = new StreamWriter(Properties.Settings.Default.TEMPPathLocation); sw.WriteLine(names); for (int i = 0; i < 100000; i++) { for (int k = 0; k < 19; k++) { sw.Write(i + k); sw.Write(","); } sw.WriteLine(i + 19); } sw.Close(); ACCESS.Application accApplication = new ACCESS.Application(); string databaseName = Properties.Settings.Default.AccessDB .Split(new char[] { ';' }).First(s => s.StartsWith("Data Source=")).Substring(12); accApplication.OpenCurrentDatabase(databaseName, false, ""); accApplication.DoCmd.RunSQL("DELETE FROM TEMP"); accApplication.DoCmd.TransferText(TransferType: ACCESS.AcTextTransferType.acImportDelim, TableName: "TEMP", FileName: Properties.Settings.Default.TEMPPathLocation, HasFieldNames: true); accApplication.CloseCurrentDatabase(); accApplication.Quit(); accApplication = null; double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds; Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds); return elapsedTimeInSeconds; }
最后,我尝试了DAO。 那里有很多网站给出了关于使用DAO的巨大警告。 但事实certificate,这只是Access和.NET之间进行交互的最佳方式,特别是在需要写出大量logging的情况下。 而且,它可以访问表的所有属性。 我在某处读到,使用DAO而不是ADO.NET编程事务是最简单的。
请注意,有几行代码被评论。 他们将很快解释。
public static double TestDAOTransferToAccess() { string databaseName = Properties.Settings.Default.AccessDB .Split(new char[] { ';' }).First(s => s.StartsWith("Data Source=")).Substring(12); DateTime start = DateTime.Now; DAO.DBEngine dbEngine = new DAO.DBEngine(); DAO.Database db = dbEngine.OpenDatabase(databaseName); db.Execute("DELETE FROM TEMP"); DAO.Recordset rs = db.OpenRecordset("TEMP"); DAO.Field[] myFields = new DAO.Field[20]; for (int k = 0; k < 20; k++) myFields[k] = rs.Fields["Field" + (k + 1).ToString()]; //dbEngine.BeginTrans(); for (int i = 0; i < 100000; i++) { rs.AddNew(); for (int k = 0; k < 20; k++) { //rs.Fields[k].Value = i + k; myFields[k].Value = i + k; //rs.Fields["Field" + (k + 1).ToString()].Value = i + k; } rs.Update(); //if (0 == i % 5000) //{ //dbEngine.CommitTrans(); //dbEngine.BeginTrans(); //} } //dbEngine.CommitTrans(); rs.Close(); db.Close(); double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds; Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds); return elapsedTimeInSeconds; }
在这段代码中,我们为每一列创build了DAO.Fieldvariables( myFields[k]
),然后使用它们。 花了2.8秒。 或者,可以直接访问在注释行rs.Fields["Field" + (k + 1).ToString()].Value = i + k;
find的那些字段rs.Fields["Field" + (k + 1).ToString()].Value = i + k;
这增加了17秒的时间。 在一个事务中包装代码(参见注释行)将其丢弃到14秒。 使用整数索引rs.Fields[k].Value = i + k;
丢到了11秒。 使用DAO.Field( myFields[k]
)和事务实际上需要更长的时间,将时间增加到3.1秒。
最后,为了完整性,所有这些代码都在一个简单的静态类中,而using
语句是:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using ACCESS = Microsoft.Office.Interop.Access; // USED ONLY FOR THE TEXT FILE METHOD using DAO = Microsoft.Office.Interop.Access.Dao; // USED ONLY FOR THE DAO METHOD using System.Data; // USED ONLY FOR THE ADO.NET/DataTable METHOD using System.Data.OleDb; // USED FOR BOTH ADO.NET METHODS using System.IO; // USED ONLY FOR THE TEXT FILE METHOD
感谢马克 ,为了投票你我在StackOverFlow上创build一个帐户…
下面是可重复使用的方法[在64位Win 7,Windows 2008 R2,Vista,XP平台的C#上testing]
性能细节:在4秒内输出120,000行。
复制下面的代码并传递参数…并查看性能。
- 只需传递与目标Access Db Table相同的模式的数据表。
- DBPath =完整的访问pathDb
- TableNm =目标访问Db表的名称。
代码:
public void BulkExportToAccess(DataTable dtOutData, String DBPath, String TableNm) { DAO.DBEngine dbEngine = new DAO.DBEngine(); Boolean CheckFl = false; try { DAO.Database db = dbEngine.OpenDatabase(DBPath); DAO.Recordset AccesssRecordset = db.OpenRecordset(TableNm); DAO.Field[] AccesssFields = new DAO.Field[dtOutData.Columns.Count]; //Loop on each row of dtOutData for (Int32 rowCounter = 0; rowCounter < dtOutData.Rows.Count; rowCounter++) { AccesssRecordset.AddNew(); //Loop on column for (Int32 colCounter = 0; colCounter < dtOutData.Columns.Count; colCounter++) { // for the first time... setup the field name. if (!CheckFl) AccesssFields[colCounter] = AccesssRecordset.Fields[dtOutData.Columns[colCounter].ColumnName]; AccesssFields[colCounter].Value = dtOutData.Rows[rowCounter][colCounter]; } AccesssRecordset.Update(); CheckFl = true; } AccesssRecordset.Close(); db.Close(); } finally { System.Runtime.InteropServices.Marshal.ReleaseComObject(dbEngine); dbEngine = null; } }
另一种考虑的方法,涉及通过DAO或ADOX连接表,然后执行这样的语句:
SELECT * INTO Table1 FROM _LINKED_Table1
请在这里看到我的完整答案:
MS访问批量更新通过ADO.Net和COM互操作性
感谢Marc提供的例子。
在我的系统上,DAO的性能不如这里所build议的那么好:
TestADONET_Insert_TransferToAccess():68秒
TestDAOTransferToAccess():29秒
由于在我的系统上使用Office互操作库不是一个选项我尝试了一种涉及写入CSV文件,然后通过ADO导入它的新方法:
public static double TestADONET_Insert_FromCsv() { StringBuilder names = new StringBuilder(); for (int k = 0; k < 20; k++) { string fieldName = "Field" + (k + 1).ToString(); if (k > 0) { names.Append(","); } names.Append(fieldName); } DateTime start = DateTime.Now; StreamWriter sw = new StreamWriter("tmpdata.csv"); sw.WriteLine(names); for (int i = 0; i < 100000; i++) { for (int k = 0; k < 19; k++) { sw.Write(i + k); sw.Write(","); } sw.WriteLine(i + 19); } sw.Close(); using (OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB)) { conn.Open(); OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "DELETE FROM TEMP"; int numRowsDeleted = cmd.ExecuteNonQuery(); Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted); StringBuilder insertSQL = new StringBuilder("INSERT INTO TEMP (") .Append(names) .Append(") SELECT ") .Append(names) .Append(@" FROM [Text;Database=.;HDR=yes].[tmpdata.csv]"); cmd.CommandText = insertSQL.ToString(); cmd.ExecuteNonQuery(); cmd.Dispose(); } double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds; Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds); return elapsedTimeInSeconds; }
TestADONET_Insert_FromCsv()的性能分析:1.9秒
与Marc的示例TestTextTransferToAccess()类似,由于许多有关使用CSV文件的原因,此方法也很脆弱。
希望这可以帮助。
洛伦佐
首先确保访问表列具有相同的列名称和类似的types。 那么你可以使用这个function,我相信是非常快速和优雅。
public void AccessBulkCopy(DataTable table) { foreach (DataRow r in table.Rows) r.SetAdded(); var myAdapter = new OleDbDataAdapter("SELECT * FROM " + table.TableName, _myAccessConn); var cbr = new OleDbCommandBuilder(myAdapter); cbr.QuotePrefix = "["; cbr.QuoteSuffix = "]"; cbr.GetInsertCommand(true); myAdapter.Update(table); }