Challenges:
SQL EditorCtrl+Enterto run
13
Output
⚡Run a query to see resultsCtrl+Enter or click Run

SQL Practice Online — Free SQL Playground

Practice SQL online with a free interactive SQL playground. Write and run real SQL queries against realistic relational datasets, explore database schemas, solve SQL practice problems, and build the skills you need for SQL interviews.

The editor above is connected to a real SQLite database with 8 related tables and 1,476 rows: an online store with customers, orders, products, payments and reviews, plus a company’s employees and departments. Nothing needs installing, and you can run queries without signing up.

  • Free
  • SQLite
  • 8 tables · 1,476 rows
  • No installation
  • No signup to run queries
  • Works on mobile

Practice SQL Online with a Real Database

Reading a query and writing one are different skills. You get better at SQL by typing a query, running it, and finding out that the result is not what you expected. This playground is built for that loop: your query is executed by SQLite and the actual rows come back, or the actual error message does.

How a practice session works

  1. Write. Look at the Schema panel to see the tables and columns, then type a query in the editor.
  2. Run. Press Ctrl+Enter or use the Run Query button.
  3. Inspect. Read the result table, the row count and the execution time. If the query fails, read the error.
  4. Improve. Change one thing and run it again. Add a filter, swap a join type, sort differently.

What is in the practice database

The tables are linked by foreign keys, so the same data supports single-table filtering, multi-table joins and analytical queries. To see how the tables connect, open the interactive database schema.

TableRowsWhat it holds
users150Customers: name, email, city, country, signup date, status
orders200Orders: customer, total, discount, status, order and ship dates, shipping city
order_items496Order lines: order, product, quantity, unit price, subtotal
products120Catalogue: name, category, price, cost, stock, SKU, status, rating
payments200Payments: order, customer, amount, method, status, paid date
reviews180Product reviews: product, customer, 1 to 5 rating, comment, review date
employees120Staff: department, title, salary, hire date, manager, city
departments10Departments: name, location, headcount, budget

The database is read-only: queries start with SELECT or WITH, results are capped at 500 rows, and the data is the same every time you come back. Orders are dated from January 2022 to December 2024, so filter on fixed dates instead of today’s date.

SQL Practice Questions with Answers

Each question below comes with a working answer. Every query on this page has been run against the playground’s database, and each one has a link that loads it into the editor and runs it. Try writing your own version first, then compare.

Beginner SQL Practice

One table at a time: choosing columns, filtering rows, sorting and limiting. If any of these are new, the lessons on the WHERE clause, ORDER BY, LIMIT and DISTINCT explain them step by step.

Find customers in one city

Covers: SELECT, WHERE, ORDER BY

Pick the columns you need, keep only the rows that match a condition, and sort the result. Most reports start exactly like this.

SELECT first_name, last_name, email, city
FROM   users
WHERE  city = 'Phoenix'
ORDER BY last_name;
▶ Run this query in the playground

List the product categories

Covers: DISTINCT

The products table stores a category on every row. DISTINCT collapses the repeats so each category appears once.

SELECT DISTINCT category
FROM   products
ORDER BY category;
▶ Run this query in the playground

Best-rated active products under $500

Covers: WHERE with AND, ORDER BY, LIMIT

Two conditions joined with AND, a two-level sort, and LIMIT to keep the top ten.

SELECT name, category, price, rating
FROM   products
WHERE  status = 'active'
  AND  price < 500
ORDER BY rating DESC, price
LIMIT  10;
▶ Run this query in the playground

The ten most recent orders

Covers: ORDER BY DESC, LIMIT

Dates are stored as YYYY-MM-DD text, so sorting them as text also sorts them chronologically.

SELECT id, user_id, total, status, order_date
FROM   orders
ORDER BY order_date DESC
LIMIT  10;
▶ Run this query in the playground

Intermediate SQL Practice

Summarising and combining tables. These questions use GROUP BY with aggregate functions, HAVING, SQL JOINs, CASE expressions and subqueries.

Orders and revenue by status

Covers: GROUP BY, COUNT, SUM

One output row per order status, with the number of orders and their combined value.

SELECT   status,
         COUNT(*)             AS orders,
         ROUND(SUM(total), 2) AS revenue
FROM     orders
GROUP BY status
ORDER BY orders DESC;
▶ Run this query in the playground

Customers who have never placed an order

Covers: LEFT JOIN, IS NULL

A LEFT JOIN keeps every customer. Where no order matched, the order columns are NULL, and that is the filter.

SELECT u.id, u.first_name, u.last_name, u.email
FROM   users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE  o.id IS NULL
ORDER BY u.last_name;
▶ Run this query in the playground

Top five products by units sold

Covers: JOIN, SUM, GROUP BY, LIMIT

Join the line items to the product catalogue, then total the quantity and revenue for each product.

SELECT   p.name,
         SUM(oi.quantity)           AS units_sold,
         ROUND(SUM(oi.subtotal), 2) AS revenue
FROM     products p
JOIN     order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name
ORDER BY units_sold DESC
LIMIT    5;
▶ Run this query in the playground

Categories priced above the catalogue average

Covers: HAVING, scalar subquery

WHERE cannot see an aggregate, so the comparison against the overall average price goes in HAVING.

SELECT   category,
         COUNT(*)             AS products,
         ROUND(AVG(price), 2) AS avg_price
FROM     products
GROUP BY category
HAVING   AVG(price) > (SELECT AVG(price) FROM products)
ORDER BY avg_price DESC;
▶ Run this query in the playground

Group products into price bands

Covers: CASE, GROUP BY

CASE turns a number into a label, and the label can then be grouped like any other column.

SELECT   CASE
           WHEN price < 500  THEN 'Under $500'
           WHEN price < 1000 THEN '$500 to $999'
           ELSE '$1,000 and up'
         END        AS price_band,
         COUNT(*)   AS products,
         MIN(price) AS lowest_price,
         MAX(price) AS highest_price
FROM     products
GROUP BY price_band
ORDER BY lowest_price;
▶ Run this query in the playground

Monthly orders and average order value in 2024

Covers: strftime, date filter, AVG

strftime is the SQLite way to cut a date down to its month. The date range is fixed because the sample orders run from January 2022 to December 2024.

SELECT   strftime('%Y-%m', order_date) AS month,
         COUNT(*)                       AS orders,
         ROUND(AVG(total), 2)           AS avg_order_value
FROM     orders
WHERE    order_date >= '2024-01-01'
  AND    order_date <  '2025-01-01'
GROUP BY month
ORDER BY month;
▶ Run this query in the playground

Advanced SQL Practice

Multi-step analysis: common table expressions, window functions, ranking with RANK and DENSE_RANK, row-to-row comparisons with LAG and LEAD, and correlated subqueries.

Split customers into spending quartiles

Covers: CTE, NTILE

The CTE totals each customer once. The outer query then ranks those totals into four equal-sized groups.

WITH customer_totals AS (
  SELECT   user_id,
           COUNT(*)   AS order_count,
           SUM(total) AS lifetime_value
  FROM     orders
  GROUP BY user_id
)
SELECT u.first_name || ' ' || u.last_name AS customer,
       ct.order_count,
       ROUND(ct.lifetime_value, 2)        AS lifetime_value,
       NTILE(4) OVER (ORDER BY ct.lifetime_value DESC) AS quartile
FROM   customer_totals ct
JOIN   users u ON u.id = ct.user_id
ORDER BY ct.lifetime_value DESC;
▶ Run this query in the playground

Top three products by revenue in every category

Covers: RANK, PARTITION BY, CTE

A window function cannot be filtered in the same SELECT that computes it, so the ranking goes in a CTE and the filter outside.

WITH product_revenue AS (
  SELECT   p.category,
           p.name,
           ROUND(SUM(oi.subtotal), 2) AS revenue
  FROM     products p
  JOIN     order_items oi ON oi.product_id = p.id
  GROUP BY p.id, p.category, p.name
),
ranked AS (
  SELECT category, name, revenue,
         RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS rank_in_category
  FROM   product_revenue
)
SELECT category, name, revenue, rank_in_category
FROM   ranked
WHERE  rank_in_category <= 3
ORDER BY category, rank_in_category;
▶ Run this query in the playground

Month-over-month revenue change

Covers: LAG

LAG reads the previous row of the ordered result. The first month has nothing before it, so its change is NULL.

WITH monthly AS (
  SELECT   strftime('%Y-%m', order_date) AS month,
           ROUND(SUM(total), 2)          AS revenue
  FROM     orders
  WHERE    status NOT IN ('cancelled', 'refunded')
  GROUP BY month
)
SELECT month,
       revenue,
       LAG(revenue) OVER (ORDER BY month)                      AS prev_month,
       ROUND(revenue - LAG(revenue) OVER (ORDER BY month), 2)  AS change
FROM   monthly
ORDER BY month;
▶ Run this query in the playground

Employees paid above their department average

Covers: correlated subquery

The inner query refers to the outer row (e.department_id), so the average is worked out for that employee’s own department.

SELECT e.first_name, e.last_name, e.department, e.salary
FROM   employees e
WHERE  e.salary > (
  SELECT AVG(e2.salary)
  FROM   employees e2
  WHERE  e2.department_id = e.department_id
)
ORDER BY e.department, e.salary DESC;
▶ Run this query in the playground

Moving average over each customer’s last three orders

Covers: window frame (ROWS BETWEEN)

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW is a three-row window: this order and the two before it. It counts rows, not calendar days, so it is a three-order average and not a three-day or three-month one.

SELECT o.user_id,
       o.order_date,
       o.total,
       ROUND(AVG(o.total) OVER (
         PARTITION BY o.user_id
         ORDER BY     o.order_date, o.id
         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ), 2) AS avg_last_3_orders
FROM   orders o
ORDER BY o.user_id, o.order_date, o.id;
▶ Run this query in the playground

Delivered revenue by customer city and product category

Covers: four-table JOIN

Follow the foreign keys: users to orders, orders to order_items, order_items to products.

SELECT   u.city,
         p.category,
         COUNT(DISTINCT o.id)       AS orders,
         ROUND(SUM(oi.subtotal), 2) AS revenue
FROM     users u
JOIN     orders o       ON o.user_id   = u.id
JOIN     order_items oi ON oi.order_id = o.id
JOIN     products p     ON p.id        = oi.product_id
WHERE    o.status = 'delivered'
GROUP BY u.city, p.category
ORDER BY revenue DESC
LIMIT    20;
▶ Run this query in the playground

SQL Exercises for Beginners and Analysts

If you want to drill one topic at a time, pair a lesson with the playground: read the lesson, then write three or four queries of your own on the same clause before moving on. A sensible order is:

  • SELECT and WHERE exercises. Start with the six core SQL clauses, then filter with WHERE and handle missing values with IS NULL.
  • ORDER BY exercises. Sort by one column, then by two, and combine ORDER BY with LIMIT to get a top ten.
  • GROUP BY and HAVING exercises. Count and total by category, month or status, then filter the groups. See SQL GROUP BY practice and GROUP BY vs HAVING.
  • JOIN exercises. Join two tables, then three. Compare an inner join with a LEFT JOIN on the same tables and count the rows. The guide to SQL JOINs explained covers each type.
  • Subquery and CTE exercises. Rewrite a subquery as a CTE and check that both return the same rows.
  • Window-function exercises. Rank rows within a group, compute a running total, and compare each row with the one before it. Start with SQL window function practice.

The full list of free lessons, in order, is on the Learn SQL page.

SQL Coding Practice

Once the individual clauses feel familiar, practice putting them together under a problem statement, the way a coding test or a real request from a colleague is phrased. The playground is the scratchpad for all of these:

  • SQL coding questions by topic and difficulty. The SQL challenges are sorted into easy, medium and hard, and can be filtered by topic. Each one opens in this editor.
  • Analytical, multi-table problems. The SQL projects give you a business scenario and a series of tasks on its data, such as the TechCart online-store project.
  • Timed practice. The SQL skill test is a timed set of questions with a score at the end.

Practice SQL for Data Analyst Interviews

SQL interviews for analyst roles vary from company to company, but the same skills come up again and again. It is more useful to be fluent in these than to memorise answers to specific questions:

  • Aggregation with GROUP BY and HAVING
  • INNER and LEFT JOINs, including finding rows with no match
  • Subqueries and CTEs for multi-step logic
  • Window functions for ranking, running totals and comparing a row with the previous one
  • Data cleaning: NULL handling and duplicate detection
  • Time-series analysis: grouping by month and measuring change over time

For a longer set organised by topic and level, see the SQL interview questions page. The four queries below are common interview patterns you can run here.

Interview-style queries to try

Second-highest salary in each department

Covers: DENSE_RANK

DENSE_RANK gives tied salaries the same rank and leaves no gaps, so rank 2 is always the second-highest distinct salary.

WITH ranked AS (
  SELECT department, first_name, last_name, salary,
         DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank
  FROM   employees
)
SELECT department, first_name, last_name, salary
FROM   ranked
WHERE  salary_rank = 2
ORDER BY department;
▶ Run this query in the playground

Find duplicates

Covers: GROUP BY, HAVING COUNT(*) > 1

Group by the columns that should be unique together and keep the groups with more than one row. Here: customers who reviewed the same product more than once.

SELECT   user_id, product_id, COUNT(*) AS reviews
FROM     reviews
GROUP BY user_id, product_id
HAVING   COUNT(*) > 1
ORDER BY reviews DESC, user_id;
▶ Run this query in the playground

Count missing values

Covers: IS NULL, COUNT(column)

COUNT(*) counts rows; COUNT(shipped_date) skips the NULLs. The difference is the number of orders with no ship date.

SELECT   status,
         COUNT(*)                       AS orders,
         COUNT(shipped_date)            AS with_ship_date,
         COUNT(*) - COUNT(shipped_date) AS missing_ship_date
FROM     orders
GROUP BY status
ORDER BY orders DESC;
▶ Run this query in the playground

Each employee with their manager

Covers: self join, COALESCE

The employees table joins to itself through manager_id. LEFT JOIN keeps the people who have no manager.

SELECT e.first_name || ' ' || e.last_name                         AS employee,
       e.title,
       COALESCE(m.first_name || ' ' || m.last_name, 'No manager') AS manager
FROM   employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY manager, employee;
▶ Run this query in the playground

Why Practice SQL on SQLab Hub?

SQLab Hub is a free SQL practice platform for students, aspiring data analysts, data engineers, developers, and anyone learning SQL through hands-on practice. The playground is one part of it; the lessons, challenges and projects all use the same kind of data, so what you learn in one carries over to the others.

Interactive SQL playground

A SQL editor with a schema browser, real query execution and a results table. Runs in the browser with nothing to install.

Realistic relational data

8 linked tables covering an online store and a company’s staff, so joins and aggregates have something meaningful to work on.

SQL challenges

A library of problems at easy, medium and hard level, filterable by topic.

Interview practice

Interview questions grouped by topic, plus a timed skill test.

Beginner-to-advanced lessons

Free lessons from SELECT through joins, subqueries, CTEs and window functions.

Free to use

Run queries without an account. Sign in to save queries and track your progress.

How SQLite relates to MySQL, PostgreSQL and SQL Server

SQLite supports many of the core SQL concepts used across relational database systems, including SELECT, JOIN, GROUP BY, HAVING, subqueries, CTEs, and window functions. However, database systems differ in areas such as date functions, string functions, JSON features, procedural SQL, and vendor-specific syntax. The playground executes SQLite only, so use it to practice the concepts and check dialect-specific syntax against the documentation for your own database.

Frequently Asked Questions

How can I practice SQL online for free?

Type a query into the editor at the top of this page and press Ctrl+Enter, or use the Run Query button. It runs on a real SQLite database with 8 tables and 1,476 rows, and the result appears under the editor. There is nothing to install and you do not need an account to run queries.

If you would rather start from a problem than a blank editor, use the practice questions on this page, the sample queries in the bar above the editor, or the SQL challenges page.

Do I need to sign up?

No. Writing and running queries works without an account. A free account adds a few things: saving queries so you can come back to them, XP and a place on the leaderboard, and the timed SQL skill test.

Which SQL dialect does the playground use?

SQLite. It covers the SQL most people need to practice: SELECT, WHERE, JOIN, GROUP BY, HAVING, subqueries, common table expressions (WITH) and window functions such as ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD and NTILE.

Two SQLite habits are worth knowing here. Dates are stored as YYYY-MM-DD text and handled with strftime() and date(), and strings are joined with the || operator.

Can I practice MySQL or PostgreSQL queries here?

The playground runs SQLite only. It does not execute MySQL, PostgreSQL or SQL Server.

SQLite supports many of the core SQL concepts used across relational database systems, including SELECT, JOIN, GROUP BY, HAVING, subqueries, CTEs, and window functions. However, database systems differ in areas such as date functions, string functions, JSON features, procedural SQL, and vendor-specific syntax. Practice the concepts here, then check the syntax of anything dialect-specific in the documentation for the database you will use.

What tables are in the practice database?

There are 8 tables: six describe an online store and two describe the company’s staff.

  • users (150 rows) — Customers: name, email, city, country, signup date, status
  • orders (200 rows) — Orders: customer, total, discount, status, order and ship dates, shipping city
  • order_items (496 rows) — Order lines: order, product, quantity, unit price, subtotal
  • products (120 rows) — Catalogue: name, category, price, cost, stock, SKU, status, rating
  • payments (200 rows) — Payments: order, customer, amount, method, status, paid date
  • reviews (180 rows) — Product reviews: product, customer, 1 to 5 rating, comment, review date
  • employees (120 rows) — Staff: department, title, salary, hire date, manager, city
  • departments (10 rows) — Departments: name, location, headcount, budget

The Schema panel next to the editor lists every column. Click a table to expand it, and click a column to insert it into your query.

Are there limits on the queries I can run?

The database is read-only, so a query has to start with SELECT or WITH. INSERT, UPDATE, DELETE and CREATE are rejected, which also means nobody else can change the data you are practicing on.

A query can run for up to 2 seconds and results are cut off at 500 rows. CROSS JOIN, WITH RECURSIVE and queries with more than five joins are not accepted.

What is the difference between WHERE and HAVING?

WHERE filters individual rows before they are grouped. HAVING filters the groups after GROUP BY has done its work. If the condition uses an aggregate such as COUNT(), SUM() or AVG(), it belongs in HAVING; if it tests a plain column, it belongs in WHERE. One query can use both.

SELECT   city, COUNT(*) AS active_customers
FROM     users
WHERE    status = 'active'      -- filters rows before grouping
GROUP BY city
HAVING   COUNT(*) >= 5          -- filters groups after counting
ORDER BY active_customers DESC;
How should I use the playground to prepare for a SQL interview?

Practice writing queries from a blank editor rather than reading solutions. Work through the topics interviewers return to most: joins, GROUP BY with HAVING, subqueries, CTEs, window functions, NULL handling and finding duplicates. Say out loud what each clause does as you write it, because interviews usually ask you to explain your reasoning.

Every company sets its own questions, so treat any list as practice for the underlying skills and not as a prediction of what you will be asked.

Is the playground suitable for complete beginners?

Yes. The editor opens with a working query that you can run straight away and then change: alter the LIMIT, sort by a different column, or remove the JOIN and see what happens. The beginner questions on this page use one table at a time, and the free lessons explain each clause before you try it.

Does it work on a phone?

Yes. On a small screen the schema, the editor and the output become three tabs, and the Run button sits above the editor.