The fastest way to get comfortable with SQL is to write queries and check them against a real answer. These problems are grouped by difficulty: beginner, intermediate, joins and advanced. Every problem has a hidden solution and the exact result it returns, so you can compare your output row by row.
Set up the practice database
All problems use four tables from a tiny online shop: customers, products, orders and order_items. The script below works in SQLite and, with minor type changes, in PostgreSQL, MySQL and SQL Server. The quickest route is a free SQLite database in a desktop client such as Beekeeper Studio: create a new SQLite database, open a query tab, paste the script and run it.
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, country TEXT, signup_date TEXT);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, order_date TEXT, status TEXT);
CREATE TABLE order_items (order_id INTEGER, product_id INTEGER, quantity INTEGER);
INSERT INTO customers VALUES
(1, 'Ana Silva', 'Brazil', '2025-01-15'),
(2, 'Ben Carter', 'USA', '2025-02-03'),
(3, 'Chloe Dubois', 'France', '2025-02-20'),
(4, 'Dev Patel', 'India', '2025-03-11'),
(5, 'Emma Jones', 'USA', '2025-04-02'),
(6, 'Felix Wagner', 'Germany', '2025-05-18'),
(7, 'Grace Kim', 'USA', '2025-06-07'),
(8, 'Hiro Tanaka', 'Japan', '2025-06-30');
INSERT INTO products VALUES
(1, 'Laptop', 'Electronics', 899.00),
(2, 'Headphones', 'Electronics', 129.50),
(3, 'Desk Lamp', 'Home', 34.99),
(4, 'Notebook', 'Stationery', 4.25),
(5, 'Pen Set', 'Stationery', 12.00),
(6, 'Office Chair', 'Home', 189.00),
(7, 'Webcam', 'Electronics', 59.90),
(8, 'Backpack', 'Accessories', 45.00);
INSERT INTO orders VALUES
(1, 1, '2025-07-01', 'shipped'),
(2, 2, '2025-07-03', 'shipped'),
(3, 2, '2025-07-15', 'cancelled'),
(4, 3, '2025-07-20', 'shipped'),
(5, 5, '2025-08-02', 'shipped'),
(6, 1, '2025-08-09', 'pending'),
(7, 4, '2025-08-12', 'shipped'),
(8, 5, '2025-08-21', 'shipped'),
(9, 7, '2025-09-04', 'shipped'),
(10, 2, '2025-09-10', 'shipped'),
(11, 3, '2025-09-18', 'pending'),
(12, 7, '2025-09-25', 'cancelled');
INSERT INTO order_items VALUES
(1, 1, 1), (1, 4, 3),
(2, 2, 1), (2, 7, 1),
(3, 6, 1),
(4, 3, 2), (4, 5, 2),
(5, 8, 1), (5, 4, 5),
(6, 2, 2),
(7, 1, 1), (7, 2, 1),
(8, 5, 3),
(9, 6, 1), (9, 3, 1),
(10, 4, 10), (10, 8, 2),
(11, 7, 1),
(12, 1, 1);
How the tables connect: orders.customer_id points at customers.id, and each row of order_items links one order to one product through order_id and product_id.
New to SQL? Do the free interactive lessons first. They run in your browser with no setup, and give hints when you get stuck. Keep our SQL cheat sheet open as a reference while you work through the problems below.
How to use these problems
- Read the problem and write your own query before opening the solution.
- Run it and compare your result with the one shown. Row order matters only where the problem asks for sorting.
- If your answer differs, read the solution, then re-run yours and find out why it differed. That is where the learning happens.
- There is usually more than one correct query. If yours returns the same rows, it is right.
Beginner: SELECT, WHERE, ORDER BY
Problem 1: List every product.
Return all columns and all rows from products.
Show solution
SELECT * FROM products;
Result (8 rows):
| id | name | category | price |
|---|---|---|---|
| 1 | Laptop | Electronics | 899.0 |
| 2 | Headphones | Electronics | 129.5 |
| 3 | Desk Lamp | Home | 34.99 |
| 4 | Notebook | Stationery | 4.25 |
| 5 | Pen Set | Stationery | 12.0 |
| 6 | Office Chair | Home | 189.0 |
| 7 | Webcam | Electronics | 59.9 |
| 8 | Backpack | Accessories | 45.0 |
Problem 2: Names and prices only.
Return just the name and price of each product.
Show solution
SELECT name, price FROM products;
Result (8 rows):
| name | price |
|---|---|
| Laptop | 899.0 |
| Headphones | 129.5 |
| Desk Lamp | 34.99 |
| Notebook | 4.25 |
| Pen Set | 12.0 |
| Office Chair | 189.0 |
| Webcam | 59.9 |
| Backpack | 45.0 |
Problem 3: Customers from the USA.
Return the name of every customer whose country is USA.
Show solution
SELECT name FROM customers WHERE country = 'USA';
Result (3 rows):
| name |
|---|
| Ben Carter |
| Emma Jones |
| Grace Kim |
Problem 4: Products over $100.
Return the name and price of products that cost more than 100, most expensive first.
Show solution
SELECT name, price FROM products WHERE price > 100 ORDER BY price DESC;
Result (3 rows):
| name | price |
|---|---|
| Laptop | 899.0 |
| Office Chair | 189.0 |
| Headphones | 129.5 |
Problem 5: Cheapest three products.
Return the three cheapest products (name and price).
Show solution
SELECT name, price FROM products ORDER BY price ASC LIMIT 3;
Result (3 rows):
| name | price |
|---|---|
| Notebook | 4.25 |
| Pen Set | 12.0 |
| Desk Lamp | 34.99 |
Problem 6: Unique countries.
List each country that has at least one customer, once, in alphabetical order.
Show solution
SELECT DISTINCT country FROM customers ORDER BY country;
Result (6 rows):
| country |
|---|
| Brazil |
| France |
| Germany |
| India |
| Japan |
| USA |
Problem 7: Names starting with a letter.
Return products whose name starts with the letter P or the letter W.
Show solution
SELECT name FROM products WHERE name LIKE 'P%' OR name LIKE 'W%';
Result (2 rows):
| name |
|---|
| Pen Set |
| Webcam |
Problem 8: Stationery or Home.
Return the name and category of every product in the Stationery or Home categories.
Show solution
SELECT name, category FROM products WHERE category IN ('Stationery', 'Home');
Result (4 rows):
| name | category |
|---|---|
| Desk Lamp | Home |
| Notebook | Stationery |
| Pen Set | Stationery |
| Office Chair | Home |
Intermediate: aggregates and GROUP BY
Problem 9: Count the customers.
How many customers are there?
Show solution
SELECT COUNT(*) AS customer_count FROM customers;
Result (1 row):
| customer_count |
|---|
| 8 |
Problem 10: Average product price.
What is the average product price, rounded to 2 decimal places?
Show solution
SELECT ROUND(AVG(price), 2) AS avg_price FROM products;
Result (1 row):
| avg_price |
|---|
| 171.71 |
Problem 11: Customers per country.
For each country, count its customers. Show the busiest countries first.
Show solution
SELECT country, COUNT(*) AS customers
FROM customers
GROUP BY country
ORDER BY customers DESC, country;
Result (6 rows):
| country | customers |
|---|---|
| USA | 3 |
| Brazil | 1 |
| France | 1 |
| Germany | 1 |
| India | 1 |
| Japan | 1 |
Problem 12: Orders per status.
Count the orders in each status.
Show solution
SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC, status;
Result (3 rows):
| status | orders |
|---|---|
| shipped | 8 |
| cancelled | 2 |
| pending | 2 |
Problem 13: Categories with more than one product.
List each product category that has more than one product, with its product count.
Show solution
SELECT category, COUNT(*) AS products
FROM products
GROUP BY category
HAVING COUNT(*) > 1
ORDER BY products DESC, category;
Result (3 rows):
| category | products |
|---|---|
| Electronics | 3 |
| Home | 2 |
| Stationery | 2 |
Problem 14: Price range per category.
For each category show the cheapest and the most expensive price.
Show solution
SELECT category, MIN(price) AS cheapest, MAX(price) AS priciest
FROM products
GROUP BY category
ORDER BY category;
Result (4 rows):
| category | cheapest | priciest |
|---|---|---|
| Accessories | 45.0 | 45.0 |
| Electronics | 59.9 | 899.0 |
| Home | 34.99 | 189.0 |
| Stationery | 4.25 | 12.0 |
Problem 15: Orders placed in August 2025.
How many orders were placed in August 2025?
Show solution
SELECT COUNT(*) AS august_orders
FROM orders
WHERE order_date >= '2025-08-01'
AND order_date < '2025-09-01';
Result (1 row):
| august_orders |
|---|
| 4 |
Problem 16: Label each product.
Return each product's name and price, plus a tier column: 'budget' under 20, 'mid' from 20 up to 100, otherwise 'premium'.
Show solution
SELECT name, price,
CASE
WHEN price < 20 THEN 'budget'
WHEN price < 100 THEN 'mid'
ELSE 'premium'
END AS tier
FROM products
ORDER BY price;
Result (8 rows):
| name | price | tier |
|---|---|---|
| Notebook | 4.25 | budget |
| Pen Set | 12.0 | budget |
| Desk Lamp | 34.99 | mid |
| Backpack | 45.0 | mid |
| Webcam | 59.9 | mid |
| Headphones | 129.5 | premium |
| Office Chair | 189.0 | premium |
| Laptop | 899.0 | premium |
Joins
Problem 17: Who placed each order?
Return each order id and the name of the customer who placed it.
Show solution
SELECT o.id AS order_id, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.id;
Result (12 rows):
| order_id | name |
|---|---|
| 1 | Ana Silva |
| 2 | Ben Carter |
| 3 | Ben Carter |
| 4 | Chloe Dubois |
| 5 | Emma Jones |
| 6 | Ana Silva |
| 7 | Dev Patel |
| 8 | Emma Jones |
| 9 | Grace Kim |
| 10 | Ben Carter |
| 11 | Chloe Dubois |
| 12 | Grace Kim |
Problem 18: Customers who never ordered.
List the customers who have not placed any order.
Show solution
SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Result (2 rows):
| name |
|---|
| Felix Wagner |
| Hiro Tanaka |
Problem 19: Items in each order.
For order 1, list each product name and the quantity ordered.
Show solution
SELECT p.name, oi.quantity
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.order_id = 1;
Result (2 rows):
| name | quantity |
|---|---|
| Laptop | 1 |
| Notebook | 3 |
Problem 20: Line totals.
For every order line, return the order id, product name and line total (price x quantity) for orders 1 to 3, sorted by order id then product name.
Show solution
SELECT oi.order_id, p.name, p.price * oi.quantity AS line_total
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.order_id <= 3
ORDER BY oi.order_id, p.name;
Result (5 rows):
| order_id | name | line_total |
|---|---|---|
| 1 | Laptop | 899.0 |
| 1 | Notebook | 12.75 |
| 2 | Headphones | 129.5 |
| 2 | Webcam | 59.9 |
| 3 | Office Chair | 189.0 |
Problem 21: Orders per customer, including zero.
Return every customer with the number of orders they placed (0 if none). Sort by count descending, then name.
Show solution
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;
Result (8 rows):
| name | orders |
|---|---|
| Ben Carter | 3 |
| Ana Silva | 2 |
| Chloe Dubois | 2 |
| Emma Jones | 2 |
| Grace Kim | 2 |
| Dev Patel | 1 |
| Felix Wagner | 0 |
| Hiro Tanaka | 0 |
Problem 22: Revenue per customer (shipped only).
Total spent per customer on shipped orders only, highest first. Skip customers with no shipped orders.
Show solution
SELECT c.name, SUM(p.price * oi.quantity) AS total_spent
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'shipped'
GROUP BY c.id, c.name
ORDER BY total_spent DESC;
Result (6 rows):
| name | total_spent |
|---|---|
| Dev Patel | 1028.5 |
| Ana Silva | 911.75 |
| Ben Carter | 321.9 |
| Grace Kim | 223.99 |
| Emma Jones | 102.25 |
| Chloe Dubois | 93.98 |
Advanced: subqueries, CTEs and window functions
Problem 23: Above-average products.
Return products priced above the average product price.
Show solution
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
Result (2 rows):
| name | price |
|---|---|
| Laptop | 899.0 |
| Office Chair | 189.0 |
Problem 24: Products never ordered.
Which products have never appeared in any order?
Show solution
SELECT name
FROM products
WHERE id NOT IN (SELECT product_id FROM order_items);
Result (0 rows):
| name |
|---|
Problem 25: Best-selling category.
Total units sold per category (all order statuses except cancelled), most units first.
Show solution
SELECT p.category, SUM(oi.quantity) AS units
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY p.category
ORDER BY units DESC, p.category;
Result (4 rows):
| category | units |
|---|---|
| Stationery | 23 |
| Electronics | 8 |
| Home | 4 |
| Accessories | 3 |
Problem 26: Rank products by price within category.
For each product, show its rank by price inside its category (1 = most expensive).
Show solution
SELECT category, name, price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS price_rank
FROM products
ORDER BY category, price_rank;
Result (8 rows):
| category | name | price | price_rank |
|---|---|---|---|
| Accessories | Backpack | 45.0 | 1 |
| Electronics | Laptop | 899.0 | 1 |
| Electronics | Headphones | 129.5 | 2 |
| Electronics | Webcam | 59.9 | 3 |
| Home | Office Chair | 189.0 | 1 |
| Home | Desk Lamp | 34.99 | 2 |
| Stationery | Pen Set | 12.0 | 1 |
| Stationery | Notebook | 4.25 | 2 |
Problem 27: Each customer's first order.
Return each customer's earliest order date. Customers with no orders should not appear.
Show solution
SELECT c.name, MIN(o.order_date) AS first_order
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY first_order;
Result (6 rows):
| name | first_order |
|---|---|
| Ana Silva | 2025-07-01 |
| Ben Carter | 2025-07-03 |
| Chloe Dubois | 2025-07-20 |
| Emma Jones | 2025-08-02 |
| Dev Patel | 2025-08-12 |
| Grace Kim | 2025-09-04 |
Problem 28: Running total of orders.
Using a CTE, count orders per month and add a running total.
Show solution
WITH monthly AS (
SELECT substr(order_date, 1, 7) AS month, COUNT(*) AS orders
FROM orders
GROUP BY substr(order_date, 1, 7)
)
SELECT month, orders,
SUM(orders) OVER (ORDER BY month) AS running_total
FROM monthly
ORDER BY month;
Result (3 rows):
| month | orders | running_total |
|---|---|---|
| 2025-07 | 4 | 4 |
| 2025-08 | 4 | 8 |
| 2025-09 | 4 | 12 |
What to practice next
- Filtering and sorting: the WHERE, ORDER BY and LIKE lessons.
- Grouping: GROUP BY lesson, plus our guides to GROUP BY and HAVING.
- Joins: the joins and LEFT JOIN lessons, plus INNER JOIN and LEFT JOIN guides.
- CASE and subqueries: CASE and subqueries.
- Make your own problems: change the questions, for example "top customer per country", and check the result by hand.
FAQ
Where can I practice SQL for free?
SQL Easy has free interactive lessons that run in your browser with instant feedback. For open-ended practice like the problems above, use any free database client with SQLite.
Which database should I use for these problems?
SQLite is the easiest because it needs no server. The queries are standard SQL, so they also work in PostgreSQL and MySQL. Only the substr month trick in problem 28 differs by database.
How many SQL problems should I solve to get good?
Aim for consistency over volume: a few problems a day for two weeks covers all the core patterns (filtering, aggregating, joining, subqueries). Redo the ones you got wrong a few days later.
Are these problems good for interview preparation?
The intermediate, join and advanced sections cover the patterns that come up most in analyst and engineering screens: anti-joins, aggregating across joins, and window functions.