#ruby-on-rails
#ruby-on-rails
Вопрос:
Я создаю модуль импорта для импорта большого набора заказов из файла csv. У меня есть модель под названием 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 это просто оболочка для Date.strptime.
Я могу добавить проверку, чтобы увидеть, существует ли запись уже, и ничего не делать, если она уже существует, и это должно сэкономить немного времени. Но я ищу что-то, что значительно быстрее. В настоящее время импорт около 100 тыс. строк занимает около 30 минут с пустой базой данных. Это будет замедляться по мере увеличения размера данных.
Поэтому я в основном ищу более быстрый способ импорта данных.
Любая помощь будет оценена.
Редактировать
После еще нескольких тестов, основанных на комментариях здесь, у меня есть замечание и вопрос. Я не уверен, должны ли они идти сюда или мне нужно открыть новую тему для вопросов. Поэтому, пожалуйста, дайте мне знать, если мне нужно перенести это в отдельный вопрос.
Я провел тест с использованием Postgres copy для импорта данных из файла, и это заняло менее минуты. Я просто импортировал данные в новую таблицу без каких-либо проверок. Таким образом, импорт может быть намного быстрее.
Накладные расходы Rails, похоже, поступают из 2 мест
- Происходит несколько вызовов базы данных, т.Е. first_or_initialize для каждой строки. В конечном итоге это становится несколькими вызовами SQL, потому что сначала нужно найти запись, а затем обновить ее, а затем сохранить.
- Пропускная способность. Каждый раз, когда вызывается 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
, в конце цикла.