SQL (Structured Query Language) is a programming language used to manage and query relational databases. It allows users to retrieve, insert, update, and delete data efficiently using commands like SELECT, JOIN, and GROUP BY.
The fastest way to improve at SQL is to actually write queries โ not just read about them. This free SQL playground runs a live SQLite engine directly in your browser, backed by 8 interconnected tables and over 1,370 rows of realistic e-commerce data covering users, orders, products, reviews, coupons, and more.
Unlike static tutorials where you read a query and move on, an interactive SQL editor forces you to think through the problem, type the query, and see the real results. You catch mistakes immediately, try different approaches side by side, and build the muscle memory that comes from repetition. That loop โ write, run, observe, adjust โ is the same one professional data analysts and engineers use every day.
No installation. No account required to start. No cloud credits. Open the editor, write SQL, and press Ctrl+Enter to run.
The best way to lock in SQL concepts is to solve real problems on real data. Below are eight progressively harder SQL practice questions โ each with a working SQL answer you can copy directly into the playground above and run immediately.
A foundational WHERE filter. Almost every real-world report starts with filtering a table by one or more column values.
SELECT first_name, last_name, email, city FROM users WHERE city = 'New York' ORDER BY last_name;
Introduces GROUP BY and COUNT() โ two of the most frequently tested SQL concepts in analyst interviews.
SELECT u.first_name || ' ' || u.last_name AS customer,
COUNT(o.id) AS total_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id
ORDER BY total_orders DESC;Aggregation across multiple tables using JOIN and SUM(). Common in product analytics and e-commerce reporting.
SELECT p.name,
SUM(oi.quantity) AS units_sold,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id
ORDER BY units_sold DESC
LIMIT 5;Date functions and time-series aggregation โ essential for any data analyst role.
SELECT strftime('%Y-%m', created_at) AS month,
COUNT(*) AS orders,
ROUND(AVG(total), 2) AS avg_order_value
FROM orders
GROUP BY month
ORDER BY month;The classic LEFT JOIN โฆ WHERE NULL anti-join pattern. This appears in virtually every SQL coding test.
SELECT 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;
Subquery inside HAVING โ tests whether you understand the difference between filtering rows and filtering groups.
SELECT u.first_name || ' ' || u.last_name AS customer,
ROUND(SUM(o.total), 2) AS lifetime_value
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id
HAVING SUM(o.total) > (SELECT AVG(total) FROM orders)
ORDER BY lifetime_value DESC;SELECT category,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue,
CASE
WHEN SUM(oi.quantity * oi.unit_price) > 50000 THEN 'High'
WHEN SUM(oi.quantity * oi.unit_price) > 20000 THEN 'Medium'
ELSE 'Low'
END AS tier
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY category
ORDER BY revenue DESC;Window functions with OVER and frame specification โ the advanced skill that separates junior from senior SQL practitioners.
SELECT u.first_name || ' ' || u.last_name AS customer,
o.created_at,
COUNT(*) OVER (
PARTITION BY o.user_id
ORDER BY o.created_at
ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
) AS orders_last_30_days
FROM orders o
JOIN users u ON u.id = o.user_id
ORDER BY customer, o.created_at;If you are just starting out, focus your first week on the five core clauses:SELECT, FROM, WHERE, ORDER BY, and LIMIT. Every advanced query is built on top of these.
-- Return every column SELECT * FROM products; -- Return only specific columns SELECT name, price, category FROM products; -- Create a computed column with AS SELECT name, price * 1.1 AS price_with_tax FROM products;
-- Products under $50 SELECT name, price FROM products WHERE price < 50; -- Multiple conditions with AND SELECT name, price, category FROM products WHERE price < 50 AND category = 'Electronics';
-- 10 most recent orders SELECT id, user_id, total, created_at FROM orders ORDER BY created_at DESC LIMIT 10;
-- How many users are registered in each city? SELECT city, COUNT(*) AS user_count FROM users GROUP BY city ORDER BY user_count DESC;
Once these feel comfortable, move on to SQL JOINs โ the most important concept in relational SQL. A JOIN combines rows from two tables using a shared key, almost always a foreign-key ID column.
-- Basic INNER JOIN: orders with their customer's name SELECT o.id, u.first_name || ' ' || u.last_name AS customer, o.total FROM orders o JOIN users u ON u.id = o.user_id ORDER BY o.created_at DESC LIMIT 20;
Once basic queries are second nature, the next level covers multi-table joins, correlated subqueries, window functions, and CTEs. These are the topics that separate a competent SQL user from a strong one โ and the topics that come up repeatedly in senior analyst and engineer interviews.
Joining three or more tables is routine in production analytics. The key is to follow the foreign-key chain and give every table a short, clear alias.
-- Order line details with customer name, product name, and subtotal
SELECT u.first_name || ' ' || u.last_name AS customer,
p.name AS product,
oi.quantity,
oi.unit_price,
ROUND(oi.quantity * oi.unit_price, 2) AS line_total
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
ORDER BY o.id, line_total DESC;A correlated subquery references a column from the outer query, so it re-evaluates once per outer row. Use these to compare each row against a group-level aggregate.
-- Products priced above their own category average SELECT name, category, price FROM products p WHERE price > ( SELECT AVG(price) FROM products WHERE category = p.category ) ORDER BY category, price DESC;
Window functions compute values across a partition of rows without collapsing them. They are essential for rankings, running totals, and period-over-period comparisons.
-- Rank products by revenue within each category
SELECT name,
category,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue,
RANK() OVER (
PARTITION BY category
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS rank_in_category
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id
ORDER BY category, rank_in_category;CTEs โ written with the WITH keyword โ let you break a complex query into named, readable steps. They are the professional standard for multi-stage analytical SQL.
WITH customer_totals AS (
SELECT user_id,
COUNT(*) AS order_count,
SUM(total) AS lifetime_value
FROM orders
GROUP BY user_id
),
ranked AS (
SELECT user_id, order_count, lifetime_value,
NTILE(4) OVER (ORDER BY lifetime_value DESC) AS quartile
FROM customer_totals
)
SELECT u.first_name || ' ' || u.last_name AS customer,
r.order_count,
ROUND(r.lifetime_value, 2) AS ltv,
r.quartile
FROM ranked r
JOIN users u ON u.id = r.user_id
ORDER BY r.quartile, r.lifetime_value DESC;-- Cities where average customer lifetime value exceeds $500
SELECT u.city,
COUNT(DISTINCT u.id) AS customers,
ROUND(AVG(totals.ltv), 2) AS avg_ltv
FROM users u
JOIN (
SELECT user_id, SUM(total) AS ltv
FROM orders
GROUP BY user_id
) totals ON totals.user_id = u.id
GROUP BY u.city
HAVING AVG(totals.ltv) > 500
ORDER BY avg_ltv DESC;-- Users who have left at least one product review (using EXISTS) SELECT first_name, last_name, email FROM users u WHERE EXISTS ( SELECT 1 FROM reviews r WHERE r.user_id = u.id ) ORDER BY last_name; -- Same result using IN (simpler, fine for small result sets) SELECT first_name, last_name, email FROM users WHERE id IN (SELECT DISTINCT user_id FROM reviews) ORDER BY last_name;
SQL interviews at data analyst, data engineer, and backend developer levels follow a predictable pattern. Here are the problem types you will see most often โ each with a ready-to-run practice query.
A classic screener. Interviewers want to see whether you knowLIMIT/OFFSET, a subquery approach, orDENSE_RANK().
-- 3rd highest order value using DENSE_RANK (handles ties correctly)
WITH ranked_orders AS (
SELECT id, total,
DENSE_RANK() OVER (ORDER BY total DESC) AS rnk
FROM orders
)
SELECT * FROM ranked_orders WHERE rnk = 3;-- Emails registered more than once (data quality check) SELECT email, COUNT(*) AS occurrences FROM users GROUP BY email HAVING COUNT(*) > 1;
-- Monthly revenue with delta vs previous month using LAG()
WITH monthly AS (
SELECT strftime('%Y-%m', created_at) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
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;-- INNER JOIN: only rows that match on both sides SELECT u.email, o.id AS order_id, o.total FROM users u JOIN orders o ON o.user_id = u.id; -- same as INNER JOIN -- LEFT JOIN: all users, even those with no orders SELECT u.email, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id;
-- Order count by status shown as separate columns SELECT SUM(CASE WHEN status = 'Pending' THEN 1 ELSE 0 END) AS pending, SUM(CASE WHEN status = 'Shipped' THEN 1 ELSE 0 END) AS shipped, SUM(CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END) AS delivered, SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled FROM orders;
-- First-order month for every customer (acquisition cohort)
SELECT u.first_name || ' ' || u.last_name AS customer,
strftime('%Y-%m', MIN(o.created_at)) AS first_order_month
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id
ORDER BY first_order_month;There are dozens of SQL practice platforms available online. Here is what makes this one worth bookmarking for your daily practice and interview preparation.
No account, no download, no waiting. Open the page and run your first query in under 10 seconds.
8 tables modelled on a real e-commerce business โ users, orders, products, reviews, coupons, and addresses.
Structured problems at beginner, intermediate, and advanced levels with automatic correctness checking.
Sign in to name and save queries permanently. Build a personal SQL snippet collection as you learn.
Tabbed layout on phones lets you switch between schema browser, editor, and output โ no desktop required.
See every table, column, and type in the left panel instantly โ no documentation to dig through.
| Feature | SQLab | Generic online IDE | Local database setup |
|---|---|---|---|
| No installation needed | โ | Often requires signup | โ |
| Pre-loaded realistic dataset | โ 8 tables | Usually empty | Manual setup |
| Guided challenges | โ | Rarely included | โ |
| Save queries free | โ | Paid tier | โ |
| Mobile-friendly layout | โ | Often broken on mobile | โ |
| Instant query execution | โ | Often slow to spin up | โ |
Open this page and start typing in the editor above โ no sign-up or payment needed. The playground runs a real SQLite engine against a pre-loaded database with over 1,370 rows across 8 tables. Press Ctrl+Enter to run any query and see results instantly. For structured practice, use the Challenges bar to pick a problem at your level and receive automatic feedback on your solution.
The engine is SQLite, which supports the full standard SQL feature set: SELECT, JOIN, GROUP BY, HAVING, subqueries, CTEs (WITH), and window functions including ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, and NTILE. The main differences from MySQL or PostgreSQL are date functions (strftime instead ofDATE_FORMAT) and the absence of stored procedures. For interview prep and learning core SQL, the syntax transfers directly to every major database system.
Make sure you can write the following from memory without hints:
Use the Challenges section here to practice under light time pressure. Aim to solve easy problems in under 2 minutes and medium problems in under 5.
WHERE filters individual rows before any grouping takes place. HAVING filters groups after GROUP BY has aggregated the data. A practical rule: if your filter references an aggregate function like COUNT(), SUM(), or AVG(), you need HAVING. If you are filtering on a plain column value, use WHERE.
-- WHERE: filter rows before grouping SELECT city, COUNT(*) FROM users WHERE status = 'active' GROUP BY city; -- HAVING: filter groups after aggregation SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 10;
The engine is SQLite, but roughly 95% of what you write here works identically in MySQL, PostgreSQL, and SQL Server. The main differences to be aware of: date and time functions vary slightly between engines, and some PostgreSQL-specific features (like array types or JSON operators) are not available. For most interview scenarios and day-to-day analytics work, practising on SQLite gives you everything you need to hit the ground running in any environment.
Data analyst SQL interviews typically test three problem types:
Work through all the beginner and intermediate challenges first. Then tackle advanced problems and time yourself. Companies at the analyst level routinely ask window function and CTE questions, so make sure those feel comfortable before your interview.
The database contains 8 tables modelled on a realistic e-commerce platform:
Click any table name in the Schema panel on the left to insert a sample SELECT query into the editor.
Yes. The default query that loads when you open the editor is a real-world example combining JOIN, GROUP BY, and ORDER BY โ you can study it, run it, and modify it immediately without writing anything from scratch. The Schema panel shows every table and column so you always know what data is available. A good beginner exercise: run the default query, then try changing the LIMIT value, removing the WHERE clause, or swapping ORDER BY to a different column. Experimenting with a working query is one of the fastest paths into SQL for absolute beginners.