Daily orders including zero days
The dashboard team is plotting daily orders. GROUP BY order_date only returns days that had an
order, so quiet days vanish and the line chart draws straight across them.
Return one row for every calendar day from the first order date to the last order date, inclusive:
| column | type | meaning |
|---|---|---|
day |
date | the calendar day |
orders |
integer | orders placed that day, any status; 0 if none |
Sort by day, oldest first. Order matters.
day must be a DATE, not a timestamp.
Tables: orders.
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.
You can’t count your way to the missing days, because they aren’t in any table. Generate them: a “spine” of every date in the range, then attach the counts to it.
generate_series(start, stop, INTERVAL 1 DAY) returns one row per day, both ends included. Get
start and stop from MIN(order_date) and MAX(order_date). It returns timestamps, so
CAST(... AS DATE).
LEFT JOIN orders onto the spine and count o.order_id, not *. COUNT(column) skips the NULL
that a day with no match produces, so empty days count 0.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Build a spine of every day in range, left-join the orders onto it, and count.
WITH bounds AS (
SELECT MIN(order_date) AS first_day, MAX(order_date) AS last_day
FROM orders
),
spine AS (
SELECT CAST(d AS DATE) AS day
FROM bounds,
generate_series(first_day, last_day, INTERVAL 1 DAY) AS g(d)
)
SELECT s.day,
COUNT(o.order_id) AS orders
FROM spine s
LEFT JOIN orders o ON o.order_date = s.day
GROUP BY s.day
ORDER BY s.day;
Why it works
The spine decides which rows exist. The data only decides the numbers in them. A LEFT JOIN keeps
every spine day, and a day with no orders gets one row of NULLs from the right side.
COUNT(*) would count that NULL row as 1. COUNT(o.order_id) counts non-null values only, so the
empty day is 0. This is the most common bug in date-spine queries.
The bounds come from the data, so the spine grows when a new order arrives after the current last
day. generate_series includes its stop value. DuckDB’s range does not, which is an easy off-by-one.
Without the explicit ORDER BY, rows come back in whatever order the hash aggregate emits them.
Under the hood
generate_series is a table function: it streams 695 rows without reading any table. The join is a
hash join with the spine on one side and orders on the other, keyed by date. The GROUP BY is a
hash aggregate, and the final ORDER BY sorts 695 rows. In a warehouse you’d often keep a permanent
calendar table instead, which also carries columns like week, month and holiday flags.
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.