it-swarm.com.de

Die effizienteste Methode zum Einfügen von Zeilen in die MySQL-Datenbank

Ich habe viele Fragen dazu gelesen, aber ich konnte keine finden, die schnell genug ist. Ich denke, es gibt bessere Möglichkeiten, viele Zeilen in eine MySQL-Datenbank einzufügen

Ich verwende den folgenden Code, um 100 KB in meine MySQL-Datenbank einzufügen:

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

Dies dauert etwa 100 Sekunden für 100.000 Zeilen. Wie kann ich das schneller oder etwas effizienter gestalten? 

Könnte schneller sein, um mehrere Zeilen über einen DataTable/DataAdapter oder auf einmal einzufügen:

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

Aus Sicherheitsgründen kann ich die Daten nicht in eine Datei laden und MySQLBulkLoaden.

36
fubo

Hier ist mein "Multiple Inserts" -Code. 

Das Einfügen von 100k-Reihen dauerte statt 40 Sekunden nur 3 Sekunden !! 

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

Die erstellte SQL-Anweisung sieht folgendermaßen aus:

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

Update : Thanks Salman A Ich habe MySQLHelper.EscapeString hinzugefügt, um die Code-Injektion zu vermeiden, die intern verwendet wird, wenn Sie Parameter verwenden.

42
fubo

Ich habe einen kleinen Test mit drei Dingen MySqlDataAdapter, Transaktionen und UpdateBatchSize durchgeführt. Es ist etwa 30 Mal schneller als beim ersten Beispiel. Mysql wird in einer separaten Box ausgeführt, sodass Latenz entsteht. Die Stapelgröße muss möglicherweise angepasst werden. Code folgt:

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

Führen Sie den Befehl in einer Transaction aus und verwenden Sie dieselbe Instanz des Befehls für jede Iteration erneut. Senden Sie zur weiteren Leistungsoptimierung 100 Abfragen in einem Befehl. Die parallele Ausführung kann zu einer besseren Leistung führen (Parallel.For), aber stellen Sie sicher, dass jede parallele Schleife ihre eigene MySqlCommand-Instanz erhält.

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

Wenn Add von AddWithValue keine Zeichenfolgen Escapezeichen enthält, müssen Sie dies im Voraus tun, um SQL-Injection- und Syntaxfehler zu vermeiden.

Erstellen Sie INSERT-Anweisungen mit jeweils nur 1000 Zeilen. Das sollte leicht 10 mal so schnell laufen, wie Sie es angefangen haben (1 Zeile pro INSERT). Alle 100K gleichzeitig zu machen, ist riskant und möglicherweise langsamer. Riskant, weil Sie ein gewisses Limit (Paketgröße usw.) ausblasen können; langsamer, da ein großes ROLLBACK-Protokoll benötigt wird. COMMIT nach jedem Stapel oder verwenden Sie autocommit=1.

7
Rick James

Dieser Weg ist möglicherweise nicht schneller als der Stringbuilder-Ansatz, er ist jedoch parametrisiert:

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

Dies verwendet die Hilfsklassen wie folgt:

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

Und diese Hilfsmethode:

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

Vielleicht nicht super elegant, aber es funktioniert gut. Ich benötige ein Fortschritts-Tracking, das für mich enthalten ist. Fühlen Sie sich frei, diesen Teil zu entfernen.

Dadurch werden SQL-Befehle erzeugt, die der gewünschten Ausgabe ähneln.

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

Eine Möglichkeit zur Beschleunigung wäre das Einbetten aller Einfügungen in eine Transaktion (SQL-Server-Code):

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

Eine andere Möglichkeit besteht darin, die CSV-Datei in eine Datentabelle zu laden und die Stapelungsfunktion von DataAdapter zu verwenden

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

Sie finden ein schönes Beispiel hier .

Oder Sie können die MySQL BulkLoader C # -Klasse verwenden:

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

Wenn Sie mehrere Einfügungen in einem Befehl ausführen, können Sie immer noch ein oder zwei Zoll herausdrücken, indem Sie StringBuilder anstelle von string verwenden.

5
Stefan Steiger

Wie Stefan Steiger sagt, ist Bulk Insert für Ihre Situation geeignet.

Ein weiterer Trick besteht in der Verwendung von Staging-Tabellen. Anstatt direkt in die Produktionstabelle zu schreiben, schreiben Sie in Staging (mit derselben Struktur) . Nachdem Sie alle Informationen geschrieben haben, tauschen Sie nur Tabellen ..__ vermeidet das Sperren von Tabellen zum Einfügen (kann auch zum Aktualisieren und Löschen verwendet werden), und dieses Muster wird in einigen Projekten stark mit MySQL verwendet. 

Das Deaktivieren von Tabellenschlüsseln kann das Einfügen beschleunigen, kann jedoch auch zu Problemen führen, wenn Sie sie aktivieren (nur für MyISAM-Engine).

Hinzugefügt:

Nehmen wir an, Sie haben eine Tabelle Products:

  • Produkt ID
  • Produktname 
  • Produktpreis

Zu Staging-Zwecken erstellen Sie eine Staging-Tabelle mit dem Namen ProductsStaging mit demselben Spaltensatz.

Alle Ihre Vorgänge, die Sie auf Staging-Tabelle ausführen:

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

weil nach dem Auslagern Ihre Staging-Tabelle nicht über die neuen Daten verfügt, rufen Sie dieselbe Methode erneut auf. __ In der SwapTables()-Methode führen Sie eine SQL-Anweisung aus:

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

Die Geschwindigkeit der Datenmanipulationen hängt von der MySql-Engine ab (z. B. InnoDB, MyISAM usw.). Sie können das Einfügen auch beschleunigen, indem Sie die Engine ändern.

4
Alex Sikilinda

Bei der Arbeit mit EF - MySQL bin ich auf ein ähnliches Problem gestoßen. Die EF-Inserts waren viel zu langsam und verwendeten daher den von fubo genannten Ansatz. Zu Beginn verbesserte sich die Leistung drastisch (~ 20.000 Datensätze wurden in ~ 10 Sekunden eingefügt), nahm jedoch mit zunehmender Größe der Tabelle ab. Bei ~ 1 Mio. Datensätzen in der Tabelle dauerte das Einfügen ~ 250 Sekunden.

Endlich das Problem herausgefunden! Die PK der Tabelle hatte den Typ GUID (UUID - Char (36)). Da UUIDs nicht sequenziell indexieren können und für jedes Einfügen die Indizes neu erstellt werden mussten, wurde die Geschwindigkeit verlangsamt.

Das Update bestand darin, die PK durch bigint (oder int) zu ersetzen und als Identitätsspalte festzulegen. Dies verbesserte die Leistung, die Einfügungen dauerten durchschnittlich ~ 12 Sekunden mit ~ 2M + Datensätzen in der Tabelle!

Ich dachte, ich würde diese Erkenntnis hier teilen, nur für den Fall, dass jemand bei einem ähnlichen Problem stecken bleibt!

2
ashin

Mein Vorschlag ist eine Idee, kein Beispiel oder eine Lösung. Was ist, wenn Sie keine INSERTs verwenden, sondern Daten als mehrere Parameter übergeben (nicht alle 100 KB gleichzeitig erforderlich, Sie können beispielsweise 1K-Pakete verwenden) an STORED PROCEDURE, die selbst INSERTs ausführt. 

1
Dzianis Yafimau

Ich habe den Weg gefunden, eine Datei für die Masseneinfügung zu vermeiden. In diesem Konnektor wurde der Implementierer aus dem Stream geladen

  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

Eine Massenoperation wäre eine gute Möglichkeit, um damit zu führen. Etwas, das Ihre Eigenschaften liest und dann eine Massenabfrage für Sie erstellt ...

Es gibt ein github-Repository, das beide nützliche Methoden enthält: BulkInsert und BulkUpdate mit MySql und EF6 +.

BulkUpdate/BulkInsert liest grundsätzlich alle Eigenschaften Ihrer generischen Entität und erstellt dann die Bulkquery für Sie.

Ps: Dieses Projekt wurde für meine Bedürfnisse entwickelt und das Projekt ist offen für diejenigen, die daran arbeiten, es zu verbessern oder zu ändern, um eine bessere Lösung zu erhalten, die für die Gemeinschaft von Wert ist.

Ps²: Wenn das Problem nicht gelöst wird, versuchen Sie, Änderungen am Projekt vorzunehmen, um das zu verbessern, was Sie wollen, es ist zumindest ein guter Anfang.

Bitte schau nach hier

0
Bruno Henrique