Lesson 4 · Module 1: Practical SQL with SQLite
Project: a monthly sales report query for the online shop
- Turn order dates into months with strftime
- Combine JOIN, WHERE, GROUP BY and ORDER BY in one query
- Save the report as a view you can rerun
Why this matters
Every month someone asks for the same numbers: how many orders, how much revenue, and the average order size, month by month. In Excel that means exporting, looking up prices, fixing dates and building a pivot table again. This lesson turns that routine into one saved query you can rerun in a second. It uses only pieces you already know.
Key ideas
- Spec before SQL. First write down what the finished report looks like: one row per month, with columns for month, orders, revenue and average order. Revenue is quantity times price, and the price lives in a different table from the orders.
- Dates become months with strftime. strftime (short for "string format time") reshapes a date using a pattern. The pattern
'%Y-%m'keeps the four digit year and the two digit month, much like Excel's TEXT function.
SELECT strftime('%Y-%m', '2025-11-30');
-- 2025-11
An order from 3 November and one from 30 November both become 2025-11, so they fall into the same group.
- Clauses follow the order the database works. First From the lesson: Combining tables like VLOOKUP: JOIN orders with customers and productsJOIN matches rows from two tables on a shared column, like VLOOKUP adding a column from another sheet.Review that lesson products to get each price. Then WHERE keeps the date range, and From the lesson: Summarizing like a pivot table: GROUP BY with COUNT, SUM and AVGGROUP BY collapses rows that share a value into one row, like a pivot table's row area.Review that lesson the month. COUNT, SUM and AVG fill in the numbers, and ORDER BY sorts the months.
- Date ranges run "from, up to but not including". The dates are text written year first, so plain text comparison puts them in the right order.
>= '2026-01-01' AND < '2026-04-01'covers January to March, and you never have to know how many days March has. - A view saves the recipe, not the results. CREATE VIEW stores the query under a name. Each time you select from it, the query runs again on today's data, like a pivot table that refreshes itself.
Try it before you watch
Which WHERE condition keeps only the orders from April, May and June 2026?
Show answerHide answer
WHERE order_date >= '2026-04-01' AND order_date < '2026-07-01'
Start on the first day of the first month you want, and stop just before the first day of the month after the last one.
Words to know
- spec: short for specification. It is a plain description of the finished report, written before any code.
- strftime: a SQLite function that turns a date into text shaped by a pattern, such as year and month only.
- view: a saved query with a name. It behaves like a table and runs fresh every time you use it.
- table alias: a short nickname for a table inside a query. For example,
ostands for orders, soo.qtymeans the qty column of orders. - sanity check: a quick test that the result makes sense, such as counting the rows or comparing a total.
What to watch for
- In the animated query, watch the eight orders pick up their prices and then collapse into three monthly rows.
- When the date range is added, notice how the extra December order quietly drops out of the result.
- In the common mistake, notice what happens when you group by the full date instead of the month. Then look at the two quick checks that catch it.
Learn it with your tutor on the live board
Key points from the video
The Monday morning report
- Your manager wants sales by month, every month
- Today you rebuild it by hand in Excel
- One saved query can do it in a second
- Today we build that query together
What you will learn
- Turn order dates into months with strftime
- Combine JOIN, WHERE, GROUP BY and ORDER BY in one query
- Save the report as a view you can rerun
Write the report spec first
- One row per month
- Columns: month, orders, revenue, average order
- Revenue = quantity times product price
- Only the months you ask for
Plan the query in steps
- Match each order to its product price
- Keep only the months we want
- Group rows by month, then summarize
- Sort the months in order
From a date to a month
SELECT strftime('%Y-%m', '2026-02-14');
-- 2026-02
SELECT order_date,
strftime('%Y-%m', order_date) AS month
FROM orders; The report query, clause by clause
- One JOIN brings in the price
- GROUP BY month makes one row per month
- ORDER BY month sorts oldest first
layout: wide
tables:
orders:
columns: [id, customer_id, product_id, qty, order_date]
rows: [[101, 1, 1, 2, "2026-01-05"], [102, 2, 2, 1, "2026-01-18"], [103, 3, 3, 3, "2026-01-27"], [104, 1, 2, 2, "2026-02-03"], [105, 4, 1, 1, "2026-02-14"], [106, 2, 3, 5, "2026-02-22"], [107, 5, 2, 3, "2026-03-02"], [108, 3, 1, 4, "2026-03-11"]]
products:
columns: [id, name, price]
rows: [[1, Mug, 12], [2, T-shirt, 20], [3, Poster, 8]]
query: |
SELECT strftime('%Y-%m', o.order_date) AS month,
COUNT(*) AS orders,
SUM(o.qty * p.price) AS revenue
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY month
ORDER BY month
steps:
- at: "a small sample"
show: tables
- at: "Here's the query"
show: query
- at: "matching product id"
show: join
- at: "collapse into months"
show: group
- at: "oldest month first"
show: order
- at: "the finished report"
show: result Add the average and a date range
- The sample now has one extra order from December 2025
SELECT strftime('%Y-%m', o.order_date) AS month,
COUNT(*) AS orders,
SUM(o.qty * p.price) AS revenue,
ROUND(AVG(o.qty * p.price), 2) AS avg_order
FROM orders o
JOIN products p ON p.id = o.product_id
WHERE o.order_date >= '2026-01-01'
AND o.order_date < '2026-04-01'
GROUP BY month
ORDER BY month;
-- month | orders | revenue | avg_order
-- 2026-01 | 3 | 68 | 22.67
-- 2026-02 | 3 | 92 | 30.67
-- 2026-03 | 2 | 108 | 54.0 Save it as a view
CREATE VIEW monthly_sales AS
SELECT strftime('%Y-%m', o.order_date)
AS month,
COUNT(*) AS orders,
SUM(o.qty * p.price) AS revenue,
ROUND(AVG(o.qty * p.price), 2)
AS avg_order
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY month;
SELECT * FROM monthly_sales;
SELECT * FROM monthly_sales
WHERE month = '2026-03'; Common mistake: grouping by the full date
- GROUP BY order_date gives one row per day
- Group by the month expression instead
- Check: row count should equal number of months
- Check: total revenue should match the raw sum
Recap
- strftime turns dates into month labels
- JOIN, WHERE, GROUP BY, ORDER BY build the report
- A view saves it for every month to come
Your exercise
- Add a column: units sold per month
- Add a column: number of distinct customers
- Hint: SUM(o.qty) and COUNT(DISTINCT ...)
- Save it as view monthly_sales_v2
Transcript
The Monday morning report. Picture the first Monday of the month. Your manager asks for last month's sales: how many orders, how much revenue, and the average order size. You export the orders, paste them into Excel, look up every product price, build a pivot table, and fix the dates that came in as text. An hour later you have a report, and next month you'll do it all again. What if that whole routine were one query you could run again whenever you like? That's today's project. We'll use everything from the last three lessons on our online shop's data and build a monthly sales report you can save and reuse.
What you will learn. By the end of this lesson you'll be able to do three things. First, turn a full order date like the fourteenth of February into a month label, so orders can be grouped by month. Second, combine the pieces you already know, JOIN, WHERE, GROUP BY and ORDER BY, into one complete report query. Third, save that query as a view, which is a stored query that behaves like a table, so next month you type one short line instead of rebuilding anything. You've seen every building block before, so this lesson is mostly about putting them together in the right order.
Write the report spec first. Before writing any SQL, write down what the finished report should look like. This is the spec, short for specification, and it's the same thing you'd sketch before building a pivot table. We want one row per month. Each row has four columns: the month, the number of orders, the total revenue, and the average order value. Our orders table stores a quantity and a product id, but not a price. The price lives in the products table, so revenue for an order is its quantity times the product's price. Finally, the report should cover only the months we choose, not the shop's whole history.
Plan the query in steps. Here's the plan, in the order the database actually works through it. Start with the orders table. Join it to products so every order row gets its price, just like a VLOOKUP adds a column. Then keep only the rows inside our date range. Next, group the remaining rows by month, the same way a pivot table puts months in its row area. For each group we calculate the count, the sum and the average. Last, sort the months from oldest to newest. If a query ever confuses you, come back to this picture and check which step is misbehaving.
From a date to a month. SQLite stores our dates as text in the form year, dash, month, dash, day. To group by month, we need just the first part. The function strftime, which stands for string format time, does that. You give it a pattern and a date. The pattern percent capital Y, dash, percent small m means four digit year, dash, two digit month. So the fourteenth of February twenty twenty six becomes two thousand twenty six dash zero two. It's like Excel's TEXT function with a format of year and month. The second query shows the original date next to the new month column, a quick way to check it works.
The report query, clause by clause. Let's run it on a small sample: eight orders and three products. Here's the query. The letters o and p are short nicknames for the two tables, so o dot qty means the qty column of the orders table. Watch it work clause by clause. First, each order is joined to the product with the matching product id, so every row now knows its price. Then the rows collapse into months: count gives the number of orders, and sum adds up quantity times price. January has three orders worth sixty eight dollars, February three orders worth ninety two, and March two orders worth one hundred and eight. Order by puts the oldest month first. And there's the finished report, three rows that used to take a pivot table and a lookup.
Add the average and a date range. Now we finish the spec. Our sample now has one extra order from December twenty twenty five. Two things are new in the query. First, average order value uses AVG on quantity times price, wrapped in ROUND with two decimal places, just like Excel's ROUND. Second, a WHERE clause keeps dates from the first of January up to, but not including, the first of April. Because our dates are written year first, plain text comparison sorts them correctly. So the December order simply drops out, and the result shows our three months with their averages: twenty two sixty seven for January, thirty sixty seven for February, and fifty four for March.
Save it as a view. Here's where SQL beats the monthly copy and paste routine. CREATE VIEW saves the query under a name, here monthly sales. A view stores the recipe, not the results, so every time you select from it, SQLite runs the query again on today's data. New orders added last night show up automatically. Think of it as a pivot table that refreshes itself. Notice the view has no date range: it keeps every month, and you choose the months with WHERE when you select from it. From now on, the whole report is one line: select star from monthly sales. You can filter it too, for example select from monthly sales where month equals two thousand twenty six dash zero three, to get just March.
Common mistake: grouping by the full date. The most common slip in this project is grouping by order date instead of by the month. The query runs without any error, but you get one row for every distinct day. On our sample that's eight rows instead of three, and a full year could give up to three hundred sixty five rows instead of twelve. Always group by the month you built with strftime. Then do two quick sanity checks, like you would in Excel. First, count the rows: three months should mean three rows. Second, add up the revenue column and compare it with a plain sum of quantity times price over the same dates. If the two totals disagree, look at your join or your date range.
Recap. Let's wrap up the project. First, strftime with the year and month pattern turns any order date into a month label you can group by. Second, a full report is just the familiar clauses in order: join to get prices, where to pick the date range, group by to make one row per month, and order by to sort. Count, sum and a rounded average fill in the numbers. Third, saving the query as a view turns a monthly chore into one short line that always uses fresh data. You've gone from your first SELECT to a real, reusable business report.
Your exercise. Now it's your turn to extend the report before the next lesson. Add two columns to the monthly query. The first is units sold, the total quantity of items per month, which is a sum of quantity. The second is how many different customers bought something that month. For that, use COUNT with the word DISTINCT in front of the customer id, which counts each customer only once, like removing duplicates in Excel before counting. Save the result as a new view called monthly sales v2 and check the numbers by hand against the sample data. For January, you should see six units and three customers.
Python runs in your browser, nothing to install. The first run downloads Python (about 20 MB, then cached).
1 Fix the Q1 monthly sales report
The shop's data is loaded into an in-memory SQLite database, including one order from December 2025. The query in query runs, but it gives one row per day and includes December. Change it so it returns one row per month for January to March 2026 only, with the columns month, orders, revenue and avg_order (rounded to 2 decimals), sorted oldest month first.
2 Save monthly_sales_v2 with units and customers
Create a view named monthly_sales_v2 with the columns month, orders, revenue, units and customers, in that order, covering every month (no date range). units is the total quantity sold, and customers counts each customer only once per month. The starter view is missing two columns and the join to products.
Answer every question, then check. 70% passes; you can retry as often as you like.
Lesson summary
- Reading
- Video watched
- Exercises
- Quiz passed (70%)
- strftime turns dates into month labels
- JOIN, WHERE, GROUP BY, ORDER BY build the report
- A view saves it for every month to come