Running revenue by month
Finance wants a cumulative revenue line: each month’s delivered revenue, plus the total so far.
Revenue for a line item is quantity * unit_price. Only count orders whose status is
'delivered'. A month is the calendar month of order_date; report it as a date on the first of the
month (for example 2025-03-01).
Return one row per month that has delivered revenue:
| column | type | meaning |
|---|---|---|
month |
date | first day of the month |
revenue |
decimal | delivered revenue in that month, rounded to 2 decimals |
running_revenue |
decimal | delivered revenue from the first month up to and including this one, rounded to 2 decimals |
Months with no delivered orders don’t appear. Order matters: oldest month first.
Tables: orders, order_items.
bitshop
A small online shop for developers. Synthetic data, generated by scripts/gen-datasets.mjs.
Click a table to preview its first 10 rows.
customers 200 rows
one row per customer. About 6% have no email. About 15% never placed an order.
products 40 rows
40 products in 5 categories.
orders 1,000 rows
1,000 orders, 2024–2025. Status is delivered, shipped, cancelled or returned.
order_items 2,423 rows
line items. unit_price can be below the list price (discounts).
price_changes 98 rows
list-price history per product (one row per change, effective from changed_at).
Hints get more specific as you go. Reveal one, try again, then come back.
Do it in two steps. First get one row per month with that month’s delivered revenue. A CTE
(WITH monthly AS (...)) keeps it readable.
CAST(date_trunc('month', order_date) AS DATE) maps every date to the first of its month.
month is a keyword in DuckDB, so name the column with AS month; a bare month alias is a syntax error.
A running total is a SUM over a window that grows one row at a time:
SUM(revenue) OVER (ORDER BY month). The window function runs after GROUP BY, so it sees the
monthly rows, not the line items.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Aggregate to one row per month, then run a cumulative SUM over those rows in month order.
WITH monthly AS (
SELECT CAST(date_trunc('month', o.order_date) AS DATE) AS month,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'delivered'
GROUP BY 1
)
SELECT month,
ROUND(revenue, 2) AS revenue,
ROUND(SUM(revenue) OVER (ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) AS running_revenue
FROM monthly
ORDER BY month;
Why it works
A window function keeps every row and adds a value computed from its neighbours. ORDER BY month
inside OVER defines who the neighbours are. The frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW says “from the first row up to me”, which is exactly a running total.
That frame is also the default when you write only ORDER BY (strictly, the default is RANGE,
which treats rows with equal sort keys as one step). Months are unique here, so both give the same
answer. Spelling out ROWS makes the intent plain.
Round at the end. Rounding each month and then summing the rounded values can drift by a cent.
Under the hood
DuckDB sorts the monthly rows by month (two years, so 24 rows) and walks them once, keeping a single
running sum: add the current row, emit, move on. For frames that don’t only grow, like “the last 3
months”, it builds a segment tree over the partition so each frame’s sum is O(log n) instead of
re-adding every row.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.