Быстрее импортировать данные CSV в rails

#ruby-on-rails

#ruby-on-rails

Вопрос:

Я создаю модуль импорта для импорта большого набора заказов из файла csv. У меня есть модель под названием 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 это просто оболочка для Date.strptime.

Я могу добавить проверку, чтобы увидеть, существует ли запись уже, и ничего не делать, если она уже существует, и это должно сэкономить немного времени. Но я ищу что-то, что значительно быстрее. В настоящее время импорт около 100 тыс. строк занимает около 30 минут с пустой базой данных. Это будет замедляться по мере увеличения размера данных.

Поэтому я в основном ищу более быстрый способ импорта данных.

Любая помощь будет оценена.

Редактировать

После еще нескольких тестов, основанных на комментариях здесь, у меня есть замечание и вопрос. Я не уверен, должны ли они идти сюда или мне нужно открыть новую тему для вопросов. Поэтому, пожалуйста, дайте мне знать, если мне нужно перенести это в отдельный вопрос.

Я провел тест с использованием Postgres copy для импорта данных из файла, и это заняло менее минуты. Я просто импортировал данные в новую таблицу без каких-либо проверок. Таким образом, импорт может быть намного быстрее.

Накладные расходы Rails, похоже, поступают из 2 мест

  1. Происходит несколько вызовов базы данных, т.Е. first_or_initialize для каждой строки. В конечном итоге это становится несколькими вызовами SQL, потому что сначала нужно найти запись, а затем обновить ее, а затем сохранить.
  2. Пропускная способность. Каждый раз, когда вызывается SQL server, данные перемещаются туда и обратно, что занимает много времени

Теперь мой вопрос. Как мне перенести логику обновления / создания в базу данных, т.Е. Если заказ уже существует на основе sku и customer_email, ему необходимо обновить запись, иначе необходимо создать новую запись. В настоящее время в rails я использую метод first_or_initialize, чтобы получить запись, если она существует, и обновить ее, иначе я создаю новую и сохраняю ее. Как мне это сделать в SQL.

Я мог бы запустить необработанный SQL-запрос, используя ActiveRecord connection execute, но я не думаю, что это был бы очень элегантный способ сделать это. Есть ли лучший способ сделать это?

Комментарии:

1. Привет и добро пожаловать в Stack Overflow. Раньше назывался драгоценный FasterCSV камень — не уверен, что он все еще актуален …. возможно, стоит изучить. Тем не менее, — google для «rails csv gem fast» или что-то еще, чтобы узнать, есть ли другие…

2. Спасибо за это. Я наткнулся на faster_csv в своем поиске, но он не обновлялся в течение нескольких лет, поэтому я не тратил на это много времени. Я посмотрел на smarter_csv, и это выглядит как возможный вариант, он позволяет обрабатывать csv по частям. Но я не мог понять, как запустить first_or_initialize для этих фрагментов, поскольку мне пришлось бы запускать их для каждого элемента, возвращаемого smarter_csv. Основываясь на моих исследованиях, я думаю, что правильным решением будут транзакции с базой данных, но я не уверен, как это сделать в rails и заставить его вести себя как first_or_initialize.

3. хммм, вы должны иметь возможность использовать first_or_initialize в любой возвращаемой строке … фрагмент — это просто массив строк, не так ли?

4. Рассмотрите возможность полного обхода ActiveRecord. Выполните любую необходимую обработку CSV в обычном Ruby, а затем отправьте CSV непосредственно в базу данных. У MySQL и Postgres есть механизмы для импорта CSV, и они невероятно быстры.

5. Да, делайте то, что сказал Мэтт, если у вас нет обратных вызовов и проверок (создание экземпляра объекта Ruby занимает значительное время) и используйте ограничения уникальности в базе данных в качестве проверки.

Ответ №1:

Начиная с ruby 1.9, fastcsv теперь является частью ruby core. Вам не нужно использовать специальный драгоценный камень. Просто используйте CSV .

При 100 тыс. записей ruby занимает 0,018 сек / запись. На мой взгляд, большая часть вашего времени будет использоваться внутри Order.first_or_initialize . Эта часть вашего кода требует дополнительного перехода к вашей базе данных. Инициализация an 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
  

Вы также должны следить за потреблением памяти во время импорта. Возможно, сборка мусора выполняется недостаточно часто или объекты не очищаются.

Чтобы увеличить скорость, вы можете следовать подсказке Мэтта Бриктсона и обходить ActiveRecord . Вы можете попробовать gem activerecord-import или начать работать параллельно, например, с многопроцессорной fork обработкой или многопоточностью Thread.new .

Комментарии:

1. 1 за использование activerecord-import для пакетного импорта больших наборов данных. Это значительно ускорит ваш импорт, избегая циклического обращения к базе данных для каждой записи.

2. Спасибо за это. Основываясь на том, что вы сказали, я провожу некоторое тестирование, чтобы выяснить, в чем заключаются проблемы, и понял, что у меня возникают дополнительные проблемы со скоростью, когда я отправляю его в heroku, потому что у него заканчивается память с файлами большего размера после того, как он прошел 10 тыс. строк или около того. Итак, мне было интересно, есть ли какой-нибудь способ вручную вызвать сборку мусора?

3. Вы можете позвонить GC.start . Этот вызов является дорогостоящим, поэтому производительность может упасть еще больше. Иногда ссылки на ciruclar могут приводить к утечкам памяти. В таких случаях Ruby не может распознать, что объект больше не нужен. Иногда это помогает, например, устанавливать переменные явно равными нулю, например order = nil; row = nil , в конце цикла.