web-dev-qa-db-ja.com

.NET / C#でAccessに大量のレコードを書き込む(一括挿入)

.NETからMS Accessデータベースに一括挿入を実行する最良の方法は何ですか? ADO.NETを使用すると、大規模なデータセットを書き出すのに1時間以上かかります。

「リファクタリング」する前の元の投稿では、質問部分に質問と回答の両方が含まれていたことに注意してください。 。

48
Marc Meketon

特定の方法でDAOを使用すると、ADO.NETを使用するよりも約30倍高速であることがわかりました。この回答でコードと結果を共有しています。背景として、以下では、テストは20列のテーブルの100 000レコードを書き出すことです。

テクニックと時間の概要-最良のものからさらに悪いものまで:

  1. 2.8秒: DAOを使用し、_DAO.Field_を使用してテーブル列を参照します
  2. 2.8秒:テキストファイルに書き込み、オートメーションを使用してテキストをAccessにインポートします
  3. 11.0秒: DAOを使用し、列インデックスを使用してテーブル列を参照します。
  4. 17.0秒: DAOを使用し、名前で列を参照します
  5. 79.0秒: ADO.NETを使用し、各行のINSERTステートメントを生成します
  6. 86.0秒: ADO.NETを使用し、DataTableを使用して「バッチ」挿入用のDataAdapterを使用します

背景として、適度に大量のデータの分析を実行する必要がある場合があり、Accessが最適なプラットフォームであることがわかりました。分析には多くのクエリが含まれ、多くの場合、多くのVBAコードが含まれます。

さまざまな理由から、VBAの代わりにC#を使用したいと考えました。典型的な方法は、OleDBを使用してAccessに接続することです。 OleDbDataReaderを使用して何百万ものレコードを取得しましたが、非常にうまく機能しました。しかし、結果をテーブルに出力するときは、長い時間がかかりました。 1時間以上。

最初に、C#からAccessにレコードを書き込む2つの典型的な方法について説明します。どちらの方法にも、OleDBとADO.NETが関係しています。 1つ目は、INSERTステートメントを1つずつ生成して実行し、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秒)、1位でした。しかし、私はいくつかの理由でこの脆弱性を考慮しています:日付フィールドの出力には注意が必要です。特別にフォーマット(someDate.ToString("yyyy-MM-dd HH:mm"))してから、このフォーマットでコーディングする特別な「インポート仕様」をセットアップする必要がありました。また、インポート仕様では、「引用符」区切り文字を正しく設定する必要がありました。以下の例では、整数フィールドのみで、インポート仕様は必要ありませんでした。

また、テキストファイルは、 "国際化"に対して脆弱です。この場合、小数点記号にコンマを使用し、異なる日付形式を使用し、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の間でやり取りするのに最適な方法であることがわかります。また、テーブルのすべてのプロパティにアクセスできます。私はどこかで、ADO.NETの代わりにDAOを使用してトランザクションをプログラムするのが最も簡単だと読みました。

コメントされるコードの行がいくつかあることに注意してください。それらはすぐに説明されます。

_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
_
70
Marc Meketon

Thanks Marc、投票するために、StackOverFlowでアカウントを作成しました...

以下は再利用可能な方法です[64ビットのC#でテスト-Win 7、Windows 2008 R2、Vista、XPプラットフォーム]

パフォーマンスの詳細: 4秒で120,000行をエクスポートします。

以下のコードをコピーしてパラメーターを渡し...、パフォーマンスを確認します。

  • ターゲットのAccess Db Tableと同じスキーマでデータテーブルを渡すだけです。
  • 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;
    }
}
11
Prasoon Pathak

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 のオープンソースです

2
Mino

例を挙げてくれてありがとう。
私のシステムでは、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ファイルの使用に関するいくつかの理由で脆弱です。

お役に立てれば。
ロレンソ

1
LorenzoB

最初に、アクセステーブルの列の列名とタイプが同じであることを確認してください。次に、この機能を使用できます。この機能は非常に高速でエレガントです。

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
0014

DAOまたはADOXを介してテーブルをリンクし、次のようなステートメントを実行することを含む、考慮する別の方法:

SELECT * INTO Table1 FROM _LINKED_Table1

ここで私の完全な答えをご覧ください:
ADO.NetおよびCOM相互運用性を介したMS Accessバッチ更新

0
Ruutsa

マークの答えに追加するには:

Mainメソッドの上に[STAThread]属性があることに注意してください。プログラムがCOMオブジェクトと簡単に通信できるようになり、速度がさらに向上します。すべてのアプリケーションに対応しているわけではありませんが、DAOに大きく依存している場合はお勧めします。

さらに、DAO挿入メソッドを使用します。不要な列があり、nullを挿入する場合は、その値を設定しないでください。値を設定すると、nullであっても時間がかかります。

0
Bart de Bever