将行插入MySQL数据库的最有效方法

时间:2014-08-15 08:41:11

标签: c# mysql performance

我已经阅读了很多关于此的问题,但我找不到足够快的问题。我认为有更好的方法可以将大量行插入MySQL数据库

我使用以下代码将100k插入我的MySQL数据库:

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

这需要100k行约40秒。我怎样才能更快或更高效?

通过DataTable / DataAdapter或一次插入多行可能会更快:

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

由于安全问题,我无法将数据加载到文件中而MySQLBulk会加载它。

11 个答案:

答案 0 :(得分:46)

这是我的&#34;多个插入&#34; -code。

插入100k行而不是40秒仅 3秒 !!

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以避免在使用参数时内部使用的代码注入。

答案 1 :(得分:10)

我使用MySqlDataAdapter,transactions和UpdateBatchSize这三个东西做了一个小测试。它比你的第一个例子快约30倍。 Mysql在单独的盒子上运行,因此涉及延迟。 batchsize可能需要一些调整。代码如下:

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

答案 2 :(得分:10)

Transaction中执行命令,并为每次迭代重用相同的命令实例。要进一步优化性能,请在一个命令中发送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();
            }
        }
    }
}

答案 3 :(得分:7)

如果public function devUserCreateTest() { DB::beginTransaction(); DB::connection('some_connection')->beginTransaction(); // same as the one used in model ChildUser try { $childUser = new ChildUser; // Exists in database B $parentUser = new User; // Exists in database A $parentUser->setEmailAttribute('mike@example.com'); $parentUser->save(); $childUser->parent_user_id = $parentUser->id; $message = sprintf('Parent user id: %s', serialize($childUser->id)); $childUser->save(); $message = sprintf('Core user id: %s | hta user id: %s', serialize($parentUser->id), serialize($childUser->id)); throw new Exception('Testing....'); DB::commit(); } catch (Exception $e) { Log::warning(sprintf('Exception: %s', $e->getMessage())); DB::rollback(); DB::connection('some_connection')->rollback(); } return $this->buildResponse(array('message' => $message)); } 的{​​{1}}没有转义字符串,则必须提前执行此操作以避免SQL注入和语法错误。

一次只有1000行构建Add语句。这应该比你开始时的速度快10倍(每AddWithValue行1行)。一次完成所有100K是有风险的,可能更慢。冒险,因为你可能会吹出一些限制(数据包大小等);因为需要巨大的INSERT日志而变慢。每批次后INSERT,或使用ROLLBACK

答案 4 :(得分:5)

加速的一种方法是将所有插入包装到一个事务(SQL-Server代码)中:

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.");

如果在一个命令中执行多次插入,则可能仍然使用StringBuilder而不是string来挤出一两英寸。

答案 5 :(得分:5)

这种方式可能不比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 :(得分:4)

正如Stefan Steiger所说,Bulk Insert适合您的情况。

另一个技巧是使用登台表,因此您不会直接写入生产表,而是写入临时表(具有相同的结构)。 写完所有信息后,你只需交换表格。 使用staging aproach,您将避免锁定表以进行插入(也可用于更新和删除),并且此模式在某些项目中大量用于MySQL。

此外,禁用表键可能会加快插入速度,但在启用它们时也会引入一些问题(仅适用于MyISAM引擎)。

<强>加

我们假设你有表Products

  • 产品编号
  • ProductName
  • ProductPrice

对于暂存目的,您可以使用相同的列集创建名为ProductsStaging的临时表。

您在临时桌上执行的所有操作:

UpdateStagingTable();
SwapTables();
UpdateStagingTable();

因为在交换后您的临时表没有新数据,您再次调用相同的方法。 在SwapTables()方法中,您执行一个SQL语句:

RENAME TABLE Products TO ProductsTemp,
             ProductsStaging TO Products,
             ProductsTemp TO ProductsStagin;

数据操作的速度取决于MySql引擎(例如InnoDB,MyISAM等),因此您还可以通过更改引擎来加速插入。

答案 7 :(得分:2)

我在使用EF-MySQL时偶然发现了类似的问题。 EF插入太慢了,因此使用fubo提到的方法。首先,性能大幅提升(~10K记录在~10秒内插入)但随着表的大小增加而降低,表中有大约1M条记录,插入时间约为250秒。

终于找到了问题!表的PK是GUID类型(UUID -  炭(36))。由于UUID无法按顺序编制索引,并且每个插入都需要重建索引,因此速度会降低。

修复是用bigint(或int)替换PK并将其设置为标识列。这提高了性能,插入平均需要约12秒,表中有~2M +记录!

我想在这里分享这个发现以防万一有人遇到类似的问题!

答案 8 :(得分:1)

我的建议是一个想法,而不是示例或解决方案。如果您不使用INSERT但将数据作为多个参数传递(不必一次全部为100K,例如,您可以使用1K的捆绑包),则STORED PROCEDURE本身会执行INSERT,该怎么办?

答案 9 :(得分:0)

批量操作将是实现此目标的好方法。会先读取您的属性,然后为您创建一个批量查询...

有一个github存储库,其中包含两个有用的方法:使用MySql和EF6 +的BulkInsert和BulkUpdate。

BulkUpdate / BulkInsert基本上从通用实体读取所有属性,然后为您创建bulkquery。

Ps:此项目是根据我的需要开发的,并且该项目向有意改善或更改它的人开放,以寻求更好的解决方案,这对于社区来说是值得的。

Ps²:如果不能解决问题,请尝试对项目进行更改以改进并实现您想要的目标,至少这是一个好的开始。

请看看here

答案 10 :(得分:0)

我找到了避免将文件用于批量插入的方法。在this connector中是实现者从流中加载。 这样可以完成类似的加载

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