web-dev-qa-db-fra.com

Le moyen le plus efficace pour insérer des lignes dans la base de données MySQL

J'ai lu beaucoup de questions à ce sujet mais je n'ai pas pu en trouver une assez rapide. Je pense qu'il y a de meilleurs moyens d'insérer beaucoup de lignes dans une base de données MySQL

J'utilise le code suivant pour insérer 100k dans ma base de données 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();
        }
    }
}

Cela prend pour 40 000 lignes environ 40 secondes. Comment puis-je rendre cela plus rapide ou un peu plus efficace? 

Pourrait être plus rapide d'insérer plusieurs lignes via un DataTable/DataAdapter ou à la fois:

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

En raison de problèmes de sécurité, je ne peux pas charger les données dans un fichier et MySQLBulkLoad le.

36
fubo

Voici mon code "inserts multiples". 

L'insertion de 100 000 lignes n'a pris que 40 secondes au lieu de 3 secondes !! 

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

L'instruction SQL créée ressemble à ceci:

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

Mise à jour: Merci Salman A J'ai ajouté MySQLHelper.EscapeString pour éviter l'injection de code, qui est utilisé en interne lorsque vous utilisez des paramètres.

42
fubo

J'ai fait un petit test en utilisant trois choses: MySqlDataAdapter, transactions et UpdateBatchSize. Il est environ 30 fois plus rapide que votre premier exemple. Mysql fonctionne sur une boîte séparée, ce qui entraîne une latence. La taille de lot peut nécessiter un ajustement. Le code suit:

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

Exécutez la commande dans Transaction et réutilisez la même instance de commande pour chaque itération. Pour optimiser davantage les performances, envoyez 100 requêtes en une seule commande. L'exécution en parallèle pourrait donner de meilleures performances (Parallel.For) mais assurez-vous que chaque boucle parallèle a sa propre instance 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();
            }
        }
    }
}
9
Sarvesh Mishra

Si Add of AddWithValue n'échappe pas aux chaînes, vous devez le faire à l'avance pour éviter les erreurs d'injection SQL et de syntaxe.

Construisez des instructions INSERT avec seulement 1000 lignes à la fois. Cela devrait fonctionner facilement 10 fois plus vite que ce que vous avez commencé avec (1 ligne par INSERT). Faire tout le 100K à la fois est risqué et peut-être plus lent. Risqué parce que vous pourriez dépasser une limite (taille du paquet, etc.); plus lent en raison de la nécessité d’un journal ROLLBACK énorme. COMMIT après chaque lot, ou utilisez autocommit=1.

7
Rick James

De cette façon, l’approche du constructeur de chaînes n’est peut-être pas plus rapide, mais elle est paramétrée:

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

Ceci utilise les classes d'assistance comme ci-dessous:

/// <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; }
}

Et cette méthode d'assistance:

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

Peut-être pas super élégant mais ça marche bien. J'ai besoin du suivi des progrès pour qu'il soit inclus pour moi, n'hésitez pas à supprimer cette partie.

Cela produira des commandes SQL similaires à la sortie souhaitée.

EDIT: 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;
    }
5
Simon

Une façon d'accélérer serait d'encapsuler toutes les insertions dans UNE transaction (code 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();
}

Une autre méthode consiste à charger le fichier CSV dans un fichier de données et à utiliser la fonctionnalité de traitement par lots de 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();

Vous trouvez un bel exemple ici .

Ou vous pouvez utiliser la classe 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.");

Si vous faites plusieurs insertions dans une commande, vous pouvez toujours en extraire un pouce ou deux en utilisant StringBuilder au lieu de chaîne.

5
Stefan Steiger

Comme le dit Stefan Steiger, Bulk Insert convient à votre situation.

Une autre astuce consiste à utiliser des tables de transfert, donc au lieu d’écrire directement dans la table de production, vous écrirez dans celle-ci (qui a la même structure) ..__ évitera de verrouiller les tables pour insertion (peut également être utilisé pour la mise à jour et la suppression), et ce modèle est fortement utilisé avec MySQL dans certains projets. 

En outre, la désactivation des clés de table peut accélérer l’insertion, mais peut également entraîner des problèmes lors de leur activation (uniquement pour le moteur MyISAM).

Ajoutée:

Disons que vous avez table Products:

  • ProductId
  • NomProduit 
  • Prix du produit

À des fins de transfert, vous créez une table de transfert appelée ProductsStaging, avec le même jeu de colonnes.

Toutes vos opérations que vous faites sur la table intermédiaire:

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

car après échange, votre table intermédiaire n'a plus les nouvelles données, vous appelez à nouveau la même méthode . Dans la méthode SwapTables(), vous exécutez une instruction SQL:

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

La vitesse de manipulation des données dépend du moteur MySql (par exemple, InnoDB, MyISAM, etc.). Vous pouvez donc accélérer les insertions en changeant de moteur.

4
Alex Sikilinda

Je suis tombé sur un problème similaire lorsque je travaillais avec EF - MySQL. Les inserts EF étaient beaucoup trop lents et utilisaient donc l'approche mentionnée par fubo . Pour commencer, les performances se sont considérablement améliorées (environ 20 000 enregistrements ont été insérés en environ 10 secondes), mais se sont dégradées à mesure que la taille de la table augmentait.

Enfin compris le problème! La PK de la table était de type GUID (UUID - Char (36)). Comme les UUID ne peuvent pas indexer séquentiellement et que chaque insertion nécessite la reconstruction des index, elle est ralentie.

Le correctif consistait à remplacer le PK par bigint (ou int) et à le définir comme une colonne d'identité. Cela a amélioré les performances, les insertions ont pris en moyenne ~ 12 secondes avec environ 2M + enregistrements dans le tableau!

Je pensais partager cette conclusion ici au cas où quelqu'un se retrouverait coincé dans un problème similaire!

2
ashin

Ma suggestion est une idée, pas un exemple ou une solution. Que faire si vous n'utilisez pas INSERT, mais transmettez des données sous forme de paramètres multiples (pas nécessairement tous les 100 Ko à la fois, vous pouvez utiliser des ensembles de 1 Ko par exemple) pour STORED PROCEDURE qui lui-même effectue des INSERT. 

1
Dzianis Yafimau

J'ai trouvé le moyen d'éviter d'utiliser un fichier pour l'insertion en bloc. Dans ce connecteur était la charge de l'implémenteur du flux . Le chargement pourrait donc être effectué de la même manière

  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

Une opération en masse serait une bonne manière de mener avec cela. Quelque chose qui lit vos propriétés, puis crée une requête groupée pour vous ...

Il existe un référentiel github qui contient les deux méthodes utiles: BulkInsert et BulkUpdate en utilisant MySql et EF6 +.

En règle générale, BulkUpdate/BulkInsert lit toutes les propriétés de votre entité générique, puis crée le bulkquery pour vous.

Ps: Cela a été conçu pour répondre à mes besoins et le projet est ouvert à qui veut l’améliorer ou le changer pour une meilleure solution qui vaudra la peine pour la communauté.

Ps²: Si cela ne résout pas le problème, essayez d’apporter des modifications au projet pour améliorer et obtenir ce que vous voulez, c’est au moins un bon début.

S'il vous plaît, jetez un oeil à ici

0
Bruno Henrique