【发布时间】:2016-10-11 10:52:21
【问题描述】:
我正在构建一个导入模块以从 csv 文件导入大量订单。我有一个名为 Order 的模型,需要在其中存储数据。
Order 模型的简化版本如下
sku
quantity
value
customer_email
order_date
status
在导入数据时,必须发生两件事
- 任何日期或货币都需要清理,即日期在 csv 中表示为字符串,这需要转换为 Rails 日期对象,货币需要通过删除任何逗号或美元符号转换为小数
- 如果行已存在,则必须对其进行更新,则根据两列检查唯一性。
目前我使用一个简单的 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 个地方
- 正在发生的多个数据库调用,即每行的 first_or_initialize。这最终变成了多个 SQL 调用,因为它必须首先找到记录,然后更新它,然后保存它。
- 带宽。每次调用 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