The practice database: 3 tables, 20 rows
Every exercise runs against this schema. It is deliberately small enough to verify by eye but has the two ingredients real practice needs: a foreign key to join on (dept_id) and a second join target (projects) for the harder exercises.
departments(dept_id, dept_name) -- 5 rows
employees(emp_id, name, dept_id, salary, hired) -- 10 rows
projects(project_id, title, dept_id, budget) -- 5 rows
-- employees quick view:
-- Engineering: Ada 9500, Grace 10200, Turing 8800
-- Design: Paula 7600, Massimo 7200
-- Marketing: Seth 6400, Ogilvy 6900
-- Sales: Zig 5800, Mary 6100
-- HR: Patty 5400Beginner exercises: SELECT, WHERE and ORDER BY
Warm up with single-table queries. Type each one into the playground, press Run, and compare against the expected result before moving on. If a row is missing from your result, the WHERE clause is the first suspect.
SELECT name, salary
FROM employees
WHERE salary > 7000
ORDER BY salary DESC;| # | Exercise | Expected result |
|---|---|---|
| 1 | List every column of every employee | 10 rows, all columns |
| 2 | List only the names and salaries of employees earning over 7000 | 5 rows: Grace, Ada, Turing, Paula, Massimo |
| 3 | Show employees sorted by salary, highest first | Grace 10200 first, Patty 5400 last |
| 4 | Find employees hired in 2022 | 2 rows: Seth (2022-02-01), Patty (2022-10-03) |
| 5 | Count how many employees there are in total | One row, value 10 |
Intermediate exercises: aggregates, GROUP BY and HAVING
Aggregates collapse rows into answers. The mental model that makes GROUP BY click: it first splits the table into buckets, then computes the aggregate once per bucket, and HAVING filters the buckets afterward — which is why aggregate conditions go in HAVING, never in WHERE.
SELECT d.dept_name,
ROUND(AVG(e.salary), 0) AS avg_salary
FROM employees e
JOIN departments d ON d.dept_id = e.dept_id
GROUP BY d.dept_name
ORDER BY avg_salary DESC;| # | Exercise | Expected result |
|---|---|---|
| 6 | Average salary per department, with the department name | 5 rows; Engineering 9500, Design 7400, Marketing 6650, Sales 5950, HR 5400 |
| 7 | Headcount per department, largest first | Engineering 3, then Design/Marketing/Sales/HR 2/2/2/1 |
| 8 | Departments where average salary exceeds 6500, using HAVING | 3 rows: Engineering, Design, Marketing |
| 9 | Total project budget across all departments | One row, 136000 |
| 10 | Earliest and latest hire date in one query | One row: 2018-07-02 and 2023-04-17 |
Advanced exercises: multi-table JOINs and subqueries
These mirror interview questions. Exercise 11 is the money query — it joins three tables and aggregates across two of them. If your numbers are off by rows, check whether you are counting employees or projects: the two GROUP BYs answer different questions with the same shape.
SELECT name, salary, dept_id
FROM employees e
WHERE salary > (
SELECT AVG(salary)
FROM employees
WHERE dept_id = e.dept_id
)
ORDER BY salary DESC;| # | Exercise | Expected result |
|---|---|---|
| 11 | Total payroll and headcount per department, joined with departments | 5 rows; Engineering leads with 28500 across 3 people |
| 12 | Department names that have a project with budget over 30000 | Engineering, Marketing (use DISTINCT) |
| 13 | Employees whose salary is above their department average (subquery) | Grace (10200 > 9500), Paula (7600 > 7400), Ogilvy (6900 > 6650), Mary (6100 > 5950) |
| 14 | The second-highest salary without using LIMIT twice | One row: 9500 (Ada) |
| 15 | Departments with no projects (LEFT JOIN + IS NULL) | No rows — all five departments have a project; rewrite with NOT EXISTS to prove it |
How to run these queries right now
Open the SQL playground and the sample database is already loaded — departments, employees and projects, exactly as described above. Type or paste an exercise query into the editor, press Run, and the result table appears below with row counts. The playground is real SQLite (compiled to WebAssembly), so JOINs, subqueries, HAVING and SQLite-specific functions like ROUND all behave as documented here.
A practical study loop that works: attempt the query before looking at any solution, run it, compare rows to the expected column in the tables above, and only then read the answer. When your result differs, shrink the query — strip the JOIN, then the WHERE — until the intermediate result shows you exactly where the rows went missing.
Questions this guide answers
- sql practice
- sql practice online
- practice sql questions
- sql exercises with solutions
- sql join practice
- sql group by exercises
- sql playground online free
- practice sql online free
Frequently asked questions
Can I practice SQL online for free without installing anything?
Yes. The playground on this site runs a real SQLite engine compiled to WebAssembly, entirely in your browser. There is no account, no usage limit and no upload — the database lives in your tab, resets on reload, and exercises like the ones on this page run in milliseconds.
Which SQL dialect do these exercises use?
SQLite. It covers everything these exercises need — joins, aggregates, subqueries, HAVING — and its syntax is close enough to PostgreSQL or MySQL that the solutions transfer almost unchanged. The main dialect differences you would meet elsewhere are date functions and string concatenation operators.
Is this enough practice to prepare for SQL interview questions?
It is a solid start: exercises 11 through 13 mirror the three most common interview patterns — multi-table aggregation, EXISTS/DISTINCT reasoning and correlated subqueries. For depth, the real skill is adapting: once a query returns the right rows, modify it (add a second condition, switch the sort) until you can predict the output before running it.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping; HAVING filters the groups after aggregation. That is why exercise 8 uses HAVING AVG(salary) > 6500 — the average does not exist until after GROUP BY, so it cannot appear in a WHERE clause.
How do I reset the sample database if I modify the data?
Reload the page — the sample schema and rows are re-created fresh every time the playground loads. You can also re-run the schema script from the Sample button at any time, which restores the original three tables and their 20 rows.
Can I use my own data for SQL practice?
Yes — the playground accepts full CREATE TABLE and INSERT statements, so you can paste your own schema and query it the same way. The sample database simply guarantees that the expected outputs in these exercises match what you see, which is what makes them gradeable.
Run the exercises
SQL Playground
Real SQLite in the browser with the sample database preloaded.
Online SQL Editor
The same engine as a focused paste-run-copy page.
SQL Compiler Online
Run and iterate on SQL without any setup.
JSON Formatter
Format API payloads before you query the same data in SQL.
Python Playground
Take queries further with Python's sqlite3 in-browser.
Markdown Editor
Write up your solutions with tables and code blocks.