# SQL Practice Online — Free Playground & Exercises

Practice SQL online free in a browser playground with 49 guided exercises (16 Beginner, 22 Intermediate, 11 Advanced), hints, and reference solutions. No signup, no install. Queries run on SQLite compiled to WebAssembly (sql.js), entirely in your browser.

- Canonical: https://sqlint.com/sql-practice
- Full text (Markdown): https://sqlint.com/sql-practice/markdown
- Question of the Day: https://sqlint.com/question-of-the-day (new free question daily, no signup)

## Own data

Upload CSV or Excel (.xlsx/.xls/.xlsm, one table per sheet), up to 10 tables and 100,000 rows each, or build tables with CREATE TABLE. Uploads become live in-browser SQLite tables (JOIN across them in free-play mode) and never leave your device.

## Sample databases

### Employees & Departments (21 exercises)

A classic HR dataset: employees with salaries, departments, managers, and hire dates. Used by exercises 1–8, 14–16, 29–33, and 43–47.

Schema:

```sql
CREATE TABLE departments (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  location TEXT
);

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department_id INTEGER REFERENCES departments(id),
  salary REAL NOT NULL,
  hired_date TEXT,
  manager_id INTEGER
);
```

### ShopCo Orders (10 exercises)

An e-commerce dataset: customers, products, orders, and line items. Used by exercises 9–13, 34, and 37–40.

Schema:

```sql
CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  city TEXT
);

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL NOT NULL
);

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER REFERENCES customers(id),
  order_date TEXT,
  status TEXT
);

CREATE TABLE order_items (
  order_id INTEGER REFERENCES orders(id),
  product_id INTEGER REFERENCES products(id),
  quantity INTEGER NOT NULL
);
```

### Fintech Transactions (18 exercises)

A payments dataset: accounts and their credit/debit transactions with dates, categories, and merchants. Used by exercises 17–28, 35–36, 41–42, and 48–49.

Schema:

```sql
CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  holder TEXT NOT NULL,
  type TEXT,
  opened_date TEXT
);

CREATE TABLE transactions (
  id INTEGER PRIMARY KEY,
  account_id INTEGER REFERENCES accounts(id),
  txn_date TEXT,
  amount REAL NOT NULL,
  category TEXT,
  merchant TEXT
);
```

## Exercises

### 1. Select every employee (Beginner, SELECT)

Return every column for every row in the employees table.

Hint: Use SELECT * with the table name.

Reference solution:

```sql
SELECT * FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=1

### 2. Names and salaries only (Beginner, Projection)

Return only the name and salary columns for all employees.

Hint: List the columns you want after SELECT, separated by a comma.

Reference solution:

```sql
SELECT name, salary FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=2

### 3. High earners (Beginner, WHERE)

Return the name and salary of employees earning more than 60,000.

Hint: Filter rows with WHERE salary > 60000.

Reference solution:

```sql
SELECT name, salary FROM employees WHERE salary > 60000;
```

Try it: https://sqlint.com/sql-practice?exercise=3

### 4. Top salaries first (Beginner, ORDER BY)

Return every employee's name and salary, highest salary first.

Hint: Sort with ORDER BY salary DESC.

Reference solution:

```sql
SELECT name, salary FROM employees ORDER BY salary DESC;
```

Try it: https://sqlint.com/sql-practice?exercise=4

### 5. How many employees? (Beginner, Aggregation)

Return a single value: the total number of employees.

Hint: COUNT(*) counts rows.

Reference solution:

```sql
SELECT COUNT(*) AS employee_count FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=5

### 6. Average salary per department (Intermediate, GROUP BY)

For each department, return the department_id and the average salary.

Hint: GROUP BY department_id and use AVG(salary).

Reference solution:

```sql
SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id;
```

Try it: https://sqlint.com/sql-practice?exercise=6

### 7. Names starting with A (Intermediate, LIKE)

Return the names of employees whose name starts with the letter A.

Hint: Use LIKE 'A%' — the % wildcard matches anything after the A.

Reference solution:

```sql
SELECT name FROM employees WHERE name LIKE 'A%';
```

Try it: https://sqlint.com/sql-practice?exercise=7

### 8. Departments with more than one employee (Intermediate, HAVING)

Return each department_id that has more than one employee, along with its headcount.

Hint: Filter aggregated groups with HAVING, not WHERE.

Reference solution:

```sql
SELECT department_id, COUNT(*) AS headcount FROM employees GROUP BY department_id HAVING COUNT(*) > 1;
```

Try it: https://sqlint.com/sql-practice?exercise=8

### 9. Shipped orders (Beginner, WHERE)

Return every column for all orders with status 'shipped'.

Hint: Filter with WHERE status = 'shipped'.

Reference solution:

```sql
SELECT * FROM orders WHERE status = 'shipped';
```

Try it: https://sqlint.com/sql-practice?exercise=9

### 10. Unique order statuses (Beginner, DISTINCT)

Return each distinct order status exactly once.

Hint: DISTINCT removes duplicate values from the result.

Reference solution:

```sql
SELECT DISTINCT status FROM orders;
```

Try it: https://sqlint.com/sql-practice?exercise=10

### 11. Revenue per order (Intermediate, JOIN + SUM)

Return each order_id with its total revenue (sum of quantity × product price).

Hint: Join order_items to products on product_id, then SUM(quantity * price) and GROUP BY order_id.

Reference solution:

```sql
SELECT oi.order_id, SUM(oi.quantity * p.price) AS revenue FROM order_items oi JOIN products p ON p.id = oi.product_id GROUP BY oi.order_id;
```

Try it: https://sqlint.com/sql-practice?exercise=11

### 12. Repeat customers (Intermediate, JOIN + HAVING)

Return the names of customers who placed more than one order.

Hint: Join orders to customers, GROUP BY the customer name, then HAVING COUNT(*) > 1.

Reference solution:

```sql
SELECT c.name, COUNT(*) AS order_count FROM orders o JOIN customers c ON c.id = o.customer_id GROUP BY c.id, c.name HAVING COUNT(*) > 1;
```

Try it: https://sqlint.com/sql-practice?exercise=12

### 13. Top 3 customers by spend (Advanced, Multi-join + LIMIT)

Return the names of the top 3 customers by total spend across all their orders.

Hint: Chain four joins (orders → customers, order_items → products), GROUP BY customer, ORDER BY total spend DESC, then LIMIT 3.

Reference solution:

```sql
SELECT c.name, SUM(oi.quantity * p.price) AS total_spend FROM orders o JOIN customers c ON c.id = o.customer_id JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id GROUP BY c.id, c.name ORDER BY total_spend DESC LIMIT 3;
```

Try it: https://sqlint.com/sql-practice?exercise=13

### 14. Rank salaries within each department (Advanced, Window functions)

Return each employee's name, department_id, and salary, plus their salary rank within their department (1 = highest).

Hint: Use RANK() OVER (PARTITION BY department_id ORDER BY salary DESC).

Reference solution:

```sql
SELECT name, department_id, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=14

### 15. Who reports to whom (Advanced, Self join)

Return each employee's name alongside their manager's name (skip employees without a manager).

Hint: Join employees to itself: e.manager_id = m.id.

Reference solution:

```sql
SELECT e.name AS employee, m.name AS manager FROM employees e JOIN employees m ON e.manager_id = m.id;
```

Try it: https://sqlint.com/sql-practice?exercise=15

### 16. Top earner per department (Advanced, Subquery + window)

Return the name, department_id, and salary of the highest-paid employee in each department.

Hint: Wrap a RANK() window query in a subquery, then filter WHERE rank = 1.

Reference solution:

```sql
SELECT name, department_id, salary FROM (SELECT name, department_id, salary, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS r FROM employees) ranked WHERE r = 1;
```

Try it: https://sqlint.com/sql-practice?exercise=16

### 17. One account's transactions (Beginner, WHERE)

Return every column for all transactions made by account 1.

Hint: Filter with WHERE account_id = 1.

Reference solution:

```sql
SELECT * FROM transactions WHERE account_id = 1;
```

Try it: https://sqlint.com/sql-practice?exercise=17

### 18. Transactions in March (Beginner, Date filtering)

Return every column for transactions dated between 2025-03-01 and 2025-03-31 (inclusive).

Hint: Dates are stored as TEXT in 'YYYY-MM-DD' form, so BETWEEN '2025-03-01' AND '2025-03-31' works.

Reference solution:

```sql
SELECT * FROM transactions WHERE txn_date BETWEEN '2025-03-01' AND '2025-03-31';
```

Try it: https://sqlint.com/sql-practice?exercise=18

### 19. Count transactions per category (Beginner, GROUP BY)

Return each category with the number of transactions in it.

Hint: GROUP BY category and COUNT(*).

Reference solution:

```sql
SELECT category, COUNT(*) AS txn_count FROM transactions GROUP BY category;
```

Try it: https://sqlint.com/sql-practice?exercise=19

### 20. Largest single debit (Beginner, ORDER BY + LIMIT)

Return every column of the single largest debit (most negative amount).

Hint: Filter amount < 0, ORDER BY amount ASC (most negative first), LIMIT 1.

Reference solution:

```sql
SELECT * FROM transactions WHERE amount < 0 ORDER BY amount ASC LIMIT 1;
```

Try it: https://sqlint.com/sql-practice?exercise=20

### 21. Inflow vs outflow (Intermediate, CASE)

Return two values: total money in (sum of positive amounts) and total money out (sum of negative amounts).

Hint: Use CASE WHEN amount > 0 THEN amount ELSE 0 END inside SUM, once per column.

Reference solution:

```sql
SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS total_in, SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END) AS total_out FROM transactions;
```

Try it: https://sqlint.com/sql-practice?exercise=21

### 22. Net flow per month (Intermediate, Date functions)

Return each month (as 'YYYY-MM') with its net flow — the sum of all amounts that month — ordered chronologically.

Hint: strftime('%Y-%m', txn_date) extracts the month from a date string in SQLite.

Reference solution:

```sql
SELECT strftime('%Y-%m', txn_date) AS month, SUM(amount) AS net_flow FROM transactions GROUP BY month ORDER BY month;
```

Try it: https://sqlint.com/sql-practice?exercise=22

### 23. Uncategorized transactions (Intermediate, NULL handling)

Return the id of each transaction with no category, labelling its category 'Uncategorized'.

Hint: COALESCE(category, 'Uncategorized') substitutes a value for NULL. Filter with WHERE category IS NULL.

Reference solution:

```sql
SELECT id, COALESCE(category, 'Uncategorized') AS category FROM transactions WHERE category IS NULL;
```

Try it: https://sqlint.com/sql-practice?exercise=23

### 24. Repeat merchants (Intermediate, HAVING)

Return each merchant (ignore NULL merchants) that appears more than once, with its transaction count.

Hint: GROUP BY merchant, filter groups with HAVING COUNT(*) > 1, and exclude NULLs with WHERE.

Reference solution:

```sql
SELECT merchant, COUNT(*) AS txn_count FROM transactions WHERE merchant IS NOT NULL GROUP BY merchant HAVING COUNT(*) > 1;
```

Try it: https://sqlint.com/sql-practice?exercise=24

### 25. Running balance per account (Advanced, Window functions)

Return each transaction's id, txn_date, and amount, plus a running balance (cumulative sum of amounts) per account, ordered by date within the account.

Hint: Use SUM(amount) OVER (PARTITION BY account_id ORDER BY txn_date, id).

Reference solution:

```sql
SELECT id, txn_date, amount, SUM(amount) OVER (PARTITION BY account_id ORDER BY txn_date, id) AS running_balance FROM transactions;
```

Try it: https://sqlint.com/sql-practice?exercise=25

### 26. Previous transaction amount (Advanced, LAG)

Return each transaction's id, txn_date, and amount, plus the previous transaction's amount for the same account (ordered by date).

Hint: LAG(amount) OVER (PARTITION BY account_id ORDER BY txn_date) — the first row in each partition gets NULL.

Reference solution:

```sql
SELECT id, txn_date, amount, LAG(amount) OVER (PARTITION BY account_id ORDER BY txn_date) AS prev_amount FROM transactions;
```

Try it: https://sqlint.com/sql-practice?exercise=26

### 27. Accounts with no transactions (Advanced, NOT EXISTS)

Return the id and holder of every account that has no transactions at all.

Hint: Use NOT EXISTS with a correlated subquery on transactions.account_id.

Reference solution:

```sql
SELECT a.id, a.holder FROM accounts a WHERE NOT EXISTS (SELECT 1 FROM transactions t WHERE t.account_id = a.id);
```

Try it: https://sqlint.com/sql-practice?exercise=27

### 28. Above your account's average (Advanced, Correlated subquery)

Return the id, account_id, and amount of every transaction whose amount is greater than the average amount for its own account.

Hint: Correlate the subquery to the outer row: WHERE account_id = t.account_id inside the subquery.

Reference solution:

```sql
SELECT t.id, t.account_id, t.amount FROM transactions t WHERE t.amount > (SELECT AVG(amount) FROM transactions WHERE account_id = t.account_id);
```

Try it: https://sqlint.com/sql-practice?exercise=28

### 29. Hired in 2022 (Intermediate, Date functions)

Return the name and hire date of employees hired during 2022.

Hint: strftime('%Y', hired_date) extracts the year from the date string.

Reference solution:

```sql
SELECT name, hired_date FROM employees WHERE strftime('%Y', hired_date) = '2022';
```

Try it: https://sqlint.com/sql-practice?exercise=29

### 30. Salary bands (Intermediate, CASE)

Return each employee's name and salary, plus a band column: 'high' for salaries of 90000 or more, 'mid' for 55000 or more, otherwise 'entry'.

Hint: A searched CASE expression: CASE WHEN ... THEN ... WHEN ... THEN ... ELSE ... END.

Reference solution:

```sql
SELECT name, salary, CASE WHEN salary >= 90000 THEN 'high' WHEN salary >= 55000 THEN 'mid' ELSE 'entry' END AS band FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=30

### 31. Uppercase names (Beginner, String functions)

Return every employee's name converted to uppercase.

Hint: UPPER(name) does it in one call.

Reference solution:

```sql
SELECT UPPER(name) AS upper_name FROM employees;
```

Try it: https://sqlint.com/sql-practice?exercise=31

### 32. Headcount by office location (Intermediate, JOIN + GROUP BY)

Return each department location with the number of employees working there.

Hint: Join employees to departments on department_id = id, then GROUP BY the location.

Reference solution:

```sql
SELECT d.location, COUNT(*) AS headcount FROM employees e JOIN departments d ON d.id = e.department_id GROUP BY d.location;
```

Try it: https://sqlint.com/sql-practice?exercise=32

### 33. Second-highest salary (Advanced, Subquery)

Return a single value: the second-highest salary in the employees table.

Hint: The largest salary that is strictly less than the overall maximum.

Reference solution:

```sql
SELECT MAX(salary) AS second_highest FROM employees WHERE salary < (SELECT MAX(salary) FROM employees);
```

Try it: https://sqlint.com/sql-practice?exercise=33

### 34. Revenue by month (Advanced, JOIN + date functions)

Return each month ('YYYY-MM') with total revenue (sum of quantity × price across all orders), ordered chronologically.

Hint: Join orders → order_items → products, group by strftime('%Y-%m', order_date).

Reference solution:

```sql
SELECT strftime('%Y-%m', o.order_date) AS month, SUM(oi.quantity * p.price) AS revenue FROM orders o JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id GROUP BY month ORDER BY month;
```

Try it: https://sqlint.com/sql-practice?exercise=34

### 35. Merchants and categories combined (Intermediate, UNION)

Return one combined, de-duplicated list of all category names and all merchant names (skip NULLs).

Hint: UNION removes duplicates automatically — select from transactions twice and combine.

Reference solution:

```sql
SELECT category AS name FROM transactions WHERE category IS NOT NULL UNION SELECT merchant FROM transactions WHERE merchant IS NOT NULL;
```

Try it: https://sqlint.com/sql-practice?exercise=35

### 36. March salaries only (Beginner, AND conditions)

Return txn_date, amount, and merchant for transactions in March 2025 that are credits (amount > 0).

Hint: Combine two conditions with AND — reuse the BETWEEN date trick and amount > 0.

Reference solution:

```sql
SELECT txn_date, amount, merchant FROM transactions WHERE txn_date BETWEEN '2025-03-01' AND '2025-03-31' AND amount > 0;
```

Try it: https://sqlint.com/sql-practice?exercise=36

### 37. Orders by status (Beginner, GROUP BY)

Return each order status with the number of orders in it.

Hint: GROUP BY status, COUNT(*).

Reference solution:

```sql
SELECT status, COUNT(*) AS order_count FROM orders GROUP BY status;
```

Try it: https://sqlint.com/sql-practice?exercise=37

### 38. Average order value (Intermediate, JOIN + ROUND)

Return a single value: the average line-item value (quantity × price) rounded to 2 decimal places.

Hint: ROUND(AVG(...), 2) after joining order_items to products.

Reference solution:

```sql
SELECT ROUND(AVG(oi.quantity * p.price), 2) AS avg_line_value FROM order_items oi JOIN products p ON p.id = oi.product_id;
```

Try it: https://sqlint.com/sql-practice?exercise=38

### 39. Products never ordered (Intermediate, LEFT JOIN / NOT EXISTS)

Return the names of products that have never appeared in any order.

Hint: LEFT JOIN products to order_items and keep rows where the joined side is NULL — or use NOT EXISTS.

Reference solution:

```sql
SELECT p.name FROM products p LEFT JOIN order_items oi ON oi.product_id = p.id WHERE oi.order_id IS NULL;
```

Try it: https://sqlint.com/sql-practice?exercise=39

### 40. Customers who never ordered (Intermediate, NOT IN / NOT EXISTS)

Return the names of customers with no orders in the orders table.

Hint: WHERE id NOT IN (SELECT customer_id FROM orders) — or NOT EXISTS for a NULL-safe version.

Reference solution:

```sql
SELECT name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders);
```

Try it: https://sqlint.com/sql-practice?exercise=40

### 41. Top account by net flow (Advanced, JOIN + aggregation)

Return the holder name and net flow (sum of amounts) of the account with the highest net flow.

Hint: Join transactions to accounts, GROUP BY account, ORDER BY the sum DESC, LIMIT 1.

Reference solution:

```sql
SELECT a.holder, SUM(t.amount) AS net_flow FROM transactions t JOIN accounts a ON a.id = t.account_id GROUP BY a.id, a.holder ORDER BY net_flow DESC LIMIT 1;
```

Try it: https://sqlint.com/sql-practice?exercise=41

### 42. Fourth-highest distinct amount (Beginner, LIMIT + OFFSET)

Return the 4th-highest distinct transaction amount — skip the top three amounts with OFFSET and keep one row with LIMIT.

Hint: ORDER BY amount DESC, add LIMIT 1 OFFSET 3, and use DISTINCT so tied amounts don't count as separate rows.

Reference solution:

```sql
SELECT DISTINCT amount FROM transactions ORDER BY amount DESC LIMIT 1 OFFSET 3;
```

Try it: https://sqlint.com/sql-practice?exercise=42

### 43. Departments above the company average (Intermediate, CTE (WITH))

Return each department_id whose average salary is above the company average, along with that department's average. Compute the company average once inside a CTE.

Hint: WITH company_avg AS (SELECT AVG(salary) AS avg_salary FROM employees) ... then HAVING AVG(salary) > (the CTE value).

Reference solution:

```sql
WITH company_avg AS (SELECT AVG(salary) AS avg_salary FROM employees) SELECT department_id, AVG(salary) AS dept_avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > (SELECT avg_salary FROM company_avg) ORDER BY department_id;
```

Try it: https://sqlint.com/sql-practice?exercise=43

### 44. High earners (view projection) (Intermediate, Views)

Return the name and salary of every employee earning 90000 or more — the rows a 'high earners' view would expose.

Hint: This is the SELECT a CREATE VIEW high_earners AS ... would encapsulate: filter salary >= 90000.

Reference solution:

```sql
SELECT name, salary FROM employees WHERE salary >= 90000 ORDER BY salary DESC;
```

Try it: https://sqlint.com/sql-practice?exercise=44

### 45. Department headcounts (procedure) (Intermediate, Stored procedures)

Return each department's id, name, and employee count — the result set a get_department_headcounts() stored procedure would return. Use a LEFT JOIN so departments with no employees still appear.

Hint: Join departments to employees on department_id, GROUP BY the department, and COUNT only the employee side.

Reference solution:

```sql
SELECT d.id AS department_id, d.name, COUNT(e.id) AS headcount FROM departments d LEFT JOIN employees e ON e.department_id = d.id GROUP BY d.id, d.name ORDER BY d.id;
```

Try it: https://sqlint.com/sql-practice?exercise=45

### 46. Instant primary-key lookup (Beginner, Indexes (PK lookup))

Return every column of the employee with id = 5 — the lookup a primary-key index resolves instantly.

Hint: A WHERE clause on the primary-key column uses the PK index.

Reference solution:

```sql
SELECT * FROM employees WHERE id = 5;
```

Try it: https://sqlint.com/sql-practice?exercise=46

### 47. Employees with their department (Intermediate, JOIN (N+1 fix))

Return each employee's name and their department's name using a single JOIN — the rewrite that fixes the N+1 query problem.

Hint: Join employees to departments on department_id = id.

Reference solution:

```sql
SELECT e.name, d.name AS department FROM employees e JOIN departments d ON d.id = e.department_id ORDER BY e.id;
```

Try it: https://sqlint.com/sql-practice?exercise=47

### 48. Projected ₹5,000 transfer (Intermediate, Transactions (projected))

Return each account holder with their net flow (sum of all amounts), plus the net flow after a ₹5,000 transfer from account 1 (Riya) to account 3 (Nisha) — computed with CASE so both sides of the transfer are one atomic expression.

Hint: SELECT holder, SUM(amount), SUM(amount) + CASE WHEN account_id = 1 THEN -5000 WHEN account_id = 3 THEN 5000 ELSE 0 END ... GROUP BY the account.

Reference solution:

```sql
SELECT a.holder, SUM(t.amount) AS net_flow, SUM(t.amount) + CASE WHEN a.id = 1 THEN -5000 WHEN a.id = 3 THEN 5000 ELSE 0 END AS net_after_transfer FROM transactions t JOIN accounts a ON a.id = t.account_id GROUP BY a.id, a.holder ORDER BY a.id;
```

Try it: https://sqlint.com/sql-practice?exercise=48

### 49. Consistency audit (LEFT JOIN) (Intermediate, ACID (consistency))

Run a consistency audit: return every account holder with their transaction count, using a LEFT JOIN so accounts with zero transactions still appear — the query that checks referential consistency between the two tables.

Hint: LEFT JOIN accounts to transactions, GROUP BY the account, and count transactions only.

Reference solution:

```sql
SELECT a.holder, COUNT(t.id) AS txn_count FROM accounts a LEFT JOIN transactions t ON t.account_id = a.id GROUP BY a.id, a.holder ORDER BY a.id;
```

Try it: https://sqlint.com/sql-practice?exercise=49

---

Source: https://sqlint.com/sql-practice
