Use Batch Processing for Large Tables in Rails
Flags .each over unbounded relations, map(&:id) instead of pluck, aggregates computed in Ruby, and unpaginated queries in Rails request paths.
What it flags
class ExpireTrialsJob < ApplicationJob
queue_as :maintenance
def perform
expired = Account.where(plan: "trial").where("trial_ends_at < ?", Time.current)
expired.each do |account| # loads every expired trial into memory at once
account.update!(plan: "free")
TrialMailer.expired(account.owner).deliver_later
end
end
endclass ExpireTrialsJob < ApplicationJob
queue_as :maintenance
def perform
expired = Account.where(plan: "trial").where("trial_ends_at < ?", Time.current)
expired.includes(:owner).find_each(batch_size: 500) do |account|
account.update!(plan: "free")
TrialMailer.expired(account.owner).deliver_later
end
end
endWhy it matters
Calling each on a relation loads every matching row as an Active Record object before the loop body runs once. A job that handled a few thousand rows at launch can later exhaust worker memory or run past its timeout, and an index action without a limit gets slower as the table grows. The code reads the same at any table size, so the reviewer has to ask how large the table can get.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Use Batch Processing for Large Tables in Rails Applies to `**/*.rb`, `**/*.rake`. Review Active Record code in jobs, rake tasks, controllers, and models for work that loads or processes large tables all at once. Flag changes that: - Iterate a relation that can grow without bound with `each`, `map`, or `to_a` (`User.all.each`, `Order.where(...).map`) instead of `find_each`, `find_in_batches`, or `in_batches`. - Load full records to read one column, such as `map(&:id)` or `map(&:email)` on an unloaded relation, instead of `pluck`, `ids`, or a subquery like `where(user_id: User.active.select(:id))`. - Compute counts or totals in Ruby (`map(&:amount).sum`, `to_a.size`, `length`, `group_by`) instead of `count`, `sum`, or `group` in SQL. - Call `present?` or `blank?` on an unloaded relation only to test for rows. Both load every matching record; `exists?` runs a `LIMIT 1` query. - Call `destroy_all`, or update records one by one, over a large relation when callbacks are not needed, instead of `in_batches` with `delete_all` or `update_all`. - Chain `order` onto a relation before `find_each`, `find_in_batches`, or `in_batches` and rely on that order. Batch methods ignore the relation's order clause and iterate by the cursor column, the primary key by default. - Return or render an unbounded relation from a controller action or API endpoint without a limit or pagination. Do not flag: - Relations bounded by a small limit, a lookup table, or a parent's `has_many` that stays small by design. - Records already loaded for other work in the same method, where mapping over them adds no query. - `find_each` loops that save records one at a time because validations or callbacks must run.
Checks files matching **/*.rb, **/*.rake.
More Ruby on Rails agents
See all Ruby on Rails code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.