ruby 53 lines · 3 tabs

Nightly Signup Rollups in Rails with a Scheduled Job and Upsert

Shared by codesnips Aug 2026
3 tabs
class DailySignupRollup < ApplicationRecord
  validates :day, presence: true, uniqueness: true
  validates :total, :verified, numericality: { greater_than_or_equal_to: 0 }

  scope :for_range, ->(from, to) do
    where(day: from.to_date..to.to_date).order(:day)
  end

  scope :recent, ->(days = 30) do
    for_range(days.days.ago, Time.zone.today)
  end

  def signup_rate
    return 0.0 if total.zero?
    (verified.to_f / total).round(4)
  end
end
3 files · ruby Explain with highlit

This snippet shows a common analytics pattern in Rails: pre-aggregating raw event rows into a compact daily rollup table so dashboards read a handful of summary rows instead of scanning millions of users. The rollup is recomputed on a schedule and made idempotent so re-running a day never double-counts.

In DailySignupRollup model, each row represents one calendar day, keyed by a unique day column. The dedupe_key uniqueness (here just day) means a rerun overwrites rather than inserts. The for_range scope filters rollups for reporting, and the signup_rate helper derives a value from stored columns so the read side stays trivial. Storing both total and verified keeps the table useful without recomputing from raw data.

The RollupSignupsJob job does the actual aggregation. It groups User records created within a UTC day boundary using group_by_day semantics expressed as a plain GROUP BY on a truncated timestamp, counting total and verified signups per day. The key detail is upsert_all with unique_by: :day: Postgres performs an INSERT ... ON CONFLICT (day) DO UPDATE, so the job is safe to re-run for backfills or after a crash. It accepts a date argument, defaulting to yesterday, which lets the scheduler process the just-completed day while a human can replay any historical date. Working in Time.zone and normalizing to beginning_of_day avoids off-by-one bucketing across time zones — a classic rollup pitfall.

The User signups scope tab holds the query building blocks the job leans on: created_between bounds the window with a half-open interval [start, end) to avoid double-counting midnight rows, and verified filters confirmed accounts. Keeping these as scopes means the job reads declaratively and the same predicates can be reused in tests or ad-hoc reports.

The trade-off is freshness: rollups lag by the scheduling interval, so they suit reporting, not real-time counters. When a day's raw data changes late (backfilled signups), the date-argument design plus the upsert make correction a one-line replay. A developer reaches for this when dashboard queries grow expensive and eventual consistency on aggregates is acceptable.


Related snips

Share this code

Here's the card — post it anywhere.

Nightly Signup Rollups in Rails with a Scheduled Job and Upsert — share card
Link copied