Запись большого количества записей (массовая вставка) для доступа in.NET/C#

каков наилучший способ выполнения массовых вставок в базу данных MS Access из .NET? Использование ADO.NET, это занимает более часа, чтобы написать большой набор данных.

обратите внимание, что мой оригинальный пост, прежде чем я "перестроить" его, как вопрос и ответ на вопрос части. Я принял предложение Игоря Турмана и переписал его в две части-вопрос выше и последующий ответ.

7 ответов


я обнаружил, что использование DAO определенным образом примерно в 30 раз быстрее, чем использование ADO.NET - ... Я делюсь кодом и результатами в этом ответе. В качестве фона, ниже, тест должен записать 100 000 записей таблицы с 20 столбцами.

краткое изложение техники и времени - от лучшего к худшему:

  1. 02.8 секунд: использовать DAO, использовать DAO.Field ' ы, чтобы обратиться к столбцам таблицы
  2. 02.8 секунд: Запишите в текстовый файл, используйте Automation для импорта текста в Access
  3. 11,0 секунд: используйте DAO, используйте индекс столбца для ссылки на столбцы таблицы.
  4. 17.0 секунд: используйте DAO, обратитесь к столбцу по имени
  5. 79.0 секунд: использовать ADO.NET, генерировать инструкции INSERT для каждой строки
  6. 86.0 секунд: использовать ADO.NET, используйте DataTable для DataAdapter для " пакета" вставить

в качестве фона, иногда мне нужно выполнить анализ достаточно больших объемов данных, и я считаю, что Access-лучшая платформа. Анализ включает в себя много запросов, и часто много кода VBA.

по разным причинам я хотел использовать C# вместо VBA. Типичный способ-использовать OleDB для подключения к Access. Я использовал OleDbDataReader чтобы захватить миллионы записей, и это сработало довольно хорошо. Но при выводе результатов в таблицу, потребовалось очень, очень давно. Больше часа.

во-первых, давайте обсудим два типичных способа записи записей для доступа с C#. Оба пути включают OleDB и ADO.NET - ... Первый-генерировать операторы INSERT по одному за раз и выполнять их, занимая 79 секунд для 100 000 записей. Код:

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 секунды-и привязано к первому месту. Но я считаю это хрупким для количество причин: Outputing поля даты сложно. Мне пришлось отформатировать их специально (someDate.ToString("yyyy-MM-dd HH:mm")), а затем настройте специальную "спецификацию импорта", которая кодирует в этом формате. В спецификации импорта также должен был быть установлен разделитель" quote". В приведенном ниже примере с целочисленными полями не было необходимости в спецификации импорта.

текстовые файлы также хрупки для "интернационализации", где используется запятая для десятичных разделителей, различных форматов дат, возможно использование unicode.

обратите внимание, что первая запись содержит имена полей, чтобы порядок столбцов не зависел от таблицы, и что мы использовали автоматизацию для фактического импорта текстового файла.

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

в этом коде мы создали DAO.Переменные поля для каждого столбца (myFields[k]), а затем использовали их. Потребовалось 2,8 секунды. Кроме того, можно получить прямой доступ к этим полям, как указано в строке комментариев rs.Fields["Field" + (k + 1).ToString()].Value = i + k; что увеличило время до 17 секунд. Упаковка кода в транзакцию (см. комментируемые строки) сократила это до 14 секунд. Использование целочисленного индекса rs.Fields[k].Value = i + k; опустил это до 11 секунд. Использование DAO.Поле (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...

Ниже приведен многоразовый метод [протестирован на C# с 64-битными платформами Win 7, Windows 2008 R2, Vista, XP]

Подробные Характеристики: Экспорт 120 000 строк за 4 секунды.

скопируйте приведенный ниже код и передачи параметров... и посмотреть представление.

  • просто передайте свой datatable с той же схемой, что и в целевой БД доступа Таблица.
  • DBPath= полный путь доступа к Db
  • TableNm = имя целевой таблицы БД доступа.

код:

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

вы можете использовать KORM, сопоставитель отношений объектов, который позволяет выполнять массовые операции над MsAccess.

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

или если у вас есть источник читатель, вы можете напрямую использовать MsAccessBulkInsert класс:

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

KORM доступен из nuget Крос.Корм.MsAccess и это opensource на GitHub


спасибо Марку за примеры.
В моей системе производительность DAO не так хороша, как предлагается здесь:

TestADONET_Insert_TransferToAccess(): 68 секунд
TestDAOTransferToAccess (): 29 секунд

поскольку в моей системе использование библиотек Office interop не является вариантом, я попробовал новый метод, включающий запись 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;
    }

анализ Performace TestADONET_Insert_FromCsv(): 1.9 секунды

подобно примеру Марка TestTextTransferToAccess (), этот метод также является хрупким по ряду причин, связанных с использованием CSV-файлов.

надеюсь, что это помогает.
Лоренцо!--4-->


другой метод для рассмотрения, включающий связывание таблиц через DAO или ADOX, а затем выполнение таких операторов:

SELECT * INTO Table1 FROM _LINKED_Table1

пожалуйста, см. мой полный ответ здесь:
обновление пакета доступа MS через ADO.Net и COM-совместимость


сначала убедитесь, что столбцы таблицы имеют одинаковые имена столбцов и аналогичные виды. Затем вы можете использовать эту функцию, которая, я считаю, очень быстрая и элегантная.

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

обратите внимание на положение компонента DAO здесь. Это помогает объяснить повышение эффективности.