Ruby 3.4, Rails, MySQL 8. Выгрузка заказов за год в CSV для бухгалтерии идёт фоновой джобой. Пока было 300 тысяч строк, жила спокойно, сейчас 2 млн, процесс воркера раздувается до 3 ГБ, и его убивает OOM. Декабрь, в четыре уже темно, все хотят годовые выгрузки, а у меня падает. Код:
csv = CSV.generate do |rows|
rows << %w[id number total created_at]
Order.where(created_at: Time.zone.local(2025).all_year).each do |o|
rows << [o.id, o.number, o.total, o.created_at]
end
end
File.write(path, csv)
Что всё в памяти, понимаю, но что из этого самое тяжёлое?
Там три потребителя. .each без батчей грузит все 2 млн объектов Active Record разом, каждый объект это сотни байт плюс строки атрибутов. Потом вся CSV-строка целиком в памяти. Потом ещё копия при записи. Я бы писал в файл сразу построчно и брал из базы только нужные колонки пачками:
CSV.open(path, "w") do |csv|
csv << %w[id number total created_at]
Order.where(created_at: Time.zone.local(2025).all_year)
.in_batches(of: 5_000) do |batch|
batch.pluck(:id, :number, :total, :created_at).each { |row| csv << row }
end
end
pluck вообще не создаёт объектов моделей, только массивы. До правки снимите memory_profiler на 50 тысячах строк, чтобы было с чем сравнить.
В Laravel для такого был lazy() поверх чанков, тут in_batches по сути то же самое
Переписал по примеру Тимура. На 2 млн строк воркер держится в районе 180 МБ, выгрузка идёт 2 минуты 40 секунд, а раньше падала на девятой минуте. memory_profiler на 50 тысячах показал 410 МБ выделенной памяти до правки и 23 МБ после.
Про in_batches одна мелочь: он ходит по первичному ключу, каждый раз WHERE id > последний, это быстро. А если добавить свой order, он его проигнорирует и только предупредит в логе. Если нужна сортировка по дате, проще выгружать помесячно.
а бухгалтерия файл на 2 млн строк чем открывает?) табличные редакторы обычно на миллионе с небольшим строк и заканчиваются