Module 2 · Advanced SQL
Date logic
Truncate dates to weeks and months, measure gaps in days, report "as of" a date, and fill in the months with no activity using a calendar.
About 20 minutes
The problem
Kingsway Foods' account manager wants a monthly chart of the shipments it booked, January 2025 to August 2026. The obvious query returns 12 rows, not 20:
SELECT strftime('%Y-%m', booking_date) AS month, COUNT(*) AS shipments
FROM shipments
WHERE customer_id = 40
GROUP BY month
ORDER BY month;Months with no bookings simply don't appear. Charted as it is, the line jumps straight from November 2025 to March 2026, and three quiet months, the most important fact about this customer, disappear.
Nearly every business question has a date in it: this month, last quarter, days to pay, how long since the last order. Date logic is where queries most often go quietly wrong.
The concept
Dates in SQLite are text
Harbourline stores dates as YYYY-MM-DD text. That format sorts correctly and compares correctly ('2026-03-01' < '2026-04-15'), and SQLite's date functions read it:
| Task | SQLite |
|---|---|
| Month label | strftime('%Y-%m', d) |
| First day of the month | date(d, 'start of month') |
| Last day of the month | date(d, 'start of month', '+1 month', '-1 day') |
| Monday of the week | date(d, '-6 days', 'weekday 1') |
| Add or subtract | date(d, '+30 days'), date(d, '-3 months') |
| Days between | julianday(d2) - julianday(d1) |
| Day of week (0 = Sunday) | strftime('%w', d) |
The same ideas in other databases
Your job may use a different database. The ideas are identical; only the spelling changes:
| Task | PostgreSQL | SQL Server | MySQL |
|---|---|---|---|
| First of month | date_trunc('month', d) | DATETRUNC(month, d) | DATE_FORMAT(d, '%Y-%m-01') |
| Add 30 days | d + INTERVAL '30 days' | DATEADD(day, 30, d) | DATE_ADD(d, INTERVAL 30 DAY) |
| Days between | d2 - d1 | DATEDIFF(day, d1, d2) | DATEDIFF(d2, d1) |
| Year | EXTRACT(YEAR FROM d) | YEAR(d) | YEAR(d) |
"As of" dates
Reports are run as of a date. Harbourline's data ends on 31 August 2026, so "the last 90 days" means after date('2026-08-31', '-90 days'). Avoid date('now') in analysis you'll hand over: the answer changes every day, and nobody can reproduce it.
A calendar for missing periods
GROUP BY can only produce groups that exist in the data. To show every month, build a list of months first and LEFT JOIN the data onto it. A recursive CTE generates the list: it starts with one row, then keeps adding a row based on the previous one until a condition stops it.
Example
The full 20 months for Kingsway Foods, with zeros where nothing was booked:
WITH RECURSIVE months(month_start) AS (
SELECT '2025-01-01' -- the first row
UNION ALL
SELECT date(month_start, '+1 month') -- each next row
FROM months
WHERE month_start < '2026-08-01' -- stop after August 2026
),
bookings AS (
SELECT date(booking_date, 'start of month') AS month_start, COUNT(*) AS shipments
FROM shipments
WHERE customer_id = 40
GROUP BY 1
)
SELECT
strftime('%Y-%m', m.month_start) AS month,
COALESCE(b.shipments, 0) AS shipments
FROM months AS m
LEFT JOIN bookings AS b ON b.month_start = m.month_start
ORDER BY m.month_start;Now the gaps show: nothing in December 2025, January or February 2026, and nothing since June 2026. That's a customer to call.
Walkthrough
- Run the
monthsCTE on its own (WITH RECURSIVE months(...) AS (...) SELECT * FROM months;) and check it lists 20 months. - Note how both sides of the join use the first of the month. Joining on the same key format is what makes the
LEFT JOINline up. - Remove
COALESCEand see the gaps become NULL. A chart would draw NULL as a break, or not at all; a zero is what the manager means. - Run
SELECT date('2026-02-14', '-6 days', 'weekday 1');to find the Monday of that week (9 February).
Practice
Practice
How long do customers take to pay? For each payment, the gap is payment_date minus the shipment's delivery_date. Show payments, avg_days_to_pay (1 decimal place) and paid_within_30 (the number of payments made 30 days or less after delivery).
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Practice
Show Harbourline's bookings by week, for the weeks starting in July 2026. Show week_start (the Monday) and shipments, ordered by week_start.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Challenge
Challenge · optional
Show every month from 2025-01 to 2026-08 with the number of shipments Harmattan Agro Ltd (customer 8) booked, including zeros. Show month ('YYYY-MM') and shipments, ordered by month.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
More practice
Drill · optional
As of 2026-08-31, how many days has it been since each customer's last booking? Show customer_id, last_booking and days_since (a whole number), longest first. Only customers who have booked.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Drill · optional
How many shipments were delivered in the 90 days up to and including 2026-08-31? Show one number, delivered_last_90.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Drill · optional
Count bookings by day of the week. Show day_number (0 = Sunday … 6 = Saturday from strftime('%w')) and shipments, ordered by day_number.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Check your understanding
Answer every question to check.