Write real SQL against a live dataset in this page. Nothing to install, and every answer is checked for you.
THE DATASET
Two Tables, Eighteen Rows
A small sales dataset, small enough to read and check by eye. Open a table to see what is in it.
orders 12 rows
order_id
customer_id
product
region
value
order_date
1001
1
Pro plan
North
240
2026-03-02
1002
2
Starter
South
60
2026-03-03
1003
3
Pro plan
North
240
2026-03-03
1004
1
Add-on
North
45
2026-03-09
1005
4
Pro plan
East
240
2026-03-11
1006
2
Add-on
South
45
2026-03-14
1007
5
Starter
West
60
2026-03-18
1008
3
Pro plan
North
240
2026-03-21
1009
4
Starter
East
60
2026-03-22
1010
6
Pro plan
South
240
2026-03-29
1011
1
Starter
North
60
2026-04-02
1012
5
Add-on
West
45
2026-04-05
customers 6 rows
customer_id
name
country
signed_up
1
Nordwind
Sweden
2025-11-04
2
Bluefin
United Kingdom
2025-12-18
3
Corelis
United Kingdom
2026-01-09
4
Harborline
Denmark
2026-01-27
5
Vantage Foods
Sweden
2026-02-14
6
Kestrel Retail
Norway
2026-02-28
This runs a teaching subset of SQL in your browser: SELECT, WHERE, JOIN, GROUP BY, HAVING, ORDER BY, LIMIT and the usual aggregates. Nothing is sent anywhere, and your work is saved on this device.
SIX EXERCISES
Write, Run, Check
Each exercise teaches the syntax first, then gives you a task to write yourself. Unlimited attempts, a hint whenever you want one, and the worked solution after three tries.
0 of 6 exercises solved
1
Exercise 1 of 6
Look at the data
Not attempted
Why this matters: Looking at the raw table is how you find out what one row represents, which decides everything after it.
First, the syntax: SELECT and FROM
Every query answers the same two questions: which columns do you want, and which table are they in? SELECT lists the columns, FROM names the table, and LIMIT stops after a set number of rows so you are not flooded.
The pattern
SELECT column_a, column_b
FROM table_name
LIMIT 10;
A worked example
SELECT name, country
FROM customers
LIMIT 3;
Returns the name and country of the first three customers. A star in place of the column list returns every column.
Your task
Return every column for the first five orders, so you can see what the table holds.
Hint
SELECT what, FROM where, and LIMIT how many.
You need SELECT *, the orders table, and LIMIT to stop at five rows.
What this tells you: One row is one order, and value is what that order was worth.
2
Exercise 2 of 6
Filter to what matters
Not attempted
Why this matters: Filtering is how a question becomes a report. Everything else is built on WHERE.
First, the syntax: WHERE
WHERE keeps only the rows that match a condition, and runs before anything is calculated. Text goes in single quotes, numbers do not, and conditions combine with AND and OR.
The pattern
SELECT column_a
FROM table_name
WHERE column_b = 'some text'
AND column_c > 100;
A worked example
SELECT name
FROM customers
WHERE country = 'Sweden';
Returns Nordwind and Vantage Foods. Swap = for <>, <, >, <= or >=, and use LIKE '%plan' for partial matches.
Your task
The North region manager wants only their orders. Return the order id, product and value for orders in the North region.
Hint
List the three columns, then add a WHERE condition. Text values go in single quotes.
Text values go in single quotes: WHERE region = 'North'.
What this tells you: Five orders came from the North region, worth £825 in total.
3
Exercise 3 of 6
Count and group
Not attempted
Why this matters: Counting by a dimension answers most questions people bring to a dataset.
First, the syntax: GROUP BY and COUNT
GROUP BY collapses many rows into one row per value, and COUNT(*) says how many rows fell into each group. AS renames the result so it reads well, and ORDER BY sorts it, with DESC for highest first.
The pattern
SELECT column_a, COUNT(*) AS how_many
FROM table_name
GROUP BY column_a
ORDER BY how_many DESC;
A worked example
SELECT country, COUNT(*) AS customers
FROM customers
GROUP BY country
ORDER BY customers DESC;
Turns six customer rows into one row per country, with a count against each.
Your task
How many orders did each region take? Return the region and the number of orders, with the busiest region first.
Hint
GROUP BY the column you want one row per, COUNT(*) the rows, name it with AS, then ORDER BY it.
GROUP BY region, then ORDER BY the counted column DESC.
What this tells you: COUNT(*) counts rows. Grouping turns twelve rows into one per region.
4
Exercise 4 of 6
Add up the money
Not attempted
Why this matters: Volume and value tell different stories. The most ordered product is often not the most valuable.
First, the syntax: SUM, AVG, MIN and MAX
COUNT tells you how many. SUM adds a column up, AVG averages it, and MIN and MAX find the extremes. Each needs to know what it is grouped by, unless you want one total for the whole table.
The pattern
SELECT column_a, SUM(number_column) AS total
FROM table_name
GROUP BY column_a
ORDER BY total DESC;
A worked example
SELECT region, AVG(value) AS average_order
FROM orders
GROUP BY region;
Gives the average order value in each region. Replace AVG with SUM for the total, or MAX for the largest single order.
Your task
Which product brings in the most revenue? Return each product and its total value, highest first.
Hint
Swap COUNT for the aggregate that adds a column up, and name the result so you can sort by it.
SUM(value) gives the money. Name it with AS so you can sort by it.
What this tells you: Starter appears often but earns least. Pro plan carries the revenue.
5
Exercise 5 of 6
Join two tables
Not attempted
Why this matters: Real answers almost always need two tables joined on a shared key.
First, the syntax: JOIN
Most useful answers need two tables. A JOIN matches rows in one table to rows in another using a column they share, usually an id. A short alias such as o or c keeps it readable, and you then write alias.column.
The pattern
SELECT a.column_1, b.column_2
FROM table_a a
JOIN table_b b ON a.shared_id = b.shared_id;
A worked example
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
LIMIT 5;
Puts the customer name beside each order. The id is how the tables connect; the name is what the business reads.
Your task
Finance wants spend by customer name, not by id. Return each customer name and their total spend, highest first.
Hint
Join orders to customers on the column they share, then group by the customer name.
Join on the column both tables share: o.customer_id = c.customer_id.
What this tells you: The id is how the tables connect. The name is what the business reads.
6
Exercise 6 of 6
Answer a real question
Not attempted
Why this matters: HAVING filters after grouping, which is the part that trips up most people the first time.
First, the syntax: HAVING
WHERE filters rows before grouping. HAVING filters the groups afterwards, which is what you need when the condition depends on an aggregate. Using one where you meant the other is the most common SQL mistake.
The pattern
SELECT column_a, SUM(number_column) AS total
FROM table_name
GROUP BY column_a
HAVING SUM(number_column) > 1000;
A worked example
SELECT product, COUNT(*) AS orders
FROM orders
GROUP BY product
HAVING COUNT(*) > 3;
Keeps only the products ordered more than three times. The same condition in WHERE would fail, because the count does not exist yet.
Your task
A manager asks: which countries spend more than £300 with us? Return the country and total spend, for countries above £300 only.
Hint
Group first, then filter the groups. WHERE will not work on a total that does not exist yet.
WHERE filters rows before grouping. HAVING filters the groups afterwards.
What this tells you: WHERE filters rows; HAVING filters groups. Asking for the wrong one is a common bug.
Build Your Confidence with Data
This workbench is the practice block behind every SQL lesson on Insyt.