2008-12-05 22 views
26

¿Cuál es la forma más rápida de hacer inserciones masivas en Oracle utilizando .NET? Necesito transferir aproximadamente 160K registros usando .NET a Oracle. Actualmente, estoy usando Insertar declaración y ejecutarlo 160K veces. Tarda unos 25 minutos en completarse. Los datos de origen se almacenan en una DataTable, como resultado de una consulta de otra base de datos (MySQL),Inserción masiva en Oracle utilizando .NET

¿Hay alguna forma mejor de hacerlo?

EDITAR: Actualmente estoy usando System.Data.OracleClient, pero dispuesto a aceptar las soluciones que utilizan otro proveedor (ODP.NET, DevArt, etc ..)

+0

¿Qué pasa con una utilidad como SQL * Loader? –

+0

¿Has probado DevArt? Me preguntaba si Devart tiene OracleBulkCopy. –

Respuesta

2

Usted no debe comprometerse cada inserto debido a cometer toma mucho tiempo.

¿Qué proveedor utiliza para conectar su aplicación .NET a la base de datos Oracle? ¿Utiliza ODP.NET o el proveedor Devart (también conocido como proveedor de corelab) o utiliza el proveedor de Microsoft para Oracle (System.Data.OracleClient)?

+0

He editado la pregunta para responder a su comentario – Salamander2007

4

Una forma muy rápida de resolver este problema es hacer un enlace de base de datos desde la base de datos Oracle a la base de datos MySQL. Puede crear enlaces de bases de datos a bases de datos que no sean de Oracle. Después de haber creado el enlace de la base de datos puede recuperar sus datos de la base de datos MySQL con una ... create table mydata como select * from ... statement. Esto se llama conectividad heterogénea. De esta forma, no tiene que hacer nada en su aplicación .net para mover los datos.

Otra forma es usar ODP.NET. En ODP.NET puede usar la clase OracleBulkCopy.

Pero no creo que insertar 160k registros en una tabla de Oracle con System.Data.OracleClient debería tomar 25 minutos. Creo que te comprometes muchas veces. ¿Y vincula sus valores a la instrucción de inserción con parámetros o concatena sus valores? La unión es mucho más rápida.

+0

Eliminé mi respuesta porque esta es una solución mucho mejor. –

+0

De hecho, configuré el dblink en nuestro entorno de desarrollo, pero cuando se trata de producción, resulta que el oráculo y MySQL ni siquiera residían en la misma red, de ahí mi solución rápida y sucia usando el cargador de .net. Realmente necesito mirar ese OracleBulkCopy. Cualquier puntero? – Salamander2007

16

La solución de Rob Stevenson-Legget es lenta porque no vincula sus valores, pero usa cadena.Formato().

Cuando le pide a Oracle que ejecute un enunciado sql, comienza con el cálculo del valor de esta declaración. Después de eso, busca en una tabla hash si ya conoce esta afirmación. Si ya conoce su declaración, puede recuperar su ruta de ejecución desde esta tabla hash y ejecutar esta declaración muy rápido porque Oracle ha ejecutado esta declaración antes. Esto se denomina memoria caché de la biblioteca y no funciona correctamente si no vincula sus instrucciones sql.

Por ejemplo, no lo haga:

int n;

for (n = 0; n < 100000; n ++) 
    { 
     mycommand.CommandText = String.Format("INSERT INTO [MyTable] ([MyId]) VALUES({0})", n + 1); 
     mycommand.ExecuteNonQuery(); 
    } 

pero lo hacen:

 OracleParameter myparam = new OracleParameter(); 
     int n; 

     mycommand.CommandText = "INSERT INTO [MyTable] ([MyId]) VALUES(?)"; 
     mycommand.Parameters.Add(myparam); 

     for (n = 0; n < 100000; n ++) 
     { 
     myparam.Value = n + 1; 
     mycommand.ExecuteNonQuery(); 
     } 

No utilizando parámetros también pueden causar la inyección de SQL.

+0

Eliminé mi respuesta porque Theo tiene razón con el enlace de la base de datos. –

+0

¡Gran respuesta publicada hace casi 5 años! :) ¿Dónde está Theo? –

26

Estoy cargando 50.000 registros en 15 segundos o menos, usando la matriz de unión en ODP.NET

Funciona mediante la invocación repetida a un procedimiento almacenado que especifique (y en el que se puede hacer actualizaciones/inserciones/eliminaciones), pero transfiere los valores de parámetros múltiples de .NET a la base de datos de forma masiva.

En lugar de especificar un valor único para cada parámetro del procedimiento almacenado, especifique una matriz de valores para cada parámetro.

Oracle pasa las matrices de parámetros de .NET a la base de datos de una sola vez, y luego invoca repetidamente el procedimiento almacenado que especifique usando los valores de parámetros que ha especificado.

http://www.oracle.com/technetwork/issue-archive/2009/09-sep/o59odpnet-085168.html

/Damian

+0

+1 Esta es la respuesta correcta con una solución .NET. – Christian13467

+0

Eso funciona muy bien con las matrices, pero está escribiendo una tabla de datos en la base de datos. ¿Cómo se usan los enlaces de matriz a una tabla de datos en ODP.Net? – mcauthorn

+0

Para cada fila en la tabla de datos, los valores de columna individuales deben agregarse a la matriz de valores (un valor por fila) para esa columna, y luego debe invocarse la inserción única utilizando el enlace de matriz. – Damian

2

Oracle dice (http://www.oracle.com/technology/products/database/utilities/htdocs/sql_loader_overview.html)

SQL * Loader es el método principal para poblando rápidamente las tablas de Oracle con datos de archivos externos

Mi experiencia es que su loade r carga sus tablas más rápido que cualquier otra cosa.

+3

Sin embargo, no estoy seguro de que tenga una API .NET. Creo que la pregunta original fue re .NET. Además, no era suficiente para mis propósitos, ya que solo inserta, quería ACTUALIZAR/INSERTAR dependiendo de si los registros ya existían. Sin embargo, podría ser suficiente para el interrogador. – Damian

+0

SQL * Loader es una herramienta de carga de datos de Oracle. Puede manejar una gran cantidad de datos almacenados en csv, txt u otros archivos. Exporte sus datos de mysql a formato csv y escriba un script de cargador para transferir los datos a Oracle. – Christian13467

+0

El enlace está muerto – billybob

2

Para dar seguimiento a la sugerencia de Theo con mis hallazgos (disculpas - Yo actualmente no tienen la reputación suficiente para publicar esto como un comentario)

En primer lugar, se trata de cómo utilizar varios parámetros con nombre:

String commandString = "INSERT INTO Users (Name, Desk, UpdateTime) VALUES (:Name, :Desk, :UpdateTime)"; 
using (OracleCommand command = new OracleCommand(commandString, _connection, _transaction)) 
{ 
    command.Parameters.Add("Name", OracleType.VarChar, 50).Value = strategy; 
    command.Parameters.Add("Desk", OracleType.VarChar, 50).Value = deskName ?? OracleString.Null; 
    command.Parameters.Add("UpdateTime", OracleType.DateTime).Value = updated; 
    command.ExecuteNonQuery(); 
} 

sin embargo, no vi ninguna variación en la velocidad entre:

  • la construcción de un nuevo commandString para cada fila (String.Format)
  • construcción de un commandString ahora con parámetros para cada fila
  • utilizando un único commandString y cambiar los parámetros

que estoy usando System.Data.OracleClient, borrar e insertar 2500 filas dentro de una transacción

21

Recientemente descubrió una clase especializada que es increíble para una inserción masiva (ODP.NET). Oracle.DataAccess.Client.OracleBulkCopy! Toma una tabla de datos como parámetro, luego llama al método WriteTOServer ... es muy rápido y efectivo, ¡buena suerte!

+0

¿Funcionará con Compact Framework también? –

1

Supongo que OracleBulkCopy es una de las formas más rápidas. Tuve algunos problemas para aprender, que necesitaba una nueva versión de ODAC. Cf. Where is type [Oracle.DataAccess.Client.OracleBulkCopy] ?

Aquí está el código completo de PowerShell para copiar desde una consulta en una tabla Oracle adecuada. Probé Sql-Server un origen de datos, pero otras fuentes válidas OLE-DB irán a.

if ($ora_dll -eq $null) 
{ 
    "Load Oracle dll" 
    $ora_dll = [System.Reflection.Assembly]::LoadWithPartialName("Oracle.DataAccess") 
    $ora_dll 
} 

# sql-server or Oracle source example is sql-server 
$ConnectionString ="server=localhost;database=myDatabase;trusted_connection=yes;Provider=SQLNCLI10;" 

# Oracle destination 
$oraClientConnString = "Data Source=myTNS;User ID=myUser;Password=myPassword" 

$tableName = "mytable" 
$sql = "select * from $tableName" 

$OLEDBConn = New-Object System.Data.OleDb.OleDbConnection($ConnectionString) 
$OLEDBConn.open() 
$readcmd = New-Object system.Data.OleDb.OleDbCommand($sql,$OLEDBConn) 
$readcmd.CommandTimeout = '300' 
$da = New-Object system.Data.OleDb.OleDbDataAdapter($readcmd) 
$dt = New-Object system.Data.datatable 
[void]$da.fill($dt) 
$OLEDBConn.close() 
#Write-Output $dt 

if ($dt) 
{ 
    try 
    { 
     $bulkCopy = new-object ("Oracle.DataAccess.Client.OracleBulkCopy") $oraClientConnString 
     $bulkCopy.DestinationTableName = $tableName 
     $bulkCopy.BatchSize = 5000 
     $bulkCopy.BulkCopyTimeout = 10000 
     $bulkCopy.WriteToServer($dt) 
     $bulkcopy.close() 
     $bulkcopy.Dispose() 
    } 
    catch 
    { 
     $ex = $_.Exception 
     Write-Error "Write-DataTable$($connectionName):$ex.Message" 
     continue 
    } 
} 

BTW: Utilizo esto para copiar la tabla con columnas CLOB. No lo conseguí para trabajar usando los servidores vinculados cf. question on dba. No reintenté los servicios vinculados con el nuevo ODAC.

6

SQL Server SQLBulkCopy es deslumbrantemente rápido. Desafortunadamente, descubrí que OracleBulkCopy es mucho más lento.También tiene problemas:

  • Usted debe estar muy seguro de que los datos de entrada es limpio si va a uso OracleBulkCopy. Si se produce una violación de clave principal, se genera un ORA-26026 y parece ser irrecuperable. Intentar reconstruir el índice no ayuda y cualquier inserción posterior en la tabla falla, también insertos normales.
  • Incluso si los datos están limpios, encontré que OracleBulkCopy a veces se atasca dentro de WriteToServer. El problema parece depender del tamaño del lote. En mis datos de prueba, el problema sería en exactamente el mismo punto de mi prueba cuando lo repito. Use un tamaño de lote más grande o más pequeño , y el problema no ocurre. Veo que la velocidad es más irregular en tamaños de lote más grandes, esto indica a problemas relacionados con la administración de memoria.

Actualmente System.Data.OracleClient.OracleDataAdapter es más rápido que OracleBulkCopy si desea llenar una tabla con registros pequeños pero muchas filas. Sin embargo, necesita ajustar el tamaño del lote, BatchSize óptimo para OracleDataAdapter es más pequeño que para OracleBulkCopy.

Ejecuté mi prueba en una máquina con Windows 7 con un ejecutable x86 y el cliente ODP.Net de 32 bits 2.112.1.0. . OracleDataAdapter es parte de System.Data.OracleClient 2.0.0.0. Mi conjunto de prueba es de aproximadamente 600,000 filas con un tamaño de registro de máx. 102 bytes (tamaño promedio 43 caracteres). La fuente de datos es un archivo de texto de 25 MB, leído en línea por línea como una secuencia.

En mi prueba construí la tabla de datos de entrada con un tamaño de tabla fijo y luego utilicé OracleBulkCopy u OracleDataAdapter para copiar el bloque de datos al servidor. Dejé BatchSize como 0 en OracleBulkCopy (para que el contenido de la tabla actual se copiara como un lote) y lo configuré en el tamaño de la tabla en OracleDataAdapter (nuevamente, eso debería crear un solo lote internamente). los mejores resultados:

  • OracleBulkCopy: tamaño de la tabla = 500, la duración total de 4'22"
  • OracleDataAdapter: tamaño de la tabla = 100, la duración total de 3'03"

para la comparación:

  • SqlBulkCopy: tamaño de la tabla = 1,000, duración total 0'15"
  • SqlDataAdapter: tamaño de la tabla = 1,000, duración total 8'05"

Mismo cliente máquina, servidor de prueba es SQL Server 2008 R2. Para SQL Server, la copia masiva es claramente la mejor manera de hacerlo. No solo es más rápido en general, sino que la carga del servidor también es menor que cuando se usa el adaptador de datos. Es una lástima que OracleBulkCopy no ofrezca la misma experiencia: la API de BulkCopy es mucho más fácil de usar que DataAdapter.

+0

Tiene toda la razón sobre la falta de memoria en el controlador de datos de Oracle. Experimenté exactamente los mismos problemas cuando se varía el número de filas de inserción. Además, cuando itero a través de lotes grandes de insertos, el rendimiento se degrada con el tiempo hasta que lotes simples de 10 insertos pueden tomar hasta 5 minutos. – grenade

2

Encontrando los ejemplos enlazados algo confusos, elaboré un código que muestra una inserción de matriz en funcionamiento en una tabla de prueba (jkl_test). Aquí está la tabla:

create table jkl_test (id number(9)); 

Aquí está el código .Net para una aplicación de consola simple que se conecta a Oracle usando ODP.Net e inserta una serie de 5 números enteros:

using Oracle.DataAccess.Client; 

namespace OracleArrayInsertExample 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      // Open a connection using ODP.Net 
      var connection = new OracleConnection("Data Source=YourDatabase; Password=YourPassword; User Id=YourUser"); 
      connection.Open(); 

      // Create an insert command 
      var command = connection.CreateCommand(); 
      command.CommandText = "insert into jkl_test values (:ids)"; 

      // Set up the parameter and provide values 
      var param = new OracleParameter("ids", OracleDbType.Int32); 
      param.Value = new int[] { 22, 55, 7, 33, 11 }; 

      // This is critical to the process; in order for the command to 
      // recognize and bind arrays, an array bind count must be specified. 
      // Set it to the length of the array. 
      command.ArrayBindCount = 5; 
      command.Parameters.Add(param); 
      command.ExecuteNonQuery(); 
     } 
    } 
} 
0

Si está utilizando el cliente de Oracle no administrado (Oracle.DataAccess), entonces la forma más rápida es utilizar OracleBulkCopy, como se indicó por Tarik.

Si está utilizando el último cliente Oracle administrado (Oracle.ManagedDataAccess), entonces la forma más rápida es utilizar el enlace de matriz, como señaló Damien. Si desea mantener el código de su aplicación limpio de los detalles vinculantes de la matriz, puede escribir su propia implementación de OracleBulkCopy utilizando el enlace de matriz.

Aquí es un ejemplo de uso del proyecto real:

var bulkWriter = new OracleDbBulkWriter(); 
    bulkWriter.Write(
     connection, 
     "BULK_WRITE_TEST", 
     Enumerable.Range(1, 10000).Select(v => new TestData { Id = v, StringValue=v.ToString() }).ToList()); 

10K registros se insertan en 500 ms!

Aquí es puesta en práctica:

public class OracleDbBulkWriter : IDbBulkWriter 
{ 
    public void Write<T>(IDbConnection connection, string targetTableName, IList<T> data, IList<ColumnToPropertyMapping> mappings = null) 
    { 
     if (connection == null) 
     { 
      throw new ArgumentNullException(nameof(connection)); 
     } 
     if (string.IsNullOrEmpty(targetTableName)) 
     { 
      throw new ArgumentNullException(nameof(targetTableName)); 
     } 
     if (data == null) 
     { 
      throw new ArgumentNullException(nameof(data)); 
     } 
     if (mappings == null) 
     { 
      mappings = GetGenericMappings<T>(); 
     } 

     mappings = GetUniqueMappings<T>(mappings); 
     Dictionary<string, Array> parameterValues = InitializeParameterValues<T>(mappings, data.Count); 
     FillParameterValues(parameterValues, data); 

     using (var command = CreateCommand(connection, targetTableName, mappings, parameterValues)) 
     { 
      command.ExecuteNonQuery(); 
     } 
    } 

    private static IDbCommand CreateCommand(IDbConnection connection, string targetTableName, IList<ColumnToPropertyMapping> mappings, Dictionary<string, Array> parameterValues) 
    { 
     var command = (OracleCommandWrapper)connection.CreateCommand(); 
     command.ArrayBindCount = parameterValues.First().Value.Length; 

     foreach(var mapping in mappings) 
     { 
      var parameter = command.CreateParameter(); 
      parameter.ParameterName = mapping.Column; 
      parameter.Value = parameterValues[mapping.Property]; 

      command.Parameters.Add(parameter); 
     } 

     command.CommandText = [email protected]"insert into {targetTableName} ({string.Join(",", mappings.Select(m => m.Column))}) values ({string.Join(",", mappings.Select(m => $":{m.Column}")) })"; 
     return command; 
    } 

    private IList<ColumnToPropertyMapping> GetGenericMappings<T>() 
    { 
     var accessor = TypeAccessor.Create(typeof(T)); 

     var mappings = accessor.GetMembers() 
      .Select(m => new ColumnToPropertyMapping(m.Name, m.Name)) 
      .ToList(); 

     return mappings; 
    } 

    private static IList<ColumnToPropertyMapping> GetUniqueMappings<T>(IList<ColumnToPropertyMapping> mappings) 
    { 
     var accessor = TypeAccessor.Create(typeof(T)); 
     var members = new HashSet<string>(accessor.GetMembers().Select(m => m.Name)); 

     mappings = mappings 
         .Where(m => m != null && members.Contains(m.Property)) 
         .GroupBy(m => m.Column) 
         .Select(g => g.First()) 
         .ToList(); 
     return mappings; 
    } 

    private static Dictionary<string, Array> InitializeParameterValues<T>(IList<ColumnToPropertyMapping> mappings, int numberOfRows) 
    { 
     var values = new Dictionary<string, Array>(mappings.Count); 
     var accessor = TypeAccessor.Create(typeof(T)); 
     var members = accessor.GetMembers().ToDictionary(m => m.Name); 

     foreach(var mapping in mappings) 
     { 
      var member = members[mapping.Property]; 

      values[mapping.Property] = Array.CreateInstance(member.Type, numberOfRows); 
     } 

     return values; 
    } 

    private static void FillParameterValues<T>(Dictionary<string, Array> parameterValues, IList<T> data) 
    { 
     var accessor = TypeAccessor.Create(typeof(T)); 
     for (var rowNumber = 0; rowNumber < data.Count; rowNumber++) 
     { 
      var row = data[rowNumber]; 
      foreach (var pair in parameterValues) 
      { 
       Array parameterValue = pair.Value; 
       var propertyValue = accessor[row, pair.Key]; 
       parameterValue.SetValue(propertyValue, rowNumber); 
      } 
     } 
    } 
} 

NOTA: esta aplicación utiliza el paquete Fastmember para el acceso optimizado a las propiedades (mucho más rápido que la reflexión)

+0

Su ejemplo de código no es utilizable porque el ejemplo omite algunas clases. * ColumnToPropertyMapping, * OracleCommandWrapper, * IDbBulkWriter – Markus1980Wien

Cuestiones relacionadas