【问题标题】:Import CSV data faster in rails在 Rails 中更快地导入 CSV 数据
【发布时间】:2016-10-11 10:52:21
【问题描述】:

我正在构建一个导入模块以从 csv 文件导入大量订单。我有一个名为 Order 的模型,需要在其中存储数据。

Order 模型的简化版本如下

sku
quantity
value
customer_email
order_date
status

在导入数据时,必须发生两件事

  1. 任何日期或货币都需要清理,即日期在 csv 中表示为字符串,这需要转换为 Rails 日期对象,货币需要通过删除任何逗号或美元符号转换为小数
  2. 如果行已存在,则必须对其进行更新,则根据两列检查唯一性。

目前我使用一个简单的 csv 导入代码

CSV.foreach("orders.csv") do |row|
  order = Order.first_or_initialize(sku: row[0], customer_email: row[3])
  order.quantity = row[1]
  order.value= parse_currency(row[2])
  order.order_date = parse_date(row[4])
  order.status = row[5]
  order.save!
end

其中 parse_currency 和 parse_date 是用于从字符串中提取值的两个函数。在日期的情况下,它只是 Date.strptime 的包装器。

我可以添加一个检查来查看记录是否已经存在,如果它已经存在则什么也不做,这样可以节省一点时间。但我正在寻找明显更快的东西。目前导入大约 100k 行大约需要 30 分钟,而数据库是空的。随着数据大小的增加,它会变慢。

所以我基本上是在寻找一种更快的方式来导入数据。

任何帮助将不胜感激。

编辑

在此处基于 cmets 进行了更多测试后,我有一个观察和一个问题。我不确定他们是否应该去这里,或者我是否需要为这些问题打开一个新线程。因此,如果我必须将其移至单独的问题,请告诉我。

我使用 Postgres 复制运行了一个测试,从文件中导入数据,只用了不到一分钟。我只是将数据导入到一个新表中,没有任何验证。所以导入可以更快。

Rails 开销似乎来自 2 个地方

  1. 正在发生的多个数据库调用,即每行的 first_or_initialize。这最终变成了多个 SQL 调用,因为它必须首先找到记录,然后更新它,然后保存它。
  2. 带宽。每次调用 SQL 服务器时,数据都会来回流动,这加起来会花费很多时间

现在回答我的问题。如何将更新/创建逻辑移动到数据库,即如果订单已经基于 sku 和 customer_email 存在,则需要更新记录,否则需要创建新记录。目前使用 rails 我正在使用 first_or_initialize 方法来获取记录以防它存在并更新它,否则我正在创建一个新记录并保存它。我如何在 SQL 中做到这一点。

我可以使用 ActiveRecord 连接执行来运行原始 SQL 查询,但我认为这不是一种非常优雅的方式。有更好的方法吗?

【问题讨论】:

  • 您好,欢迎来到 Stack Overflow。曾经有一个名为FasterCSV 的宝石 - 不确定它是否仍然是最新的......可能值得研究。也就是说 - 谷歌“rails csv gem fast”或其他东西看看是否有其他......
  • 谢谢。我确实在搜索中遇到了 faster_csv,但它已经几年没有更新了,所以我并没有真正花很多时间在上面。我查看了 smarter_csv ,它看起来像是一个可能的选项,它允许您分块处理 csv。但我无法真正弄清楚如何在这些块上运行 first_or_initialize,因为我必须在 smarter_csv 返回的每个项目上运行它们。根据我的研究,我认为要走的路是数据库事务,但我不确定如何在 rails 中做到这一点并使其表现得像 first_or_initialize。
  • 嗯,你应该可以在返回的任何行上使用first_or_initialize...一个块只是一个行数组不是吗?
  • 考虑完全绕过 ActiveRecord。在纯 Ruby 中对 CSV 进行任何您需要做的处理,然后将 CSV 直接发送到数据库。 MySQL 和 Postgres 都有导入 CSV 的机制,而且速度非常快。
  • 是的,如果您没有任何回调和验证(Ruby 对象实例化需要大量时间)并使用数据库的唯一性约束作为检查,请按照 Matt 所说的操作。

标签: ruby-on-rails


【解决方案1】:

由于 ruby​​ 1.9 fastcsv 现在是 ruby​​ 核心的一部分。您不需要使用特殊的宝石。只需使用CSV

10 万条记录 ruby​​ 需要 0.018 秒/记录。在我看来,您的大部分时间都将用于Order.first_or_initialize。您的这部分代码需要额外往返于您的数据库。 ActiveRecord 的初始化也需要时间。但要确定的是,我建议您对代码进行基准测试。

Benchmark.bm do |x|
   x.report("CSV evel") { CSV.foreach("orders.csv") {} }
   x.report("Init: ") { 1.upto(100_000) {Order.first_or_initialize(sku:  rand(...), customer_email: rand(...))} } # use rand query to prevent query caching 
   x.report('parse_currency') { 1.upto(100_000) { parse_currency(...} }
   x.report('parse_date') { 1.upto(100_000) { parse_date(...} }
end

您还应该在导入期间注意内存消耗。可能垃圾收集运行不够频繁,或者对象没有清理干净。

为了加快速度,您可以按照 Matt Brictson 的提示绕过 ActiveRecord。 您可以尝试 gem activerecord-import 或者您可以开始并行化,例如使用fork 进行多处理或使用Thread.new 进行多线程。

【讨论】:

  • +1 用于使用 activerecord-import 批量导入大量数据。通过避免为每条记录往返数据库,它将大大加快您的导入速度。
  • 谢谢。根据您所说的,我正在做一些测试以找出问题所在,并意识到当我将它推送到 heroku 时我遇到了额外的速度问题,因为它在超过 10K 行后内存不足,文件更大.所以我想知道是否有一些方法可以手动调用垃圾收集?
  • 您可以致电GC.start。这个调用很昂贵,因此性能可能会进一步下降。有时循环引用会导致内存泄漏。在这种情况下,Ruby 无法识别不再需要某个对象。有时它有助于例如在循环结束时将变量显式设置为 nil,例如 order = nil; row = nil
猜你喜欢
  • 2020-11-09
  • 2012-07-27
  • 2013-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多