web-dev-qa-db-ja.com

MySQLデータベースに行を挿入する最も効率的な方法

私はそれについて多くの質問を読みましたが、十分に速いものを見つけることができませんでした。 MySQLデータベースに多くの行を挿入するより良い方法があると思います

次のコードを使用して、MySQLデータベースに100kを挿入します。

public static void CSVToMySQL()
{
    string ConnectionString = "server=192.168.1xxx";
    string Command = "INSERT INTO User (FirstName, LastName ) VALUES (@FirstName, @LastName);";
    using (MySqlConnection mConnection = new MySqlConnection(ConnectionString))
    {
        mConnection.Open();

        for(int i =0;i< 100000;i++) //inserting 100k items
        using (MySqlCommand myCmd = new MySqlCommand(Command, mConnection))
        {
            myCmd.CommandType = CommandType.Text;
            myCmd.Parameters.AddWithValue("@FirstName", "test");
            myCmd.Parameters.AddWithValue("@LastName", "test");
            myCmd.ExecuteNonQuery();
        }
    }
}

これには、100,000行で約40秒かかります。これをより速く、または少し効率的にするにはどうすればよいですか?

DataTable/DataAdapterを介して、または一度に複数の行を挿入する方が高速になる場合があります。

INSERT INTO User (Fn, Ln) VALUES (@Fn1, @Ln1), (@Fn2, @Ln2)...

セキュリティの問題により、データをファイルにロードしてMySQLBulkLoadすることができません。

39
fubo

これが私の「複数挿入」コードです。

10万行の挿入には40秒しかかかりませんでした !!

public static void BulkToMySQL()
{
    string ConnectionString = "server=192.168.1xxx";
    StringBuilder sCommand = new StringBuilder("INSERT INTO User (FirstName, LastName) VALUES ");           
    using (MySqlConnection mConnection = new MySqlConnection(ConnectionString))
    {
        List<string> Rows = new List<string>();
        for (int i = 0; i < 100000; i++)
        {
            Rows.Add(string.Format("('{0}','{1}')", MySqlHelper.EscapeString("test"), MySqlHelper.EscapeString("test")));
        }
        sCommand.Append(string.Join(",", Rows));
        sCommand.Append(";");
        mConnection.Open();
        using (MySqlCommand myCmd = new MySqlCommand(sCommand.ToString(), mConnection))
        {
            myCmd.CommandType = CommandType.Text;
            myCmd.ExecuteNonQuery();
        }
    }
}

作成されたSQLステートメントは次のようになります。

INSERT INTO User (FirstName, LastName) VALUES ('test','test'),('test','test'),... ;

更新:ありがとうSalman A追加MySQLHelper.EscapeStringパラメータを使用するときに内部的に使用されるコードインジェクションを回避します。

50
fubo

MySqlDataAdapter、transactions、UpdateBatchSizeの3つのことを使用して小さなテストを行いました。最初の例よりも約30倍高速です。 Mysqlは別のボックスで実行されているため、遅延が発生します。バッチサイズには調整が必要な場合があります。コードは次のとおりです。

string ConnectionString = "server=xxx;Uid=xxx;Pwd=xxx;Database=xxx";

string Command = "INSERT INTO User2 (FirstName, LastName ) VALUES (@FirstName, @LastName);";


 using (var mConnection = new MySqlConnection(ConnectionString))
     {
         mConnection.Open();
         MySqlTransaction transaction = mConnection.BeginTransaction();

        //Obtain a dataset, obviously a "select *" is not the best way...
        var mySqlDataAdapterSelect = new MySqlDataAdapter("select * from User2", mConnection);

        var ds = new DataSet();

        mySqlDataAdapterSelect.Fill(ds, "User2");


        var mySqlDataAdapter = new MySqlDataAdapter();

        mySqlDataAdapter.InsertCommand = new MySqlCommand(Command, mConnection);


        mySqlDataAdapter.InsertCommand.Parameters.Add("@FirstName", MySqlDbType.VarChar, 32, "FirstName");
        mySqlDataAdapter.InsertCommand.Parameters.Add("@LastName", MySqlDbType.VarChar, 32, "LastName");
        mySqlDataAdapter.InsertCommand.UpdatedRowSource = UpdateRowSource.None;

        var stopwatch = new Stopwatch();
        stopwatch.Start();

        for (int i = 0; i < 50000; i++)
        {
            DataRow row = ds.Tables["User2"].NewRow();
            row["FirstName"] = "1234";
            row["LastName"] = "1234";
            ds.Tables["User2"].Rows.Add(row);
        }

         mySqlDataAdapter.UpdateBatchSize = 100;
         mySqlDataAdapter.Update(ds, "User2");

         transaction.Commit();

         stopwatch.Stop();
         Debug.WriteLine(" inserts took " + stopwatch.ElapsedMilliseconds + "ms");
    }
}
11
Konstantin

Transactionでコマンドを実行し、各反復でコマンドの同じインスタンスを再利用します。パフォーマンスをさらに最適化するには、1つのコマンドで100個のクエリを送信します。並列実行を行うと、パフォーマンスが向上する可能性があります(Parallel.For)ただし、各並列ループが独自のMySqlCommandインスタンスを取得するようにしてください。

public static void CSVToMySQL()
{
    string ConnectionString = "server=192.168.1xxx";
    string Command = "INSERT INTO User (FirstName, LastName ) VALUES (@FirstName, @LastName);";
    using (MySqlConnection mConnection = new MySqlConnection(ConnectionString)) 
    {
        mConnection.Open();
        using (MySqlTransaction trans = mConnection.BeginTransaction()) 
        {
            using (MySqlCommand myCmd = new MySqlCommand(Command, mConnection, trans)) 
            {
                myCmd.CommandType = CommandType.Text;
                for (int i = 0; i <= 99999; i++) 
                {
                    //inserting 100k items
                    myCmd.Parameters.Clear();
                    myCmd.Parameters.AddWithValue("@FirstName", "test");
                    myCmd.Parameters.AddWithValue("@LastName", "test");
                    myCmd.ExecuteNonQuery();
                }
                trans.Commit();
            }
        }
    }
}
10
Sarvesh Mishra

AddAddWithValueが文字列をエスケープしない場合、SQLインジェクションと構文エラーを回避するために、事前にそのようにする必要があります。

一度に1000行だけでINSERTステートメントを作成します。これは、開始時の10倍の速さで簡単に実行できます(INSERTごとに1行)。一度に100Kをすべて実行するのは危険であり、場合によっては遅くなります。制限(パケットサイズなど)を破る可能性があるため、危険です。巨大なROLLBACKログが必要なため、遅くなります。 COMMIT各バッチの後に、またはautocommit=1を使用します。

7
Rick James

この方法は、stringbuilderアプローチより高速ではないかもしれませんが、パラメータ化されています:

/// <summary>
    /// Bulk insert some data, uses parameters
    /// </summary>
    /// <param name="table">The Table Name</param>
    /// <param name="inserts">Holds list of data to insert</param>
    /// <param name="batchSize">executes the insert after batch lines</param>
    /// <param name="progress">Progress reporting</param>
    public void BulkInsert(string table, MySQLBulkInsertData inserts, int batchSize = 100, IProgress<double> progress = null)
    {
        if (inserts.Count <= 0) throw new ArgumentException("Nothing to Insert");

        string insertcmd = string.Format("INSERT INTO `{0}` ({1}) VALUES ", table,
                                         inserts.Fields.Select(p => p.FieldName).ToCSV());
        StringBuilder sb = new StringBuilder(); 
        using (MySqlConnection conn = new MySqlConnection(ConnectionString))
        using (MySqlCommand sqlExecCommand = conn.CreateCommand())
        {
            conn.Open();
            sb.AppendLine(insertcmd);
            for (int i = 0; i < inserts.Count; i++)
            {
                sb.AppendLine(ToParameterCSV(inserts.Fields, i));
                for (int j = 0; j < inserts[i].Count(); j++)
                {
                    sqlExecCommand.Parameters.AddWithValue(string.Format("{0}{1}",inserts.Fields[j].FieldName,i), inserts[i][j]);
                }
                //commit if we are on the batch sizeor the last item
                if (i > 0 && (i%batchSize == 0 || i == inserts.Count - 1))
                {
                    sb.Append(";");
                    sqlExecCommand.CommandText = sb.ToString();
                    sqlExecCommand.ExecuteNonQuery();
                    //reset the stringBuilder
                    sb.Clear();
                    sb.AppendLine(insertcmd);
                    if (progress != null)
                    {
                        progress.Report((double)i/inserts.Count);
                    }
                }
                else
                {
                    sb.Append(",");
                }
            }
        }
    }

これは、以下のヘルパークラスを使用します。

/// <summary>
/// Helper class to builk insert data into a table
/// </summary>
public struct MySQLFieldDefinition
{
    public MySQLFieldDefinition(string field, MySqlDbType type) : this()
    {
        FieldName = field;
        ParameterType = type;
    }

    public string FieldName { get; private set; }
    public MySqlDbType ParameterType { get; private set; }
}

///
///You need to ensure the fieldnames are in the same order as the object[] array
///
public class MySQLBulkInsertData : List<object[]>
{
    public MySQLBulkInsertData(params MySQLFieldDefinition[] fieldnames)
    {
        Fields = fieldnames;
    }

    public MySQLFieldDefinition[] Fields { get; private set; }
}

そして、このヘルパーメソッド:

    /// <summary>
    /// Return a CSV string of the values in the list
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    private string ToParameterCSV(IEnumerable<MySQLFieldDefinition> p, int row)
    {
        string csv = p.Aggregate(string.Empty,
            (current, i) => string.IsNullOrEmpty(current)
                    ? string.Format("@{0}{1}",i.FieldName, row)
                    : string.Format("{0},@{2}{1}", current, row, i.FieldName));
        return string.Format("({0})", csv);
    }

超エレガントではないかもしれませんが、うまく機能します。進行状況の追跡が必要なため、含まれていますが、その部分を削除してください。

これにより、目的の出力に似たSQLコマンドが生成されます。

編集:ToCSV:

        /// <summary>
    /// Return a CSV string of the values in the list
    /// </summary>
    /// <param name="intValues"></param>
    /// <param name="separator"></param>
    /// <param name="encloser"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static string ToCSV<T>(this IEnumerable<T> intValues, string separator = ",", string encloser = "")
    {
        string result = String.Empty;
        foreach (T value in intValues)
        {
            result = String.IsNullOrEmpty(result)
                ? string.Format("{1}{0}{1}", value, encloser)
                : String.Format("{0}{1}{3}{2}{3}", result, separator, value, encloser);
        }
        return result;
    }
6
Simon

高速化する1つの方法は、すべての挿入を1つのトランザクションにラップすることです(SQLサーバーコード)。

using (SqlConnection connection = new SqlConnection(CloudConfigurationManager.GetSetting("Sql.ConnectionString")))
{
    conn.Open();
    SqlTransaction transaction = conn.BeginTransaction();

    try 
    {  
        foreach (string commandString in dbOperations)
        {
            SqlCommand cmd = new SqlCommand(commandString, conn, transaction);
            cmd.ExecuteNonQuery();
        }
        transaction.Commit(); 
    } // Here the execution is committed to the DB
    catch (Exception)
    {
      transaction.Rollback();
      throw;
    }
    conn.Close();
}

別の方法は、CSVファイルをデータテーブルにロードし、DataAdapterのバッチ機能を使用することです

 DataTable dtInsertRows = GetDataTable(); 

    SqlConnection connection = new SqlConnection(connectionString);
    SqlCommand command = new SqlCommand("sp_BatchInsert", connection);
    command.CommandType = CommandType.StoredProcedure;
    command.UpdatedRowSource = UpdateRowSource.None;

    // Set the Parameter with appropriate Source Column Name
    command.Parameters.Add("@PersonId", SqlDbType.Int, 4, dtInsertRows.Columns[0].ColumnName);   
    command.Parameters.Add("@PersonName", SqlDbType.VarChar, 100, dtInsertRows.Columns[1].ColumnName);

    SqlDataAdapter adpt = new SqlDataAdapter();
    adpt.InsertCommand = command;
    // Specify the number of records to be Inserted/Updated in one go. Default is 1.
    adpt.UpdateBatchSize = 2;

    connection.Open();
    int recordsInserted = adpt.Update(dtInsertRows);   
    connection.Close();

素敵な例があります here

または、MySQL BulkLoader C#クラスを使用できます。

var bl = new MySqlBulkLoader(connection);
bl.TableName = "mytable";
bl.FieldTerminator = ",";
bl.LineTerminator = "\r\n";
bl.FileName = "myfileformytable.csv";
bl.NumberOfLinesToSkip = 1;
var inserted = bl.Load();
Debug.Print(inserted + " rows inserted.");

1つのコマンドで複数の挿入を行う場合、文字列の代わりにStringBuilderを使用して、1〜2インチ押し出すことができます。

5
Stefan Steiger

Stefan Steigerが言うように、 一括挿入 はあなたの状況に適しています。

もう1つのトリックはステージングテーブルを使用することです。そのため、プロダクションテーブルに直接書き込む代わりに、ステージングテーブル(同じ構造を持つ)に書き込みます。すべての情報を書いたら、テーブルを交換するだけです。ステージング手法を使用すると、挿入のためにテーブルをロックすることを回避できます(更新および削除にも使用できます)。このパターンは、一部のプロジェクトでMySQLで頻繁に使用されます。

また、テーブルキーを無効にすると挿入が高速化されますが、それらを有効にすると問題が発生する可能性があります(MyISAMエンジンのみ)。

追加

テーブルProductsがあるとしましょう:

  • ProductId
  • ProductName
  • ProductPrice

ステージングのために、ProductsStagingというステージングテーブルを作成し、同じ列セットを使用します。

ステージングテーブルで行うすべての操作:

_UpdateStagingTable();
SwapTables();
UpdateStagingTable();
_

スワップ後、ステージングテーブルに新しいデータがないため、同じメソッドをもう一度呼び出します。 SwapTables()メソッドでは、1つのSQLステートメントを実行します。

_RENAME TABLE Products TO ProductsTemp,
             ProductsStaging TO Products,
             ProductsTemp TO ProductsStagin;
_

データ操作の速度はMySqlエンジン(InnoDB、MyISAMなど)に依存するため、エンジンを変更して挿入を高速化することもできます。

4
Alex Sikilinda

EFでの作業中に、MySQLという同様の問題に遭遇しました。 EFの挿入は非常に遅いため、 fubo で言及されているアプローチを使用しました。まず、パフォーマンスは大幅に向上しました(〜10秒で〜20Kレコードが挿入されました)が、テーブルのサイズが大きくなるにつれて低下し、テーブルに〜1Mレコードが挿入されると、〜250秒かかりました。

最後に問題を見つけました!テーブルのPKは、タイプGUID(UUID-char(36))。UUIDは連続してインデックスを作成できず、すべての挿入でインデックスの再構築が必要であったため、速度が低下しました。

修正は、PKをbigint(またはint)に置き換え、ID列として設定することでした。これにより、パフォーマンスが向上しました。テーブルに〜2M +個のレコードがある場合、挿入には平均で約12秒かかりました。

誰かが同様の問題に巻き込まれた場合に備えて、ここでこの発見を共有したいと思います!

2
ashin

私の提案はアイデアであり、例や解決策ではありません。 INSERTを使用せず、複数のパラメーターとしてデータを渡して(一度にすべて100Kである必要はありません。たとえば1Kのバンドルを使用できます)、それ自体がINSERTを実行するSTORED PROCEDUREに渡した場合はどうなりますか。

1
Dzianis Yafimau

一括挿入にファイルを使用しないようにする方法を見つけました。 このコネクタ では、実装者がストリームからロードしていました。したがって、ロードはそのようなことを行うことができます

  public void InsertData(string table, List<string> columns, List<List<object>> data) {

  using (var con = OpenConnection() as MySqlConnection) {
    var bulk = new MySqlBulkLoader(con);
    using (var stream = new MemoryStream()) {
      bulk.SourceStream = stream;
      bulk.TableName = table;
      bulk.FieldTerminator = ";";
      var writer = new StreamWriter(stream);

      foreach (var d in data)
        writer.WriteLine(string.Join(";", d));

      writer.Flush();
      stream.Position = 0;
      bulk.Load();
    }
  }
}
0
vik_78

バルク操作は、それを導く良い方法です。プロパティを読み取って、一括クエリを作成するもの...

MySqlとEF6 +を使用したBulkInsertとBulkUpdateの両方の便利なメソッドを含むgithubリポジトリがあります。

BulkUpdate/BulkInsertは基本的に、ジェネリックエンティティからすべてのプロパティを読み取り、bulkqueryを作成します。

追伸:これは私のニーズに合わせて開発されたものであり、プロジェクトはコミュニティにとって価値のあるより良いソリューションのためにそれを改善したり変更したりすることを懸念する人に開かれています。

Ps²:問題が解決しない場合は、プロジェクトに変更を加えて、必要なものを改善し、達成してください。少なくとも良いスタートです。

こちら をご覧ください

0
Bruno Henrique