/ Tags: RAILS-CODE / Categories: SOLUTIONS

Batch Process A Large Activerecord Query Without Memory Bloat

Order.all.each loads every row into memory before iterating. At forty thousand rows that is slow; at four million it is an out-of-memory kill. Rails has three batching methods and they solve slightly different problems.

Description

find_each yields one record at a time while loading a batch (1000 by default) behind the scenes. find_in_batches yields the array so you can do bulk work per batch. in_batches yields a relation, which lets you call update_all or delete_all on it without instantiating any models at all. All three order by primary key internally, so a custom order is ignored β€” and Rails warns about it. Use in_batches(order: :desc) if direction matters. Add a sleep between batches on production data so replicas can catch up and vacuum can run.

Sample input:

  Order.where(status: "pending").in_batches(of: 5_000) { |batch| ... }


Sample Output:

  Constant memory regardless of table size

Answer

  # One record at a time, constant memory
  Order.where(status: "pending").find_each(batch_size: 1_000) do |order|
    order.recalculate_total!
  end

  # An array per batch β€” for bulk work
  Order.find_in_batches(batch_size: 500) do |orders|
    Warehouse.push_bulk(orders.map(&:to_payload))
  end

  # A relation per batch β€” no model instantiation at all, much faster
  Order.where(archived: false).in_batches(of: 5_000) do |relation|
    relation.update_all(archived: true, archived_at: Time.current)
    sleep 0.1
  end

  # Resume from a known point after a failure
  Order.where("id > ?", checkpoint).find_each do |order|
    process(order)
    Checkpoint.update!(last_id: order.id) if order.id % 1_000 == 0
  end

  # Careful: a custom order is IGNORED and Rails warns
  Order.order(created_at: :desc).find_each { }   # ordered by id, not created_at
  Order.in_batches(order: :desc) { |r| }         # this direction IS respected

  # Lazy enumerator when you want Enumerable methods
  Order.find_each.lazy.select(&:overdue?).first(10)

Learn More

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! πŸ’» 🏑 Grad. Student, MCS. πŸŽ“ Class of '23. GitKraken Ambassador πŸ‡³πŸ‡΅ 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More