sql >> Base de Datos >  >> RDS >> Sqlserver

¿Cómo puedo insertar 10 millones de registros en el menor tiempo posible?

Por favor no crear una DataTable para cargar a través de BulkCopy. Esa es una buena solución para conjuntos de datos más pequeños, pero no hay absolutamente ninguna razón para cargar los 10 millones de filas en la memoria antes de llamar a la base de datos.

Su mejor apuesta (fuera de BCP / BULK INSERT / OPENROWSET(BULK...) ) es transmitir el contenido del archivo a la base de datos a través de un parámetro con valores de tabla (TVP). Al usar un TVP, puede abrir el archivo, leer una fila y enviar una fila hasta que termine, y luego cerrar el archivo. Este método tiene una huella de memoria de una sola fila. Escribí un artículo, Transmisión de datos a SQL Server 2008 desde una aplicación, que tiene un ejemplo de este mismo escenario.

Una descripción simplista de la estructura es la siguiente. Estoy asumiendo la misma tabla de importación y el mismo nombre de campo que se muestra en la pregunta anterior.

Objetos de base de datos requeridos:

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

El código de la aplicación C# para hacer uso de los objetos SQL anteriores se encuentra a continuación. Observe cómo en lugar de llenar un objeto (por ejemplo, DataTable) y luego ejecutar el procedimiento almacenado, en este método es la ejecución del procedimiento almacenado lo que inicia la lectura del contenido del archivo. El parámetro de entrada de Stored Proc no es una variable; es el valor de retorno de un método, GetFileContents . Ese método se llama cuando SqlCommand llama a ExecuteNonQuery , que abre el archivo, lee una fila y la envía a SQL Server a través de IEnumerable<SqlDataRecord> y yield return construye y luego cierra el archivo. El procedimiento almacenado solo ve una variable de tabla, @ImportTable, a la que se puede acceder tan pronto como los datos comiencen a llegar (nota:los datos persisten por un corto tiempo, incluso si no es el contenido completo, en 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();
   }
}

El GetFileContents El método anterior se usa como el valor del parámetro de entrada para el procedimiento almacenado como se muestra a continuación:

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

Notas adicionales:

  1. Con algunas modificaciones, el código C# anterior se puede adaptar para agrupar los datos.
  2. Con una pequeña modificación, el código C# anterior se puede adaptar para enviar en varios campos (el ejemplo que se muestra en el artículo "Transmisión de datos..." vinculado anteriormente pasa en 2 campos).
  3. También puede manipular el valor de cada registro en el SELECT declaración en el proceso.
  4. También puede filtrar filas usando una condición WHERE en el proceso.
  5. Puede acceder a la variable de tabla TVP varias veces; es READONLY pero no "forward only".
  6. Ventajas sobre SqlBulkCopy :
    1. SqlBulkCopy es solo INSERTAR, mientras que el uso de un TVP permite que los datos se usen de cualquier manera:puede llamar a MERGE; puedes DELETE basado en alguna condición; puede dividir los datos en varias tablas; y así sucesivamente.
    2. Debido a que un TVP no es solo INSERT, no necesita una tabla de preparación separada para volcar los datos.
    3. Puede recuperar datos de la base de datos llamando a ExecuteReader en lugar de ExecuteNonQuery . Por ejemplo, si hubiera una IDENTITY campo en DATAs tabla de importación, podría agregar un OUTPUT cláusula al INSERT para devolver INSERTED.[ID] (asumiendo ID es el nombre de la IDENTITY campo). O puede devolver los resultados de una consulta completamente diferente, o ambos, ya que se pueden enviar y acceder a múltiples conjuntos de resultados a través de Reader.NextResult() . No es posible recuperar información de la base de datos cuando se usa SqlBulkCopy sin embargo, hay varias preguntas aquí en S.O. de personas que quieren hacer exactamente eso (al menos con respecto a la nueva IDENTITY valores).
    4. Para obtener más información sobre por qué a veces es más rápido para el proceso general, incluso si es un poco más lento para obtener los datos del disco en SQL Server, consulte este documento técnico del Equipo de asesoramiento al cliente de SQL Server:Maximización del rendimiento con TVP
    5. >