当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《在.NET/C#中将大量记录(批量插入)写入Access》经验,为你挑选了2个好方法。

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

请注意,在我"重构"它之前,我的原始帖子在问题部分中都有问题和答案.我采取了Igor Turman的建议,并将其重新分为两部分 - 上面的问题,然后是我的答案.



1> Marc Meketon..:

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

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

    02.8秒:使用DAO,使用DAO.Field's来引用表列

    02.8秒:写出文本文件,使用Automation将文本导入Access

    11.0秒:使用DAO,使用列索引来引用表列.

    17.0秒:使用DAO,按名称参阅列

    79.0秒:使用ADO.NET,为每一行生成INSERT语句

    86.0秒:使用ADO.NET,将DataTable用于DataAdapter以进行"批量"插入

作为背景,偶尔我需要对相当大量的数据进行分析,我发现Access是最好的平台.分析涉及许多查询,并且通常包含大量VBA代码.

由于各种原因,我想使用C#而不是VBA.典型的方法是使用OleDB连接到Access.我用a 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


.net确实需要为db表添加"表直接"访问方法.批量装载太痛苦了,不应该.
非常棒的答案!
我知道这是一篇非常古老的帖子,但我想说谢谢.我写了400,000条记录,耗时近1.5小时,现在几分钟就完成了.

2> 小智..:

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


我编辑了你的代码并删除了"catch-exception-throw-ex"行.除非你意识到后果,否则尽量不要在C#中这样做.以这种方式重新抛出异常会从中删除原始堆栈跟踪.要正确地重新抛出,请使用`throw;`而不是`throw ex;`.catch子句也是空的,所以除了损坏stacktrace之外它什么都没做.
推荐阅读
mobiledu2402852357
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有