web-dev-qa-db-fra.com

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

J'ai un fichier (qui a 10 millions d'enregistrements) comme ci-dessous:

    line1
    line2
    line3
    line4
   .......
    ......
    10 million lines

Donc, fondamentalement, je veux insérer 10 millions d'enregistrements dans la base de données. j'ai donc lu le fichier et l'ai téléchargé sur SQL Server.

Code C #

System.IO.StreamReader file = 
    new System.IO.StreamReader(@"c:\test.txt");
while((line = file.ReadLine()) != null)
{
    // insertion code goes here
    //DAL.ExecuteSql("insert into table1 values("+line+")");
}

file.Close();

mais l'insertion prendra beaucoup de temps. Comment puis-je insérer 10 millions d'enregistrements dans les plus brefs délais en utilisant C #?

Mise à jour 1:
INSERER en vrac:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(

     ROWTERMINATOR =' \n'
  );

Ma table est comme ci-dessous:

DATAs
(
     DatasField VARCHAR(MAX)
)

mais j'obtiens l'erreur suivante:

Msg 4866, niveau 16, état 1, ligne 1
Le chargement en bloc a échoué. La colonne est trop longue dans le fichier de données pour la ligne 1, colonne 1. Vérifiez que le terminateur de champ et le terminateur de ligne sont correctement spécifiés.

Msg 7399, niveau 16, état 1, ligne 1
Le fournisseur de base de données OLE "BULK" pour le serveur lié "(null)" a signalé une erreur. Le fournisseur n'a donné aucune information sur l'erreur.

Msg 7330, niveau 16, état 2, ligne 1
Impossible de récupérer une ligne de OLE fournisseur de base de données "BULK" pour le serveur lié "(null)".

Le code ci-dessous a fonctionné:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(
    FIELDTERMINATOR = '\t',
    ROWTERMINATOR = '\n'
);
27
MD TAHMID HOSSAIN

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...)) est de diffuser le contenu du fichier dans la base de données via un paramètre table (TVP). En utilisant un TVP, vous pouvez ouvrir le fichier, lire une ligne et envoyer une ligne jusqu'à la fin, 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. J'assume le même nom de table d'importation et de champ comme indiqué 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 SqlCommand appelle ExecuteNonQuery, ce qui ouvre le fichier, lit une ligne et l'envoie à SQL Server via IEnumerable<SqlDataRecord> Et yield return construit, puis ferme le fichier. La procédure stockée ne voit qu'une variable de table, @ImportTable, qui peut être accessible dès que les données commencent à arriver ( remarque: les données persistent pendant une courte période, même si elles ne sont pas complètes) contenu, 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();
   }
}

La méthode GetFileContents 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;
}

Notes complémentaires:

  1. Avec quelques modifications, le code C # ci-dessus peut être adapté pour regrouper les données.
  2. Avec une modification mineure, le code C # ci-dessus peut être adapté pour envoyer dans plusieurs champs (l'exemple illustré dans l'article "Steaming Data ..." lié ci-dessus passe dans 2 champs).
  3. Vous pouvez également manipuler la valeur de chaque enregistrement dans l'instruction SELECT dans le proc.
  4. Vous pouvez également filtrer les lignes en utilisant une condition WHERE dans le proc.
  5. Vous pouvez accéder à la variable de table TVP plusieurs fois; il est READONLY mais pas "forward only".
  6. Avantages par rapport à SqlBulkCopy:
    1. SqlBulkCopy est uniquement INSERT 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 en fonction d'une condition; vous pouvez diviser les données en plusieurs tables; etc.
    2. Étant donné qu'un TVP n'est pas uniquement INSERT, vous n'avez pas besoin d'une table de transfert distincte pour vider 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 champ IDENTITY sur la table d'importation DATAs, vous pourriez ajouter une clause OUTPUT à la INSERT pour renvoyer INSERTED.[ID] (En supposant que ID est le nom du champ IDENTITY). Ou vous pouvez renvoyer les résultats d'une requête complètement différente, ou les deux, car plusieurs ensembles de résultats peuvent être envoyés et accessibles via Reader.NextResult(). Récupérer des informations de la base de données n'est pas possible lors de l'utilisation de SqlBulkCopy mais il y a plusieurs questions ici sur S.O. des personnes voulant faire exactement cela (au moins en ce qui concerne les valeurs IDENTITY nouvellement créées).
    4. Pour plus d'informations sur les raisons pour lesquelles il est parfois plus rapide pour le processus global, même s'il est légèrement plus lent à obtenir les données du disque dans SQL Server, veuillez consulter ce livre blanc de l'équipe de conseil client SQL Server: Maximizing Throughput with TVP =
41
Solomon Rutzky

En C #, la meilleure solution est de laisser le SqlBulkCopy lire le fichier. Pour ce faire, vous devez passer un IDataReader directement à SqlBulkCopy.WriteToServer méthode. Voici un exemple: http://www.codeproject.com/Articles/228332/IDataReader-implementation-plus-SqlBulkCopy

6
playful

la meilleure façon est de mélanger votre 1ère et la 2ème solution, créez DataTable et dans la boucle ajoutez-y des lignes puis utilisez BulkCopy pour télécharger vers la base de données dans une connexion tilisez ceci pour aide en copie groupée

une autre chose pour faire attention que la copie en bloc est une opération très sensible que presque chaque erreur annulera la copie, par exemple si vous déclarez le nom de la colonne dans le dataTable comme "texte" et dans la base de données son "texte", il lèvera une exception , bonne chance.

3
Liran