2010-11-12 56 views
20

Mi empresa obtiene un conjunto de archivos CSV llenos de información de la cuenta bancaria cada mes que necesito importar a una base de datos. Algunos de estos archivos pueden ser bastante grandes. Por ejemplo, uno es de aproximadamente 33 MB y cerca de 65 000 líneas.Mejores prácticas para importar archivos CSV grandes

Ahora tengo una aplicación symfony/Doctrine (PHP) que lee estos archivos CSV y los importa en una base de datos. Mi base de datos tiene aproximadamente 35 tablas diferentes y en el proceso de importación, tomo estas filas, las divido en sus objetos constituyentes y las inserto en la base de datos. Todo funciona muy bien, excepto que es lento (cada fila ocupa aproximadamente un cuarto de segundo) y usa mucha memoria.

El uso de memoria es tan malo que tengo que dividir mis archivos CSV. Un archivo de 20,000 líneas casi no lo logra. Para cuando está cerca del final, tengo un 95% de uso de memoria. Importar ese archivo de 65,000 líneas simplemente no es posible.

He encontrado que Symfony es un marco excepcional para la construcción de aplicaciones y normalmente no consideraría usar nada más, pero en este caso estoy dispuesto a arrojar todas mis ideas preconcebidas por la ventana en nombre del rendimiento. No estoy comprometido con ningún lenguaje específico, DBMS ni nada.

A Stack Overflow no me gustan las preguntas subjetivas, así que trataré de hacerlo lo menos subjetivo posible: para aquellos que no tienen una opinión pero experiencia importando archivos CSV grandes, ¿qué herramientas/¿Tiene prácticas utilizadas en el pasado que han tenido éxito?

Por ejemplo, ¿acabas de usar el ORM/OOP de Django y no has tenido ningún problema? ¿O leíste todo el archivo CSV en la memoria y preparaste unas cuantas declaraciones genéricas INSERT?

Una vez más, no solo quiero una opinión, sino algo que realmente funcionó para ti en el pasado.

Editar: No solo estoy importando una hoja de cálculo CSV de 85 columnas en una tabla de base de datos de 85 columnas. Estoy normalizando los datos y poniéndolos en docenas de tablas diferentes. Por esta razón, no puedo simplemente usar LOAD DATA INFILE (estoy usando MySQL) o cualquier otra función de DBMS que solo lea en archivos CSV.

Además, no puedo usar ninguna solución específica de Microsoft.

+0

¿ha realizado algún análisis de rendimiento en el extremo de DB en términos de cómo se crean/comprometen las transacciones? –

+0

No. Toda mi importación está envuelta en una gran transacción. En cuanto a las declaraciones individuales de 'INSERT', no he realizado ningún análisis de rendimiento. Cualquier consejo allí sería apreciado. (Sin embargo, eso solo no resuelve mis problemas de uso de memoria.) –

Respuesta

10

Tuve exactamente el mismo problema hace 2 semanas. Escribí algunos .NET para hacer insertos ROW BY ROW y según mis cálculos con la cantidad de datos que tenía, tomaría alrededor de una semana hacerlo de esta manera.

Así que, en su lugar, utilicé un generador de cadenas para crear una consulta ENORME y la envié a mi sistema relacional de una sola vez. Pasó de tomar una semana a tomar 5 minutos. Ahora no sé qué sistema relacional estás usando, pero con enormes consultas probablemente tendrás que modificar tu parámetro max_allowed_packet o similar.

+0

@ Kmarks2: suena una solución interesante, pero eche un vistazo a mi solución a esta respuesta; aunque no es relevante para Jason, realmente puede haberlo ayudado, Bulk Insert es extremadamente rápido y si usted Usando .NET, entonces usted tiene control total sobre qué datos se insertan (es decir, no tiene que provenir de un archivo) –

+0

Interesante. ¿Cuántas filas se insertaron cada una de sus instrucciones 'INSERT'? (Estoy en MySQL, por cierto.) –

+1

@Jason había alrededor de 1.5 millones. – kmarks2

1

Si está utilizando el servidor Sql y tiene acceso a .NET, puede escribir una aplicación rápida para usar la clase SQLBulkCopy. Lo he usado en proyectos anteriores para obtener una gran cantidad de datos en SQL muy rápidamente. La clase SQLBulkCopy hace uso del BCP de SQL Server, por lo que si está utilizando algo que no sea .NET, puede valer la pena analizar si esa opción también está abierta para usted. No estoy seguro si está usando una base de datos que no sea SQL Server.

16

Perdóneme si no estoy entendiendo exactamente su problema correctamente, pero parece que está tratando de obtener una gran cantidad de datos CSV en una base de datos SQL. ¿Hay alguna razón por la que desee usar una aplicación web u otro código para procesar los datos CSV en las instrucciones INSERT? Tuve éxito importando grandes cantidades de datos CSV en SQL Server Express (versión gratuita) usando SQL Server Management Studio y usando BULK INSERT. Una inserción masiva simple se vería así:

BULK INSERT [Company].[Transactions] 
    FROM "C:\Bank Files\TransactionLog.csv" 
    WITH 
    (
     FIELDTERMINATOR = '|', 
     ROWTERMINATOR = '\n', 
     MAXERRORS = 0, 
     DATAFILETYPE = 'widechar', 
     KEEPIDENTITY 
    ) 
GO 
+0

+1 Buena respuesta. Esto también usa BCP (al igual que mi respuesta) pero el tuyo no requiere codificación. @Jason: Si un archivo llena varias tablas (creo que sí) luego BCP en una sola tabla y usa sentencias SQL por lotes para dividirlas en tablas relevantes, todavía debería ser más rápido que tu solución actual –

+1

La razón es porque estoy no solo importar una hoja de cálculo CSV de 85 columnas en una tabla de base de datos de 85 columnas. Estoy normalizando los datos y poniéndolos en diferentes tablas. –

+1

Jason: Gracias por la actualización, sí cambia un poco las cosas, pero las respuestas reales aún podrían ser válidas. Puede utilizar el método más rápido disponible para obtener datos en MySQL y luego hacer la normalización/división dentro de MySQL como instrucciones de proceso por lotes. –

1

que no me gustan algunas de las otras respuestas :)

que solía hacer esto en un trabajo.

Escribe un programa para crear un gran script SQL lleno de instrucciones INSERT, una por línea. Entonces, ejecutas el script. Puede guardar el script para futuras referencias (registro barato). Usa gzip y reducirá el tamaño al 90%.

No necesita ninguna herramienta sofisticada y realmente no importa qué base de datos está utilizando.

Puede hacer unos cientos Inserts por transacción o todos ellos en una sola transacción. Depende de usted.

Python es un buen lenguaje para esto, pero estoy seguro de que php también está bien.

Si tiene problemas de rendimiento, algunas bases de datos como Oracle tienen un programa especial de carga masiva que es más rápido que las instrucciones INSERT.

Debe quedarse sin memoria porque solo debe analizar una línea por vez. No es necesario que guardes todo en la memoria, ¡no hagas eso!

+0

Pure Genius, Resolvió mi problema. versión más simple: la importación Dont Ahora, Crear archivo sql y su posterior importación (de preferencia con una herramienta de importación de SQL como http://www.mysqldumper.net/ para manejar la importación real grande) Convert y luego importar. – iGNEOS

0

Estoy leyendo un archivo CSV que tiene cerca de 1 millón de registros y 65 columnas. Cada 1000 registros procesados ​​en PHP, hay una gran declaración de MySQL que va a la base de datos. La escritura no lleva mucho tiempo. Es el análisis lo que hace. La memoria utilizada para procesar este archivo descomprimido de 600 MB es de aproximadamente 12 MB.

0

Necesito hacer esto también de vez en cuando (importo grandes CSV no estandarizados donde cada fila crea una docena de objetos DB relacionados) así que escribí un script python donde puedo especificar qué va y dónde está relacionado. El script simplemente genera instrucciones INSERT.

Aquí está: csv2db

responsabilidad: Soy básicamente un novato cuando se trata de bases de datos, lo que puede haber mejores maneras de lograr esto.

4

Primero: 33MB es no grande. MySQL puede manejar fácilmente datos de este tamaño.

Como habrás notado, la inserción fila por fila es lenta. Usar un ORM además es aún más lento: hay gastos generales para construir objetos, serialización, etc. Usar un ORM para hacer esto en 35 tablas es incluso más lento. No hagas esto

De hecho, puede usar LOAD DATA INFILE; solo escriba un script que transforme sus datos en el formato deseado, separándolo en archivos por tabla en el proceso. A continuación, puede LOAD cada archivo en la tabla adecuada. Este script puede escribirse en cualquier idioma.

Aparte de eso, a granel INSERT (column, ...) VALUES ... también funciona.No adivine cuál debe ser el tamaño de su lote de fila; tiempo que empíricamente, como el tamaño de lote óptimo dependerá de la configuración de la base de datos en particular (configuración del servidor, los tipos de columnas, índices, etc.)

granel INSERT no va a ser tan rápido como LOAD DATA INFILE, y se le todavía tiene que escribir una secuencia de comandos para transformar los datos sin procesar en consultas utilizables INSERT. Por esta razón, probablemente haría LOAD DATA INFILE si fuera posible.

2

Usted puede usar MySQL LOAD DATA INFILE statemnt, que le permite leer datos de un archivo de texto e importar los datos del archivo en una tabla de base de datos muy rápido ..

LOAD DATA INFILE '/opt/lampp/htdocs/sample.csv' INTO TABLE discounts FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (title,@expired_date,discount) SET expired_date = STR_TO_DATE(@expired_date, '%m/%d/%Y');

para más información: http://dev.mysql.com/doc/refman/5.5/en/load-data.html y http://www.mysqltutorial.org/import-csv-file-mysql-table/

4

Fwiw los siguientes pasos causaron un enorme aumento de velocidad de mi LOAD DATA INFILE:

SET FOREIGN_KEY_CHECKS = 0; 
SET UNIQUE_CHECKS = 0; 
SET SESSION tx_isolation='READ-UNCOMMITTED'; 
SET sql_log_bin = 0; 
#LOAD DATA LOCAL INFILE.... 
SET UNIQUE_CHECKS = 1; 
SET FOREIGN_KEY_CHECKS = 1; 
SET SESSION tx_isolation='READ-REPEATABLE'; 

Ver el artículo here

+0

Esto tomó mi inserción de datos de carga para 18 millones de filas de 20 minutos a 11. ¡Super útil! –

0

Puede usar el generador para el archivo de memoria eficiente listo. El pequeño fragmento a continuación podría ser útil.

#Method 
public function getFileRecords($params) 
{ 
    $fp = fopen('../' . $params['file'] . '.csv', 'r'); 
    //$header = fgetcsv($fp, 1000, ','); // skip header 

    while (($line = fgetcsv($fp, 1000, ',')) != FALSE) { 
     $line = array_map(function($str) { 
      return str_replace('\N', '', $str); 
     }, $line); 

     yield $line; 
    } 

    fclose($fp); 

    return; 
} 

#Implementation 
foreach ($yourModel->getFileRecords($params) as $row) { 
    // you get row as an assoc array; 
    $yourModel->save($row); 
} 
Cuestiones relacionadas