Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Comment puis-je insérer 10 millions d'enregistrements dans les plus brefs délais ?

Veuillez ne pas créer un DataTable à charger via BulkCopy. C'est une bonne solution pour les petits ensembles de données, mais il n'y a absolument aucune raison de charger les 10 millions de lignes en mémoire avant d'appeler la base de données.

Votre meilleur pari (en dehors de BCP / BULK INSERT / OPENROWSET(BULK...) ) consiste à diffuser le contenu du fichier dans la base de données via un paramètre de valeur table (TVP). En utilisant un TVP, vous pouvez ouvrir le fichier, lire une ligne et envoyer une ligne jusqu'à ce que vous ayez terminé, puis fermer le fichier. Cette méthode a une empreinte mémoire d'une seule ligne. J'ai écrit un article, Streaming Data Into SQL Server 2008 From an Application, qui contient un exemple de ce scénario.

Un aperçu simpliste de la structure est le suivant. Je suppose la même table d'importation et le même nom de champ que ceux indiqués dans la question ci-dessus.

Objets de base de données requis :

-- 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

Le code d'application C # pour utiliser les objets SQL ci-dessus est ci-dessous. Remarquez comment, plutôt que de remplir un objet (par exemple DataTable) puis d'exécuter la procédure stockée, dans cette méthode, c'est l'exécution de la procédure stockée qui lance la lecture du contenu du fichier. Le paramètre d'entrée du Stored Proc n'est pas une variable; c'est la valeur de retour d'une méthode, GetFileContents . Cette méthode est appelée lorsque le SqlCommand appelle ExecuteNonQuery , qui ouvre le fichier, lit une ligne et envoie la ligne à SQL Server via le IEnumerable<SqlDataRecord> et yield return construit, puis ferme le fichier. La procédure stockée ne voit qu'une variable de table, @ImportTable, accessible dès que les données commencent à arriver (remarque :les données persistent pendant une courte période, même si ce n'est pas le contenu complet, dans 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();
   }
}

Le GetFileContents méthode ci-dessus est utilisée comme valeur de paramètre d'entrée pour la procédure stockée, comme indiqué ci-dessous :

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

Remarques supplémentaires :

  1. Avec quelques modifications, le code C# ci-dessus peut être adapté pour regrouper les données.
  2. Avec des modifications mineures, le code C# ci-dessus peut être adapté pour envoyer dans plusieurs champs (l'exemple montré dans l'article "Steaming Data..." lié ci-dessus passe dans 2 champs).
  3. Vous pouvez également manipuler la valeur de chaque enregistrement dans le SELECT déclaration dans la procédure.
  4. Vous pouvez également filtrer les lignes en utilisant une condition WHERE dans la procédure.
  5. Vous pouvez accéder plusieurs fois à la variable de table TVP ; c'est READONLY mais pas "forward only".
  6. Avantages par rapport à SqlBulkCopy :
    1. SqlBulkCopy est INSERT uniquement alors que l'utilisation d'un TVP permet d'utiliser les données de n'importe quelle manière :vous pouvez appeler MERGE; vous pouvez DELETE basé sur une condition; vous pouvez diviser les données en plusieurs tables ; et ainsi de suite.
    2. Étant donné qu'un TVP n'est pas uniquement INSERT, vous n'avez pas besoin d'une table intermédiaire distincte pour y transférer les données.
    3. Vous pouvez récupérer les données de la base de données en appelant ExecuteReader au lieu de ExecuteNonQuery . Par exemple, s'il y avait un IDENTITY champ sur les DATAs table d'importation, vous pouvez ajouter un OUTPUT clause à INSERT pour renvoyer INSERTED.[ID] (en supposant ID est le nom de l'IDENTITY domaine). Ou vous pouvez renvoyer les résultats d'une requête complètement différente, ou les deux puisque plusieurs ensembles de résultats peuvent être envoyés et accessibles via Reader.NextResult() . Il n'est pas possible de récupérer les informations de la base de données lors de l'utilisation de SqlBulkCopy pourtant il y a plusieurs questions ici sur S.O. de personnes voulant faire exactement cela (au moins en ce qui concerne le nouveau IDENTITY valeurs).
    4. Pour plus d'informations sur les raisons pour lesquelles le processus global est parfois plus rapide, même s'il est légèrement plus lent lors de l'obtention des données du disque dans SQL Server, veuillez consulter ce livre blanc de l'équipe de conseil client SQL Server :Maximiser le débit avec TVP