Free Interactive Course · SQL

SQL Mastery

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.

The dataset

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
⌘/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'.)
02

Filtering: AND, OR, IN, BETWEEN, LIKE, NULL

Combine conditions with AND / OR. Customers from India who signed up in 2025:

SQL
⌘/Ctrl + Enter

IN matches a list; BETWEEN matches a range (inclusive):

SQL
⌘/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.)
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
⌘/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
⌘/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.
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
⌘/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.
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
⌘/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.
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).
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.
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'.
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
⌘/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_items and cost more than 20.
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.
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).
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
You finished the course. 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.

Want to go deeper into data?

SQL is the foundation. When you're ready for pipelines, Spark, and the modern data stack, the full Data Engineering course is free too.

Explore Data Engineering →