Query Records Created Or Updated In The Last N Hours In Rails
Dashboards, digest emails, and sync jobs all need “what changed recently”. Rails’ duration helpers plus a beginless or endless range make this readable and, crucially, index-friendly.
Description
3.hours.ago.. is an endless range, and ActiveRecord turns it into >= ? — which a B-tree index on the timestamp can use. Writing where("updated_at > ?", ...) works too but loses the composability of a hash condition.
Prefer a range over Time.current - 3.hours arithmetic; the duration helpers handle DST and month lengths correctly.
Always store and compare in UTC. Mixing a local Time.now with a UTC column is the most common source of off-by-hours bugs here.
Wrap the common cases in scopes so the intent is named rather than recomputed.
Sample input:
Order.updated_within(6.hours)
Sample Output:
SELECT orders.* FROM orders WHERE orders.updated_at >= '2026-05-17 03:00:00'
Answer
class Order < ApplicationRecord
scope :created_within, ->(duration) { where(created_at: duration.ago..) }
scope :updated_within, ->(duration) { where(updated_at: duration.ago..) }
scope :updated_between, ->(from, to) { where(updated_at: from..to) }
scope :stale, ->(duration) { where(updated_at: ...duration.ago) }
end
Order.updated_within(6.hours)
Order.created_within(1.day).count
Order.stale(30.days) # not touched in a month
# Explicit window
Order.updated_between(2.days.ago, 1.day.ago)
# Calendar-relative rather than rolling
Order.where(created_at: Time.current.all_day)
Order.where(created_at: Date.current.beginning_of_week..)
# Records changed since a stored checkpoint — the sync-job pattern
last_run = SyncCheckpoint.last_run_at
Order.where(updated_at: last_run..).find_each do |order|
Warehouse.push(order)
end
# Index this, or the scan gets expensive
# add_index :orders, :updated_at
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀