在.NET / C#中将大量记录(批量插入)写入Access

时间:2011-08-15 19:55:32

标签: c# ms-access dao bulkinsert

从.NET执行批量插入MS Access数据库的最佳方法是什么?使用ADO.NET,编写大型数据集需要花费一个多小时。

请注意,在我“重构”它之前,我的原始帖子在问题部分中都有问题和答案。我接受了Igor Turman的建议,并分两部分重写 - 上面的问题,然后是我的回答。

8 个答案:

答案 0 :(得分:70)

我发现以特定方式使用DAO大约比使用ADO.NET快30倍。我正在分享代码并得出这个答案。作为背景,在下面,测试是写出100列的20列记录。

技术和时间的总结 - 从最好到更差:

  1. 02.8秒:使用DAO,使用DAO.Field来引用表格列
  2. 02.8秒:写出文本文件,使用自动化将文本导入Access
  3. 11.0秒:使用DAO,使用列索引来引用表格列。
  4. 17.0秒:使用DAO,请参阅按名称列
  5. 79.0秒:使用ADO.NET,为每行生成INSERT语句
  6. 86.0秒:使用ADO.NET,将DataTable用于DataAdapter以进行“批量”插入
  7. 作为背景,偶尔我需要对相当大量的数据进行分析,我发现Access是最好的平台。分析涉及许多查询,通常还有很多VBA代码。

    由于各种原因,我想使用C#而不是VBA。典型的方法是使用OleDB连接到Access。我使用OleDbDataReader来获取数百万条记录,并且效果很好。但是当将结果输出到表格时,花了很长时间。一个多小时。

    首先,让我们讨论从C#向Access写入记录的两种典型方法。两种方式都涉及OleDB和ADO.NET。第一种是一次生成一个INSERT语句,并执行它们,为100 000条记录花费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中找不到允许批量插入的方法。

    我曾经想过,使用带有数据适配器的数据表可能会很有用。特别是因为我认为我可以使用数据适配器的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秒 - 并列第一名。但我认为这很脆弱有很多原因:输出日期字段很棘手。我必须专门格式化它们(someDate.ToString("yyyy-MM-dd HH:mm")),然后设置一个特殊的“导入规范”,以这种格式编码。导入规范还必须设置“quote”分隔符。在下面的示例中,仅使用整数字段,不需要导入规范。

    文本文件对于“国际化”也很脆弱,其中使用逗号表示小数分隔符,不同的日期格式,可能使用unicode。

    请注意,第一个记录包含字段名称,因此列顺序不依赖于表,并且我们使用Automation来实际导入文本文件。

    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的巨大警告。但是,事实证明它只是在Access和.NET之间进行交互的最佳方式,尤其是当您需要写出大量记录时。此外,它还允许访问表的所有属性。我在某处读到使用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;
    }
    

    在此代码中,我们为每列(myFields[k])创建了DAO.Field变量,然后使用它们。花了2.8秒。或者,可以直接访问在注释行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
    

答案 1 :(得分:11)

感谢Marc ,为了投票,我在StackOverFlow上创建了一个帐户......

以下是可重用的方法[在C#上测试64位 - Win 7,Windows 2008 R2,Vista,XP平台]

效果明细: 在4秒内输出120,000行。

复制以下代码并传递参数......并查看效果。

  • 只需使用相同的架构传递数据表,就像目标访问Db表一样。
  • DBPath =访问的完整路径Db
  • 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;
    }
}

答案 2 :(得分:2)

您可以使用KORM,对象关系映射器,允许对MsAccess进行批量操作。

database
  .Query<Movie>()
  .AsDbSet()
  .BulkInsert(_data);

或者如果您有源阅读器,则可以直接使用MsAccessBulkInsert类:

using (var bulkInsert = new MsAccessBulkInsert("connection string"))
{
   bulkInsert.Insert(sourceReader);
}

KORM可以从nuget Kros.KORM.MsAccess获得,它的开源位于GitHub

答案 3 :(得分:1)

感谢Marc的例子。
在我的系统上,DAO的性能不如这里建议的那样好:

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文件的原因,此方法也很脆弱。

希望这有帮助。
洛伦佐

答案 4 :(得分:0)

另一种需要考虑的方法,包括通过DAO或ADOX链接表,然后执行如下语句:

SELECT * INTO Table1 FROM _LINKED_Table1

请在此处查看我的完整答案:
MS Access Batch Update via ADO.Net and COM Interoperability

答案 5 :(得分:0)

首先确保访问表列具有相同的列名和相似的类型。然后你可以使用这个我认为非常快速和优雅的功能。

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);
}

答案 6 :(得分:0)

要添加到马克的答案中:

请注意,在Main方法上方具有[STAThread]属性。将使您的程序轻松能够与COM对象进行通信,从而进一步提高速度。我知道这并不适用于所有应用程序,但是如果您严重依赖DAO,我会推荐它。

此外,使用DAO插入方法。如果您有不需要的列,并且想插入null,甚至不要设置它的值。设置价值成本时间,即使它为空。

答案 7 :(得分:-1)

注意DAO组件here的位置。这有助于解释效率的提高。