N064-H2 Tier 5 · Expert · hard ecommerce · Brightlane

Return each order day, the average order value for that day alone, and the running average order value across every individual order placed from the earliest day through that day

Part of Running Totals and Cumulative Metrics in SQL

The problem

Scenario: Brightlane's pricing team is tracking how the true average order value has evolved over time. The running average must reflect the genuine average across all individual orders accumulated to date, not a simple average of daily averages — those two numbers diverge whenever the per-day order counts vary.

Task: Write a query to return each order day, the average order value for that day alone, and the running average order value across every individual order placed from the earliest day through that day.

Assumptions:

  • An order day is identified by its date.
  • A day's daily_avg_order_value is the average total_amount across orders placed on that day.
  • A day's running_avg_order_value is the combined total_amount across every order placed from the earliest day through that day, divided by the combined order count over the same window — which gives the true average across individual orders, not the average of daily averages.

Output:

  • One row per order day present in the data.
  • Columns in this order: order_day, daily_avg_order_value, running_avg_order_value.
  • Sorted by order_day ascending.
Schema · ecommerce 5 tables
categories
id integer
name text
parent_id? integer
products
id integer
name text
category_id integer
price numeric
stock_qty integer
attributes? jsonb
order_items
id integer
order_id integer
product_id integer
quantity integer
unit_price numeric
customers
id integer
name text
email text
city? text
country text
created_at timestamptz
is_active boolean
orders
id integer
customer_id integer
ordered_at timestamptz
status text
total_amount numeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
SELECT
  DATE_TRUNC('day', ordered_at) AS order_day,
  AVG(total_amount) AS daily_avg_order_value,
  SUM(SUM(total_amount)) OVER (
    ORDER BY
      DATE_TRUNC('day', ordered_at) ROWS BETWEEN UNBOUNDED PRECEDING
      AND CURRENT ROW
  ) / SUM(COUNT(*)) OVER (
    ORDER BY
      DATE_TRUNC('day', ordered_at) ROWS BETWEEN UNBOUNDED PRECEDING
      AND CURRENT ROW
  ) AS running_avg_order_value
FROM
  orders
GROUP BY
  DATE_TRUNC('day', ordered_at)
ORDER BY
  order_day

The shape

A true running average across every individual order is the running total of revenue divided by the running total of order count. Computing AVG(SUM(...) OVER (...)) (an average of daily averages) would weight every day equally regardless of how many orders that day carried; the prompt rules that out. The cumulative numerator and cumulative denominator are computed separately with window functions, then divided once at the SELECT.

Clause by clause

  • SELECT DATE_TRUNC('day', ordered_at) AS order_day, AVG(total_amount) AS daily_avg_order_value produces one row per order day with that day's own average order value. This is the per-day metric the prompt calls for; it is not used to compute the running average.
  • The running_avg_order_value expression has two windowed pieces. SUM(SUM(total_amount)) OVER (ORDER BY DATE_TRUNC('day', ordered_at) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) runs cumulative revenue forward; SUM(COUNT(*)) OVER (ORDER BY DATE_TRUNC('day', ordered_at) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) runs cumulative order count forward. Dividing the two gives the true average across every order placed from the earliest day through the current day.
  • FROM orders GROUP BY DATE_TRUNC('day', ordered_at) aggregates the raw orders to one row per day. After GROUP BY, the inner SUM(total_amount) and COUNT(*) produce the day's revenue and the day's count; the outer windowed sums accumulate those over the ordered day rows.
  • ORDER BY order_day sorts the final output chronologically.

Why this and not AVG(total_amount) OVER (...)

AVG(total_amount) OVER (...) would not be legal here because the query is already aggregating to day level — every reference outside an aggregate or window has to be a grouping column. More importantly, even if rewritten to run on raw orders, AVG OVER with an unbounded-preceding frame would still produce the correct number on this data. The two-stage shape is required because the prompt is structured as "one row per day with both the day's average and the running average," which forces the aggregation to day level. Once aggregated, the average must be reconstructed from the running revenue and running count, because the day rows have lost the per-order detail AVG needs.

The trap

The natural-sounding "running average of daily averages" gives the wrong answer whenever the per-day order counts vary. A day with one $1000 order and the next day with ten $100 orders both have a daily average of $100 and $1000 respectively. Averaging those two daily averages gives $550. Averaging the eleven individual orders gives $182. The two numbers diverge because the second day carries ten times the weight in reality. Running totals of numerator and denominator divided once is what restores correct per-order weighting; averaging averages drops that weighting entirely.

You practiced building a true running average as cumulative-total-over-cumulative-count, rather than averaging daily averages — the two numbers diverge whenever the per-day order counts vary.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.