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

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

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

Ответы [ 8 ]

70 голосов
/ 16 августа 2011

Я обнаружил, что использование 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, чтобы получить миллионы записей, и это работало довольно хорошо.Но при выводе результатов в таблицу это заняло много времени.Более часа.

Сначала давайте обсудим два типичных способа записи записей в Access из C #.Оба способа связаны с 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;
}

Затем я попробовал нестандартные способы.Сначала я записал в текстовый файл, а затем использовал Automation для его импорта. Это было быстро - 2,8 секунды - и привязано к первому месту.Но я считаю это хрупким по ряду причин: Выводить поля даты сложно.Мне пришлось специально отформатировать их (someDate.ToString("yyyy-MM-dd HH:mm")), а затем настроить специальную «спецификацию импорта», которая кодирует в этом формате.В спецификации импорта также должен быть установлен правильный разделитель «кавычек».В приведенном ниже примере, только с целочисленными полями, в спецификации импорта не было необходимости.

Текстовые файлы также хрупки для "интернационализации", где используются запятые для десятичных разделителей, возможны разные форматы датыиспользование 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;
}

В этом коде мы создали переменные DAO.Field для каждого столбца (myFields[k]) и затем использовали их. Это заняло 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
11 голосов
/ 22 августа 2012

Спасибо, Марк , чтобы проголосовать, я создал аккаунт на StackOverFlow ...

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

Информация о производительности: Экспортирует 120000 строк за 4 секунды.

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

  • Просто передайте вашу таблицу данных с той же схемой, что и в целевой таблице доступа к базе данных.
  • 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;
    }
}
2 голосов
/ 29 апреля 2018

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

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

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

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

KORM доступен из nuget Kros.KORM.MsAccess и с открытым исходным кодом на GitHub

1 голос
/ 29 июля 2015

Спасибо Марк за примеры.В моей системе производительность 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 секунды

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

Надеюсь, это поможет.Lorenzo

0 голосов
/ 21 декабря 2018

Добавить к ответу Марка:

Обратите внимание, что атрибут [STAThread] указан выше вашего метода Main. сделает вашу программу легко способной взаимодействовать с COM-объектами, увеличивая скорость в дальнейшем. Я знаю, что это не для каждого приложения, но если вы сильно зависите от DAO, я бы порекомендовал это.

Далее, используя метод вставки DAO. Если у вас есть столбец, который не требуется, и вы хотите вставить ноль, даже не устанавливайте его значение. Установка значения стоимости времени, даже если оно равно нулю.

0 голосов
/ 07 декабря 2017

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

0 голосов
/ 07 сентября 2016

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

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);
}
0 голосов
/ 10 октября 2014

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

SELECT * INTO Table1 FROM _LINKED_Table1

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

...