2015-12-29 80 views
2

我有一个csv文件,其范围可以从50k到超过100k行数据。使用Laravel在MySQL中导入大型CSV文件

我目前使用Laravel w/Laravel Forge,MySQL和Maatwebsite Laravel Excel软件包。

这是由最终用户使用,而不是自己,所以我创建了一个简单的表单上我的刀片鉴于这样:

{!! Form::open(
    array(
     'route' => 'import.store', 
     'class' => 'form', 
     'id' => 'upload', 
     'novalidate' => 'novalidate', 
     'files' => true)) !!} 

    <div class="form-group"> 
     <h3>CSV Product Import</h3> 
     {!! Form::file('upload_file', null, array('class' => 'file')) !!} 
    </div> 

    <div class="form-group"> 
     {!! Form::submit('Upload Products', array('class' => 'btn btn-success')) !!} 
    </div> 
{!! Form::close() !!} 

这则存储在服务器上的文件,成功地和我现在可以使用诸如foreach循环之类的东西遍历结果。

现在,这里是我面临的时间顺序和修复/企图的问题: (10K行测试CSV文件)

  1. [问题] PHP超时。
  2. [remedy]将其更改为通过作业命令异步运行。
  3. [结果]进口多达1500行。
  4. [问题]服务器内存不足。
  5. [补救]增加了1GB的交换驱动器。
  6. [结果]最多可导入3000行。
  7. [问题]服务器内存不足。
  8. [补救]打开每个块的250行分块结果。
  9. [结果]最多可导入5000行。
  10. [问题]服务器内存不足。
  11. [修正]删除了一些转置/连接表逻辑。
  12. [结果]进口多达7000行。

正如你所看到的结果是边际和远不及50k,我几乎可以使它接近10k。

我读过了,看着可行的建议,如:

  • 使用原始查询运行LOAD DATA LOCAL INFILE。
  • 导入前分割文件。
  • 在服务器上存储,然后将服务器分割成文件并使用cron处理它们。
  • 作为最后的手段将我的512mb DO溶滴升级到1GB。

与LOAD DATA LOCAL INFILE走向可能无法工作,因为我的标题列可能每个文件,这就是为什么我有逻辑处理/遍历它们改变。

在导入之前拆分文件在10k以下是不错的,但是对于50k以上的版本吗?这将是非常不切实际的。

存储在服务器上,然后让服务器拆分它并单独运行它们,而不会让最终用户困扰?可能但不确定如何在PHP中实现这一点,但只是简要阅读一下。

另外要注意,我的队列工作设置10000秒,这也是非常不切实际和坏实践超时,但似乎这是它会继续运行内存占用一击之前的唯一途径。

现在我可以给,并刚刚升级显存为1GB,但我觉得充其量再次失败之前它可以跳到我20K行。有些东西需要快速高效地处理所有这些行。

最后,这里是我的表结构的一瞥:

Inventory 
+----+------------+-------------+-------+---------+ 
| id | profile_id | category_id | sku | title | 
+----+------------+-------------+-------+---------+ 
| 1 |   50 |  51234 | mysku | mytitle | 
+----+------------+-------------+-------+---------+ 

Profile 
+----+---------------+ 
| id |  name  | 
+----+---------------+ 
| 50 | myprofilename | 
+----+---------------+ 

Category 
+----+------------+--------+ 
| id | categoryId | name | 
+----+------------+--------+ 
| 1 |  51234 | brakes | 
+----+------------+--------+ 

Specifics 
+----+---------------------+------------+-------+ 
| id | specificsCategoryId | categoryId | name | 
+----+---------------------+------------+-------+ 
| 1 |     20 |  57357 | make | 
| 2 |     20 |  57357 | model | 
| 3 |     20 |  57357 | year | 
+----+---------------------+------------+-------+ 

SpecificsValues 
+----+-------------+-------+--------+ 
| id | inventoryId | name | value | 
+----+-------------+-------+--------+ 
| 1 |   1 | make | honda | 
| 2 |   1 | model | accord | 
| 3 |   1 | year | 1998 | 
+----+-------------+-------+--------+ 

Full CSV Sample 
+----+------------+-------------+-------+---------+-------+--------+------+ 
| id | profile_id | category_id | sku | title | make | model | year | 
+----+------------+-------------+-------+---------+-------+--------+------+ 
| 1 |   50 |  51234 | mysku | mytitle | honda | accord | 1998 | 
+----+------------+-------------+-------+---------+-------+--------+------+ 

所以我的逻辑流程尽可能简单的快速运行,通过将是:

  1. 加载文件到Maatwebsite/Laravel -Excel并通过分块循环
  2. 检查迭代如果CATEGORY_ID和SKU是空否则忽略并记录错误到一个数组。
  3. 查找category_id并从它使用的所有相关表中拉出所有相关的列字段,然后如果没有null插入数据库。
  4. 使用文件中可用字段的更多逻辑来生成自定义标题。
  5. 冲洗并重复。
  6. 最后将错误数组导出到文件中,并将其记录到数据库中以供下载,以便在最后查看错误。

我希望有人能和我一起上,我应该如何解决这个同时牢记使用Laravel的一些可能的想法分享一些见解,也认为它不是一个简单的上传我需要处理并投入不同的相关表每行其他我会加载数据infile它一次。

谢谢!

+0

所有的csv文件都被插入到同一个表中吗?如果是这种情况,我不明白为什么使用'load data local infile'会是一个问题 - 有些列只是'NULL'。您可以使用Python(通过'exec()')通过PHP子进程执行,以便在上载到服务器之后但在将其插入表之前根据需要解析文件。 – Terry

+0

@Terry它只是一个CSV文件,但如上所述插入到多个表中,为什么我无法轻松使用本地infile的加载数据。此外,每个文件的数据更改取决于涉及哪些categoryid,这些列将具有不同的列。也因为这个变量,现在很难指定每个字段的数据类型。 – dmotors

+0

如果它只是一个CSV文件,然后使用Maatwebsite Laravel Excel包和PHPExcel是矫枉过正,虽然Maatwebsite Laravel Excel包(我相信)提供访问PHPExcel chunking函数来加载文件 –

回答

4

你似乎已经想通了逻辑解释的CSV线,将它们转换为数据库中插入查询,所以我将专注于内存耗尽的问题。

当与PHP大型文件,整个文件加载到内存要么失败的任何方法,成为不能忍受缓慢或需要更多的内存比你滴了。

所以我的建议是:

使用fgetcsv

$handle = fopen('file.csv', 'r'); 
if ($handle) { 
    while ($line = fgetcsv($handle)) { 
     // Process this line and save to database 
    } 
} 

这样只有一行在将被加载到内存中的时间逐行读取文件中的行。然后,您可以处理它,保存到数据库,并用下一个覆盖它。

保持一个单独的文件句柄记录

你的服务器是短暂的记忆,所以错误记录到一个数组可能不是一个好主意,因为所有的错误都将被保存在它。如果您的csv有大量空skus和类别id的条目,那么这可能会成为问题。

Laravel出来与Monolog箱子,你可以尝试,以使其适应您的需求。但是,如果它最终还是使用了太多的资源,或者不适合您的需求,那么更简单的方法可能就是解决方案。

$log = fopen('log.txt', 'w'); 
if (some_condition) { 
    fwrite($log, $text . PHP_EOL); 
} 

然后,在脚本的末尾,您可以将日志文件存储到任何地方。

禁用Laravel的查询日志

Laravel保持存储在内存中您所有的疑问,而这可能是您的应用程序有问题。幸运的是,您可以使用disableQueryLog method来释放一些珍贵的RAM。

DB::connection()->disableQueryLog(); 

使用原始查询,如果需要的话

我认为这是不可能的,你将再次耗尽内存,如果你遵循这些提示,但你总是可以牺牲一些Laravel的便利,以提取最后一滴血的表现。

如果你知道你的SQL的方式,你可以execute raw queries to the database


编辑:

至于超时问题,您应该运行该代码为排队的任务,因为在意见建议不管。插入那么多行需要一些时间(特别是如果你有很多索引),并且用户不应该长时间盯着没有响应的页面。

+0

伟大的建议。我禁用了查询日志,并将我的Maatwebsite Laravel Excel转换为使用您建议的fgetcsv示例。它目前正在运行,我的记忆一直没有飙升。我确实有一个问题,一次只能用1块(使用laravel excel软件包)与fgetcsv 1一样分块,还是会一直妨碍并耗尽内存? – dmotors

+0

我不知道Laravel-Excel如何特别分块,所以我不能回答这个问题。但是,您可以非常轻松地修改fgetcsv循环,以便一次读取更多行,从而在不使用太多内存的情况下提高性能。 –

+0

它达到30k行,这是一个巨大的差异相比,7k。我的队列工作人员有一个10000秒的超时时间,所以我会把它提高到一个很高的数字,因为Laravel Forge似乎并没有让我没有超时。我会认为这是一个可行的解决方案。 – dmotors