Project: Analysing a Shop
Time to use everything together. You're the new data analyst at the demo shop, and the managers have questions. Each task below states a business question; try writing the query yourself in the editor before opening the model solution. Every solution runs against the same database you've used throughout the course.
1. Headline numbers
How many customers, orders and products do we have, and what is total revenue from orders that weren't cancelled?
Show a model solution
SELECT (SELECT COUNT(*) FROM customers) AS customers, (SELECT COUNT(*) FROM orders) AS orders, (SELECT COUNT(*) FROM products) AS products, (SELECT round(SUM(oi.quantity * oi.unit_price), 2) FROM order_items AS oi JOIN orders AS o ON o.id = oi.order_id WHERE o.status <> 'cancelled') AS revenue;
| customers | orders | products | revenue |
|---|---|---|---|
| 10 | 15 | 12 | 1055.02 |
2. Revenue by month, with growth
Show revenue per month and the change from the previous month.
Show a model solution
WITH monthly AS ( SELECT strftime('%Y-%m', o.order_date) AS month, round(SUM(oi.quantity * oi.unit_price), 2) AS revenue FROM orders AS o JOIN order_items AS oi ON oi.order_id = o.id WHERE o.status <> 'cancelled' GROUP BY month ) SELECT month, revenue, round(revenue - LAG(revenue) OVER (ORDER BY month), 2) AS change FROM monthly ORDER BY month;
| month | revenue | change |
|---|---|---|
| 2025-09 | 109.89 | NULL |
| 2025-10 | 171.48 | 61.59 |
| 2025-11 | 81 | -90.48 |
| 2025-12 | 148.89 | 67.89 |
| 2026-01 | 203.78 | 54.89 |
| 2026-02 | 162 | -41.78 |
| 2026-03 | 177.98 | 15.98 |
3. Best-selling products
List the top five products by revenue, with units sold and their share of total revenue.
Show a model solution
WITH sales AS ( SELECT p.name, SUM(oi.quantity) AS units, SUM(oi.quantity * oi.unit_price) AS revenue FROM order_items AS oi JOIN orders AS o ON o.id = oi.order_id JOIN products AS p ON p.id = oi.product_id WHERE o.status <> 'cancelled' GROUP BY p.id, p.name ) SELECT name, units, round(revenue, 2) AS revenue, round(100.0 * revenue / SUM(revenue) OVER (), 1) AS pct_of_total FROM sales ORDER BY revenue DESC LIMIT 5;
| name | units | revenue | pct_of_total |
|---|---|---|---|
| Wireless Earbuds | 4 | 239.96 | 22.7 |
| Linen Throw | 3 | 147 | 13.9 |
| Learning SQL Step by Step | 4 | 130 | 12.3 |
| Mechanical Keyboard | 1 | 89 | 8.4 |
| Ceramic Mug | 7 | 87.5 | 8.3 |
4. Customer value
For every customer, show number of orders, total spent, first and last order dates — including customers who have never ordered.
Show a model solution
SELECT c.name, COUNT(DISTINCT o.id) AS orders, round(COALESCE(SUM(oi.quantity * oi.unit_price), 0), 2) AS spent, MIN(o.order_date) AS first_order, MAX(o.order_date) AS last_order FROM customers AS c LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status <> 'cancelled' LEFT JOIN order_items AS oi ON oi.order_id = o.id GROUP BY c.id, c.name ORDER BY spent DESC;
| name | orders | spent | first_order | last_order |
|---|---|---|---|---|
| Ben Carter | 2 | 168.98 | 2025-09-14 | 2025-12-01 |
| Ana Pereira | 3 | 168.88 | 2025-09-03 | 2026-02-03 |
| George Brown | 2 | 137.9 | 2025-12-12 | 2026-03-05 |
| Hana Novak | 1 | 119.98 | 2026-01-22 | 2026-01-22 |
| Chloé Martin | 2 | 117 | 2025-10-20 | 2026-02-20 |
| Isaac Levi | 1 | 97.5 | 2026-02-14 | 2026-02-14 |
| Farah Haddad | 1 | 83.8 | 2026-01-07 | 2026-01-07 |
| Elena García | 1 | 81 | 2025-11-15 | 2025-11-15 |
| David Müller | 1 | 79.98 | 2026-03-01 | 2026-03-01 |
| Julia Santos | 0 | 0 | NULL | NULL |
5. Repeat customers
What percentage of customers who ordered have ordered more than once?
Show a model solution
WITH per_customer AS ( SELECT customer_id, COUNT(*) AS orders FROM orders WHERE status <> 'cancelled' GROUP BY customer_id ) SELECT COUNT(*) AS customers_who_ordered, COUNT(*) FILTER (WHERE orders > 1) AS repeat_customers, round(100.0 * COUNT(*) FILTER (WHERE orders > 1) / COUNT(*), 1) AS repeat_pct FROM per_customer;
| customers_who_ordered | repeat_customers | repeat_pct |
|---|---|---|
| 9 | 4 | 44.4 |
6. Stock alerts
Which products have fewer units in stock than are waiting in pending orders, or are out of stock?
Show a model solution
SELECT p.name, p.stock, COALESCE(SUM(oi.quantity) FILTER (WHERE o.status = 'pending'), 0) AS pending_units FROM products AS p LEFT JOIN order_items AS oi ON oi.product_id = p.id LEFT JOIN orders AS o ON o.id = oi.order_id GROUP BY p.id, p.name, p.stock HAVING p.stock = 0 OR p.stock < pending_units ORDER BY p.stock;
| name | stock | pending_units |
|---|---|---|
| Mechanical Keyboard | 0 | 0 |
7. Category performance by country
Show revenue per category for each country, with each country's best category marked.
Show a model solution
WITH revenue AS ( SELECT c.country, p.category, round(SUM(oi.quantity * oi.unit_price), 2) AS revenue FROM customers AS c JOIN orders AS o ON o.customer_id = c.id JOIN order_items AS oi ON oi.order_id = o.id JOIN products AS p ON p.id = oi.product_id WHERE o.status <> 'cancelled' GROUP BY c.country, p.category ) SELECT country, category, revenue, CASE WHEN RANK() OVER (PARTITION BY country ORDER BY revenue DESC) = 1 THEN '★' ELSE '' END AS best FROM revenue ORDER BY country, revenue DESC;
| country | category | revenue | best |
|---|---|---|---|
| Czechia | Electronics | 119.98 | ★ |
| France | Home | 133 | ★ |
| France | Books | 49.8 | |
| France | Toys | 18 | |
| Germany | Electronics | 79.98 | ★ |
| Portugal | Books | 154.9 | ★ |
| Portugal | Home | 71.5 | |
| Portugal | Electronics | 39.98 | |
| Spain | Books | 45 | ★ |
| Spain | Toys | 36 | |
| UK | Electronics | 168.98 | ★ |
| UK | Home | 98 | |
| UK | Toys | 39.9 |
8. Payroll report
For each department, show headcount, total and average salary, and the highest-paid person — and include the Research department and the CEO.
Show a model solution
WITH ranked AS ( SELECT e.*, ROW_NUMBER() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS pos FROM employees AS e ) SELECT COALESCE(d.name, '(no department)') AS department, COUNT(r.id) AS headcount, COALESCE(SUM(r.salary), 0) AS payroll, round(AVG(r.salary)) AS avg_salary, MAX(CASE WHEN r.pos = 1 THEN r.first_name || ' ' || r.last_name END) AS top_earner FROM departments AS d FULL JOIN ranked AS r ON r.department_id = d.id GROUP BY d.id, d.name ORDER BY payroll DESC;
| department | headcount | payroll | avg_salary | top_earner |
|---|---|---|---|---|
| Engineering | 4 | 319000 | 79750 | Mei Tanaka |
| Sales | 3 | 188000 | 62667 | Rafael Costa |
| (no department) | 1 | 150000 | 150000 | Olivia Brandt |
| Marketing | 2 | 119000 | 59500 | Daniel Kim |
| Support | 2 | 102000 | 51000 | Ethan Walsh |
| Research | 0 | 0 | NULL | NULL |
9. Build a reporting view
Create a view sales_lines with one row per order line: date, month, customer, country, product, category, quantity and line total (excluding cancelled orders). Then answer question 7 again using it.
Show a model solution
CREATE VIEW sales_lines AS SELECT o.order_date, strftime('%Y-%m', o.order_date) AS month, c.name AS customer, c.country, p.name AS product, p.category, oi.quantity, oi.quantity * oi.unit_price AS line_total FROM order_items AS oi JOIN orders AS o ON o.id = oi.order_id JOIN customers AS c ON c.id = o.customer_id JOIN products AS p ON p.id = oi.product_id WHERE o.status <> 'cancelled'; SELECT country, category, round(SUM(line_total), 2) AS revenue FROM sales_lines GROUP BY country, category ORDER BY country, revenue DESC;
| country | category | revenue |
|---|---|---|
| Czechia | Electronics | 119.98 |
| France | Home | 133 |
| France | Books | 49.8 |
| France | Toys | 18 |
| Germany | Electronics | 79.98 |
| Portugal | Books | 154.9 |
| Portugal | Home | 71.5 |
| Portugal | Electronics | 39.98 |
| Spain | Books | 45 |
| Spain | Toys | 36 |
| UK | Electronics | 168.98 |
| UK | Home | 98 |
| UK | Toys | 39.9 |