Learn SQL by running it. Every example below is a real SQLite database living in your browser — edit any query and press Run. Nothing is sent to a server. Work top to bottom, or jump around with the contents on the left.
Every lesson uses the same tiny e-commerce database. Four tables:
customersid, name, country, signup_date
productsid, name, category, price
ordersid, customer_id, order_date, status
order_itemsid, order_id, product_id, quantity
01
SELECT, WHERE & ORDER BY
Every query starts with SELECT — you name the columns you want and the table they live in. Run this to see all customers:
SQL
⌘/Ctrl + Enter
WHERE filters rows. Only keep customers from India:
SQL
🔮Before you run — how many customers are from India?
⌘/Ctrl + Enter
ORDER BY sorts the result. Show the most recent signups first with DESC:
SQL
⌘/Ctrl + Enter
Your turn: edit the query above to show only customers who signed up in 2025, sorted by name. (Hint: WHERE signup_date >= '2025-01-01'.)
P
PriyaFounder · Kettle & Co.now
Morning! ☕ Investor call at 3 and I need a quick one: pull our customers from India, newest signups first. Just name and signup date, please. 🙏
🎯 Your mission Return name and signup_date for customers in India, most recent signup first.
SQL
⌘/Ctrl + Enter
You need a WHERE to keep only India, and an ORDER BY to sort.
Filter with WHERE country = 'India', then ORDER BY signup_date DESC for newest first.
02
Filtering: AND, OR, IN, BETWEEN, LIKE, NULL
Combine conditions with AND / OR. Customers from India who signed up in 2025:
SQL
🔮Now AND narrows it to India *and* a 2025 signup. How many survive?
⌘/Ctrl + Enter
IN matches a list; BETWEEN matches a range (inclusive):
SQL
🔮IN two categories *and* a price range. How many products match?
⌘/Ctrl + Enter
LIKE does pattern matching — % is any run of characters, _ is a single character. Names starting with 'A':
SQL
⌘/Ctrl + Enter
Your turn: find every product whose name contains the word Cable or Mug. (Two LIKEs joined by OR.)
P
PriyaFounder · Kettle & Co.now
We’re running a “budget tech” promo 🎉 Can you list every Electronics product that costs under $30? I just need the name and price.
🎯 Your mission Return name and price for products in the Electronics category priced below 30.
SQL
⌘/Ctrl + Enter
Two conditions joined by AND: the category and the price.
WHERE category = 'Electronics' AND price < 30.
03
JOINs: Combining Tables
Data is split across tables to avoid duplication. A JOIN stitches them back together on a shared key. Match each order to the customer who placed it:
SQL
🔮Every order joins to its customer. How many rows come back?
⌘/Ctrl + Enter
An INNER JOIN (the default) keeps only rows that match on both sides. A LEFT JOIN keeps every row from the left table even when there is no match — perfect for finding gaps. Which customers have never ordered?
SQL
🔮This anti-join finds customers with NO orders. How many?
⌘/Ctrl + Enter
SQLite has no RIGHT or FULL OUTER JOIN — you get the same effect by swapping table order or using UNION. Your turn: join order_items to products to list every product name that was ordered.
P
PriyaFounder · Kettle & Co.now
I want to win back quiet customers. Which of our customers have never placed an order? Just give me their names and I’ll email them a coupon. 💌
🎯 Your mission Return the name of every customer who has no orders at all.
SQL
⌘/Ctrl + Enter
A LEFT JOIN keeps every customer even with no matching order.
LEFT JOIN orders o ON o.customer_id = c.id, then WHERE o.id IS NULL.
04
GROUP BY, Aggregations & HAVING
Aggregation functions (COUNT, SUM, AVG, MIN, MAX) collapse many rows into one summary. Count how many orders each status has:
SQL
🔮Orders collapse into one row per status. How many groups?
⌘/Ctrl + Enter
GROUP BY defines the buckets; the aggregate runs per bucket. Total revenue per product category (joining three tables):
SQL
⌘/Ctrl + Enter
Filter groups (not rows) with HAVING — it runs after aggregation, so it can reference the aggregate. Only categories with revenue over 100:
SQL
⌘/Ctrl + Enter
Rule of thumb:WHERE filters rows before grouping, HAVING filters groups after. Your turn: find each customer's total number of orders, highest first.
P
PriyaFounder · Kettle & Co.now
Board deck time 📊 I need our total revenue per product category, biggest earner at the top. Give me category and revenue (price × quantity summed).
🎯 Your mission Return category and total revenue (SUM of price × quantity), highest revenue first.
SQL
⌘/Ctrl + Enter
Join order_items to products for the price, then group by category.
GROUP BY p.category and ORDER BY revenue DESC.
05
Subqueries
A subquery is a query nested inside another. A scalar subquery returns a single value you can compare against — products priced above the average:
SQL
🔮How many products cost more than the average price?
⌘/Ctrl + Enter
A subquery in IN returns a list. Customers who have placed at least one order:
SQL
⌘/Ctrl + Enter
A correlated subquery references the outer row and re-runs per row — here, an order count per customer:
SQL
⌘/Ctrl + Enter
Your turn: list products that have never appeared in order_items using NOT IN.
P
PriyaFounder · Kettle & Co.now
Thinking about our “premium” line. Which products cost more than our average price? Just the names, cheapest of those first — use a subquery for the average. 🧮
🎯 Your mission Return the name of every product priced above the average product price, ordered by price ascending.
SQL
⌘/Ctrl + Enter
A scalar subquery (SELECT AVG(price) FROM products) returns one number to compare against.
WHERE price > (SELECT AVG(price) FROM products) ORDER BY price.
06
CTEs: The WITH Clause
A Common Table Expression (CTE) names a subquery up front with WITH, so your main query reads cleanly. Compute each order's total, then keep only the big ones:
SQL
⌘/Ctrl + Enter
You can chain multiple CTEs, and even reference one from the next. CTEs can also be recursive — generate the numbers 1 to 5 with no table at all:
SQL
⌘/Ctrl + Enter
Your turn: extend the recursive CTE to count 1 to 20, then only show even numbers (WHERE n % 2 = 0 in the final SELECT).
P
PriyaFounder · Kettle & Co.now
Chasing our big baskets 🛒 Using a CTE, find the orders worth more than $40. I need the order id and its total, biggest first.
🎯 Your mission Using a CTE, return order_id and total (SUM of quantity × price) for orders totalling over 40, highest first.
SQL
⌘/Ctrl + Enter
The CTE already computes each order’s total — you just filter and sort the outer query.
WHERE total > 40 ORDER BY total DESC on the CTE.
07
Window Functions
A window function computes across a set of rows without collapsing them — you keep every row and add a calculated column. ROW_NUMBER() numbers rows inside each PARTITION:
SQL
⌘/Ctrl + Enter
RANK() ranks by a value across the whole set. Rank customers by total spend (CTE + window together):
SQL
⌘/Ctrl + Enter
Add ORDER BY inside the window to get a running total — cumulative spend per customer over time:
SQL
⌘/Ctrl + Enter
Your turn: swap RANK() for DENSE_RANK() and LAG(total) OVER (ORDER BY total DESC) to see the previous customer's spend on each row.
P
PriyaFounder · Kettle & Co.now
VIP program idea 🌟 Rank our customers by total spend — top spender = rank 1. I need name, their total, and the rank. (Customers who never ordered can drop off.)
🎯 Your mission Return name, total spend, and a spend RANK() (1 = highest) for every customer who has ordered.
SQL
⌘/Ctrl + Enter
The spend CTE already totals each customer. Add a window function to rank them.
RANK() OVER (ORDER BY total DESC) AS rnk.
08
CASE: Conditional Logic
CASE is SQL's if/else. Bucket products into price tiers:
SQL
⌘/Ctrl + Enter
CASE inside an aggregate is how you pivot rows into columns — count orders by status in a single row:
SQL
⌘/Ctrl + Enter
Your turn: add a CASE column labelling each customer 'domestic' when country = 'India' else 'international'.
P
PriyaFounder · Kettle & Co.now
For the pricing page I want to tag each product by tier. Call it 'premium' at $50+, 'mid' at $15+, otherwise 'budget'. Give me name and tier. 🏷️
🎯 Your mission Return each product’s name and a tier label: premium if price ≥ 50, mid if ≥ 15, else budget.
SQL
⌘/Ctrl + Enter
Order matters in CASE — test the highest threshold first.
WHEN price >= 50 THEN 'premium' WHEN price >= 15 THEN 'mid' ELSE 'budget'.
09
Set Operations: UNION, INTERSECT, EXCEPT
Set operators stack the results of two queries. UNION merges and de-duplicates; UNION ALL keeps duplicates and is faster:
SQL
⌘/Ctrl + Enter
EXCEPT returns rows in the first query that are not in the second — a clean way to find products that were never ordered:
SQL
🔮EXCEPT subtracts ordered products. How many were never ordered?
⌘/Ctrl + Enter
The two queries must return the same number of columns with compatible types. Your turn: use INTERSECT to list product ids that appear in order_itemsand cost more than 20.
P
PriyaFounder · Kettle & Co.now
Inventory clean-up 🧹 Which products have we never actually sold? Give me their ids. Bonus points if you use EXCEPT.
🎯 Your mission Return the id of every product that has never appeared in order_items. Use EXCEPT.
SQL
⌘/Ctrl + Enter
EXCEPT subtracts the second result from the first.
SELECT id FROM products EXCEPT SELECT product_id FROM order_items.
10
String & Text Functions
SQL can transform text inline. UPPER, LOWER, LENGTH, and SUBSTR are the everyday ones:
SQL
⌘/Ctrl + Enter
The || operator concatenates strings — great for building labels:
SQL
⌘/Ctrl + Enter
Your turn: use REPLACE(name, 'a', '@') and INSTR(name, 'a') to see substitution and position-finding in action.
P
PriyaFounder · Kettle & Co.now
Printing mailing labels 🏷️ Build me one label per customer in the form NAME (COUNTRY) — name in UPPERCASE. One column, call it label.
🎯 Your mission Return a single column label for each customer, formatted as UPPERCASE NAME (Country).
SQL
⌘/Ctrl + Enter
|| concatenates; UPPER() upper-cases.
UPPER(name) || ' (' || country || ')' AS label.
11
Date & Time Functions
Dates are stored as ISO text (YYYY-MM-DD), which sorts and compares correctly. STRFTIME pulls out parts of a date:
SQL
⌘/Ctrl + Enter
julianday() converts a date to a number of days, so subtracting two of them gives an age in days. How long ago (from 9 Jul 2025) was each order placed?
SQL
⌘/Ctrl + Enter
Your turn: count how many orders fell in each month using GROUP BY STRFTIME('%Y-%m', order_date).
P
PriyaFounder · Kettle & Co.now
Trying to spot our busy months 📈 How many orders did we get each month? Give me the month as YYYY-MM and the count.
🎯 Your mission Return the month (formatted YYYY-MM) and the number of orders in each month.
SQL
⌘/Ctrl + Enter
STRFTIME('%Y-%m', order_date) pulls the year-month out of a date.
SELECT STRFTIME('%Y-%m', order_date) AS month, COUNT(*) AS n ... GROUP BY month.
12
Modifying Data: INSERT, UPDATE, DELETE
These statements change the in-browser database. Changes persist across the cells on this page until you reload, which restores the original data. Nothing touches a real server.
INSERT adds rows. Add a new customer, then read it back:
SQL
⌘/Ctrl + Enter
SQL
⌘/Ctrl + Enter
UPDATE ... SET ... WHERE changes existing rows. Mark every shipped order as delivered:
SQL
⌘/Ctrl + Enter
SQL
⌘/Ctrl + Enter
DELETE ... WHERE removes rows. Always keep the WHERE — without it you delete the whole table:
SQL
⌘/Ctrl + Enter
SQL
⌘/Ctrl + Enter
P
PriyaFounder · Kettle & Co.now
Final one — we just onboarded Sara from India (id 6, signed up 2025-07-05). Add her, then show me every Indian customer’s name in alphabetical order so I can double-check she’s in. 🎉
🎯 Your missionINSERT Sara (id 6, India, signup 2025-07-05), then SELECT the name of all India customers, sorted A→Z.
SQL
⌘/Ctrl + Enter
Run two statements: the INSERT, then a SELECT ... WHERE country = 'India' ORDER BY name.
After inserting Sara, SELECT name FROM customers WHERE country = 'India' ORDER BY name; should list Aisha, Rohan, Sara.
You finished the course — every mission solved. 🏆 Reload the page to reset the data and practise again — or move on to the full Data Engineering course to see SQL running inside real pipelines.
Final Challenge
Boss Battle: The Quarterly Board Report
You've earned your stripes, analyst. 🎖️ The board meets Friday and I need the full revenue story — no training wheels this time. Three questions. Solve all three and the SQL Mastery badge is yours.
0 / 3 challenges solved
Challenge 1 / 3
Our biggest spenders
P
PriyaFounder · Kettle & Co.now
First up: who are our whales? 🐋 List every customer who has spent more than $100 in total — name and total spend, biggest first.
🎯 Your mission Return name and total spend for customers whose lifetime spend exceeds 100, highest first.
SQL
⌘/Ctrl + Enter
The CTE already totals each customer — you just filter and sort the outer query.
WHERE total > 100 ORDER BY total DESC.
Challenge 2 / 3
The top spender in every country
P
PriyaFounder · Kettle & Co.now
Now segment it: for each country, who is our #1 spender? Give me country, name and their total — exactly one row per country. A window function earns its keep here. 🪟
🎯 Your mission Return country, name, and total spend for the single highest spender in each country (one row per country).
SQL
⌘/Ctrl + Enter
Wrap the spend CTE in a second CTE that adds ROW_NUMBER() OVER (PARTITION BY country ORDER BY total DESC).
Then keep only the winners with WHERE rn = 1.
Challenge 3 / 3
The champion product
P
PriyaFounder · Kettle & Co.now
Last one. 🥇 What is our best-selling product by units sold? Product name and total units — just the single champion.
🎯 Your mission Return the name and total units sold of the one product with the most units ordered.
SQL
⌘/Ctrl + Enter
Sum quantity per product, then sort descending.
GROUP BY p.id ORDER BY units DESC LIMIT 1.
🏆
SQL Mastery — Complete
You beat the Boss Battle. Claim your Proof-of-Skill card and show it off: