【问题标题】:Most efficient way to insert Rows into MySQL Database将行插入 MySQL 数据库的最有效方法
【发布时间】:2016-02-03 14:00:38
【问题描述】:

我已经阅读了很多关于此的问题,但我找不到足够快的问题。我认为有更好的方法可以将大量行插入 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)...

由于安全问题,我无法将数据加载到文件和 MySQLBulkLoad 中。

【问题讨论】:

  • Fubo 是在正确的轨道上,但它需要是parameterized。然后,您可以调整批量大小以获得最佳性能。这些批量大小将根据 CPU/RAM 等而有所不同

标签: c# mysql performance


【解决方案1】:

我找到了避免使用文件进行批量插入的方法。在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();
    }
  }
}

【讨论】:

  • 谢谢你!请记住也要设置列。
【解决方案2】:

批量操作将是一个很好的领导方式。读取您的属性然后为您创建批量查询的东西...

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

BulkUpdate/BulkInsert 基本上从您的通用实体中读取所有属性,然后为您创建批量查询。

Ps:这是为了满足我的需要而开发的,并且该项目向关心改进或更改它以获得对社区有价值的更好解决方案的人开放。

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

请看here

【讨论】:

    【解决方案3】:

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

    终于解决问题了!表的 P​​K 类型为 GUID(UUID - 字符(36))。由于 UUID 无法按顺序索引,并且每次插入都需要重建索引,因此速度变慢了。

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

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

    【讨论】:

      【解决方案4】:

      这是我的“多次插入”代码。

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

      【讨论】:

      • 只需用双撇号替换“test”中的撇号,就可以了。 "test" ==> "test".Replace("'", "''")
      • @fubo 应该是。我猜它使用相同或相似的代码来转义参数值。
      • 它只是顺便说一句,而不是 string.Format,我发现使用 $"'{MySqlHelper.EscapeString("test")}'" 更容易
      • 现在你在内存中创建了一个很长的字符串。
      • 我们可以使用存储过程吗?
      【解决方案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-File 加载到数据表中,并使用 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 而不是字符串来挤出一两英寸。

      【讨论】:

      • 交易代码在这种情况下需要 50 秒,而不是之前的 40 秒 :(
      • @fubo:看起来 MySQL 的行为与 SQL-Server 不同。
      • 注意隔离级别。您可能会降低数据库速度并导致死锁
      • 为了帮助,如果列名中有空格,MySqlBulkLoader 会报错。所以首先从 CSV 中删除列名中的空格。
      【解决方案6】:

      这种方式可能不会比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;
          }
      

      【讨论】:

      • 扩展名ToCSV 丢失
      【解决方案7】:

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

      【讨论】:

      • 感谢您的回答。这花了 44 秒 :( 似乎 MySqlTransaction 由 SQL-Server 处理,每一行都是单独发送的
      • 只是指出,这个方法是阻塞的,将方法更改为异步,将ExecuteNonQuery更改为ExecuteNonQueryAsync,将Open更改为OpenAsync不会阻止阻塞。
      • 答案中获得100分赏金的方法比这个方法性能要好得多。
      【解决方案8】:

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

      另一个技巧是使用暂存表,因此您无需直接写入生产表,而是写入暂存表(具有相同的结构)。 写完所有信息后,您只需交换表即可。 使用分段方法,您将避免锁定表以进行插入(也可用于更新和删除),并且这种模式在某些项目中与 MySQL 大量使用。

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

      添加:

      假设你有桌子Products:

      • ProductId
      • 产品名称
      • 产品价格

      为了暂存目的,您创建一个名为 ProductsStaging 的暂存表,其中包含相同的列集。

      您在暂存台上所做的所有操作:

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

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

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

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

      【讨论】:

      • 您可以使用单个语句使RENAMEs 成为原子。这样,Products 始终可用。
      • @RickJames,是的,你是对的。我已经编辑了答案。
      【解决方案9】:

      如果AddWithValue 中的Add 不转义字符串,则必须提前进行转义,以避免SQL 注入和语法错误。

      构建 INSERT 语句,一次只有 1000 行。它的运行速度应该是您开始时的 10 倍(每个 INSERT 1 行)。一次完成所有 100K 是有风险的,而且可能更慢。有风险,因为您可能会超出一些限制(数据包大小等);由于需要巨大的ROLLBACK 日志,因此速度较慢。每批后COMMIT,或使用autocommit=1。

      【讨论】:

      • 要添加,构建只有 max_allowed_packet 字节或更少的插入语句。这是大型查询最有可能出现的限制。
      • 带有字符串连接或参数的语句?你能给我看个样品吗?
      【解决方案10】:

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

      【讨论】:

        【解决方案11】:

        我使用三个东西 MySqlDataAdapter、transactions 和 UpdateBatchSize 做了一个小测试。它比您的第一个示例快大约 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");
            }
        }
        

        【讨论】:

        • 只用了 13 秒 - 取决于 UpdateBatchSize。并且 MySqlTransaction 似乎是必需的。
        • 好吧,你只快了 3 倍,太糟糕了
        • 可以使用mySqlDataAdapterSelect.FillSchema(dt, SchemaType.Mapped);从数据库中获取空结构
        • 不错不错。我刚刚为你做了这个样本,我很好奇它是否可行。我总是使用 NHibernate 或类似的 :)
        • 为了使这个方法更通用,可以myAdapter.InsertCommand= new MySqlCommandBuilder(myAdapter).GetInsertCommand()
        猜你喜欢
        • 2012-11-27
        • 2012-05-10
        • 2018-07-30
        • 1970-01-01
        • 1970-01-01
        • 2012-06-01
        • 1970-01-01
        相关资源
        最近更新 更多