Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Wie kann ich 10 Millionen Datensätze in kürzester Zeit einfügen?

Bitte nicht Erstellen Sie eine DataTable per BulkCopy zu laden. Das ist eine gute Lösung für kleinere Datensätze, aber es gibt absolut keinen Grund, alle 10 Millionen Zeilen in den Speicher zu laden, bevor die Datenbank aufgerufen wird.

Ihre beste Chance (außerhalb von BCP / BULK INSERT / OPENROWSET(BULK...) ) besteht darin, den Inhalt aus der Datei über einen Tabellenwertparameter (TVP) in die Datenbank zu streamen. Durch die Verwendung eines TVP können Sie die Datei öffnen, eine Zeile lesen und eine Zeile senden, bis Sie fertig sind, und dann die Datei schließen. Diese Methode hat einen Speicherbedarf von nur einer einzigen Zeile. Ich habe einen Artikel „Streaming Data into SQL Server 2008 From an Application“ geschrieben, der ein Beispiel für genau dieses Szenario enthält.

Ein vereinfachter Überblick über die Struktur ist wie folgt. Ich gehe davon aus, dass die gleiche Importtabelle und der gleiche Feldname wie in der obigen Frage gezeigt werden.

Erforderliche Datenbankobjekte:

-- First: You need a User-Defined Table Type
CREATE TYPE ImportStructure AS TABLE (Field VARCHAR(MAX));
GO

-- Second: Use the UDTT as an input param to an import proc.
--         Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
   @ImportTable    dbo.ImportStructure READONLY
)
AS
SET NOCOUNT ON;

-- maybe clear out the table first?
TRUNCATE TABLE dbo.DATAs;

INSERT INTO dbo.DATAs (DatasField)
    SELECT  Field
    FROM    @ImportTable;

GO

C#-App-Code zur Verwendung der oben genannten SQL-Objekte finden Sie unten. Beachten Sie, dass anstatt ein Objekt (z. B. DataTable) zu füllen und dann die gespeicherte Prozedur auszuführen, bei dieser Methode die Ausführung der gespeicherten Prozedur das Lesen des Dateiinhalts initiiert. Der Eingabeparameter der Stored Proc ist keine Variable; es ist der Rückgabewert einer Methode, GetFileContents . Diese Methode wird aufgerufen, wenn der SqlCommand ruft ExecuteNonQuery auf , die die Datei öffnet, eine Zeile liest und die Zeile über IEnumerable<SqlDataRecord> an SQL Server sendet und yield return erstellt und schließt dann die Datei. Die gespeicherte Prozedur sieht nur eine Tabellenvariable, @ImportTable, auf die zugegriffen werden kann, sobald die Daten herüberkommen (Hinweis:Die Daten bleiben für kurze Zeit, auch wenn nicht der vollständige Inhalt, in tempdb ).

using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;

private static IEnumerable<SqlDataRecord> GetFileContents()
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[] {
      new SqlMetaData("Field", SqlDbType.VarChar, SqlMetaData.Max)
   };
   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);
   StreamReader _FileReader = null;

   try
   {
      _FileReader = new StreamReader("{filePath}");

      // read a row, send a row
      while (!_FileReader.EndOfStream)
      {
         // You shouldn't need to call "_DataRecord = new SqlDataRecord" as
         // SQL Server already received the row when "yield return" was called.
         // Unlike BCP and BULK INSERT, you have the option here to create a string
         // call ReadLine() into the string, do manipulation(s) / validation(s) on
         // the string, then pass that string into SetString() or discard if invalid.
         _DataRecord.SetString(0, _FileReader.ReadLine());
         yield return _DataRecord;
      }
   }
   finally
   {
      _FileReader.Close();
   }
}

Die GetFileContents Die obige Methode wird als Eingabeparameterwert für die gespeicherte Prozedur verwendet, wie unten gezeigt:

public static void test()
{
   SqlConnection _Connection = new SqlConnection("{connection string}");
   SqlCommand _Command = new SqlCommand("ImportData", _Connection);
   _Command.CommandType = CommandType.StoredProcedure;

   SqlParameter _TVParam = new SqlParameter();
   _TVParam.ParameterName = "@ImportTable";
   _TVParam.TypeName = "dbo.ImportStructure";
   _TVParam.SqlDbType = SqlDbType.Structured;
   _TVParam.Value = GetFileContents(); // return value of the method is streamed data
   _Command.Parameters.Add(_TVParam);

   try
   {
      _Connection.Open();

      _Command.ExecuteNonQuery();
   }
   finally
   {
      _Connection.Close();
   }

   return;
}

Zusätzliche Anmerkungen:

  1. Mit einigen Modifikationen kann der obige C#-Code angepasst werden, um die Daten per Batch einzufügen.
  2. Mit geringfügigen Änderungen kann der obige C#-Code so angepasst werden, dass er mehrere Felder einsendet (das im oben verlinkten Artikel „Steaming Data...“ gezeigte Beispiel passt in 2 Felder).
  3. Sie können auch den Wert jedes Datensatzes im SELECT manipulieren Anweisung in der Prozedur.
  4. Sie können Zeilen auch herausfiltern, indem Sie eine WHERE-Bedingung in der Prozedur verwenden.
  5. Sie können mehrmals auf die TVP-Tabellenvariable zugreifen; es ist READONLY, aber nicht "forward only".
  6. Vorteile gegenüber SqlBulkCopy :
    1. SqlBulkCopy ist nur INSERT, während die Verwendung eines TVP es erlaubt, die Daten auf beliebige Weise zu verwenden:Sie können MERGE aufrufen; Sie können DELETE basierend auf einer Bedingung; Sie können die Daten in mehrere Tabellen aufteilen; und so weiter.
    2. Da ein TVP nicht nur INSERT-fähig ist, benötigen Sie keine separate Staging-Tabelle, in die die Daten ausgegeben werden.
    3. Sie können Daten aus der Datenbank zurückholen, indem Sie ExecuteReader aufrufen statt ExecuteNonQuery . Zum Beispiel, wenn es eine IDENTITY gab Feld auf den DATAs import-Tabelle, könnten Sie einen OUTPUT hinzufügen -Klausel zum INSERT um INSERTED.[ID] zurückzugeben (vorausgesetzt ID ist der Name der IDENTITY Feld). Oder Sie können die Ergebnisse einer völlig anderen Abfrage zurückgeben, oder beides, da mehrere Ergebnissätze gesendet und über Reader.NextResult() abgerufen werden können . Das Abrufen von Informationen aus der Datenbank ist nicht möglich, wenn SqlBulkCopy verwendet wird dennoch gibt es hier mehrere Fragen zu S.O. der Leute, die genau das tun wollen (zumindest in Bezug auf die neu geschaffene IDENTITY Werte).
    4. Weitere Informationen dazu, warum der Gesamtprozess manchmal schneller ist, wenn auch etwas langsamer beim Einlesen der Daten von der Festplatte in SQL Server, finden Sie in diesem Whitepaper des SQL Server-Kundenberatungsteams:Maximieren des Durchsatzes mit TVP