Practice · Scaling · Card 8
How many SQL writes does this loop fire?
A cleanup task. 100,000 draft posts to mark published. The code reads like a one-liner. The database sees it differently.
The code
A rake task to publish all draft posts older than a week.
Post.where(status: "draft")
.where("created_at < ?", 1.week.ago)
.each do |post|
post.update(status: "published")
end
# Suppose this matches 100,000 posts. The question
For 100,000 matching posts, how many SQL UPDATE statements does this fire?
Take a moment. Pick the best answer. Wrong picks reveal why they're wrong, which is half the point.
✅ Answer breakdown
✗ A. 1 UPDATE.
Only true if you use update_all, which is the right fix. With .each { post.update(...) }, Rails loads every record and saves each one individually.
✓ B. 100,000 UPDATEs, plus 1 SELECT to load them all.
.each on a Relation loads all 100,000 rows into memory at once (no batching by default), then runs .update on each, which is one UPDATE per record, plus validation, plus callbacks. 100,000 round trips and 100,000 callback chains. The fix: Post.where(...).update_all(status: "published") for 1 UPDATE. Or .find_each if you need the callbacks but want batched memory.
✗ C. Rails automatically batches the updates into a single UPDATE under the hood.
It doesn't. Rails could, but the way .update on a record is designed (with validations, callbacks, dirty tracking), each update has to run separately.
✗ D. Postgres collapses identical UPDATEs into one transaction.
Postgres doesn't deduplicate or collapse statements. Each UPDATE hits the database as its own statement, even if they're inside a transaction.
💡 The principle
Three different shapes for "update many records," all in Rails, all doing very different things:
.each { |p| p.update(...) }— loads everything, N updates, runs validations + callbacks. Slow + memory-hungry..find_each(batch_size: 1000) { |p| p.update(...) }— loads 1,000 at a time, N updates, callbacks still run. Slow but memory-safe..update_all(status: "published")— one UPDATE statement in SQL. No validations. No callbacks. Fast.
The choice depends on whether the validations and callbacks are doing work that matters per-record. For this case (a simple flag change), update_all is correct. For a case where each record needs to fire an after_save (e.g., emit an audit event), find_each is the middle ground.
📚 Theory
For the full walkthrough, read Scaling · Bulk Operations & Memory.