SQL Practice Test
A self-check practice test covering SELECT, filtering, joins, aggregation, grouping, subqueries, set operations, and window functions
How to Use This Practice Test
This practice test checks your understanding of standard SQL across the most commonly tested topics: the SELECT statement and logical clause order, filtering with WHERE, the different JOIN types, NULL handling, aggregate functions, GROUP BY and HAVING, subqueries, set operations (UNION, INTERSECT, EXCEPT), and the basics of window functions. Work through Section A (multiple choice) and Section B (write-the-query) using only the reference schema below, write down your answers, and then self-check against the Answer Key. Every question is numbered so you can match each answer to its question. The example queries follow ANSI/standard SQL and run on most major databases (PostgreSQL, SQL Server, modern MySQL/MariaDB, SQLite, Oracle).
Reference Schema
All questions below reference these three related tables
customers
Columns: customer_id (INTEGER, primary key), name (VARCHAR), country (VARCHAR), signup_date (DATE).
products
Columns: product_id (INTEGER, primary key), product_name (VARCHAR), category (VARCHAR), unit_price (DECIMAL).
orders
Columns: order_id (INTEGER, primary key), customer_id (INTEGER, references customers), product_id (INTEGER, references products), quantity (INTEGER), order_date (DATE). The line total is quantity * unit_price after joining to products.
Section A: Multiple Choice
Choose the single best answer for each of the 10 questions
A1. Logical clause order
In which order are the clauses of a single SELECT query logically evaluated?
- A. SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
- B. FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
- C. FROM → SELECT → WHERE → GROUP BY → ORDER BY → HAVING
- D. WHERE → FROM → GROUP BY → SELECT → HAVING → ORDER BY
A2. JOIN types
A query joins customers to orders and you need every customer in the result, including those who have never placed an order. Which join do you use (with customers on the left)?
- A. INNER JOIN
- B. LEFT OUTER JOIN
- C. RIGHT OUTER JOIN
- D. CROSS JOIN
A3. NULL comparison
Which predicate correctly returns rows where country has no value?
- A. WHERE country = NULL
- B. WHERE country != NULL
- C. WHERE country IS NULL
- D. WHERE country = ''
A4. COUNT and NULLs
For a column category that contains some NULL values, how do COUNT(*) and COUNT(category) differ?
- A. They are always equal
- B. COUNT(*) counts all rows; COUNT(category) counts only non-NULL category values
- C. COUNT(category) counts all rows; COUNT(*) skips NULLs
- D. Both ignore NULLs and duplicates
A5. WHERE vs HAVING
Which statement about WHERE and HAVING is correct?
- A. WHERE filters individual rows before grouping; HAVING filters groups after aggregation
- B. HAVING runs before WHERE
- C. You can use aggregate functions in WHERE
- D. HAVING can only be used without GROUP BY
A6. GROUP BY rule
In standard SQL, a non-aggregated column in the SELECT list of a grouped query must also appear in:
- A. The ORDER BY clause
- B. The GROUP BY clause
- C. The HAVING clause
- D. A subquery
A7. SUM over no rows
If SUM(quantity) is applied to a result set that contains zero matching rows (no GROUP BY), what does it return?
- A. 0
- B. An error
- C. NULL
- D. An empty string
A8. DISTINCT vs GROUP BY
Which pair of queries returns the same set of distinct country values?
- A. SELECT DISTINCT country FROM customers and SELECT country FROM customers GROUP BY country
- B. SELECT country FROM customers and SELECT DISTINCT country FROM customers
- C. SELECT COUNT(country) FROM customers and SELECT DISTINCT country FROM customers
- D. None of these are equivalent
A9. UNION vs UNION ALL
What is the difference between UNION and UNION ALL?
- A. UNION keeps duplicate rows; UNION ALL removes them
- B. UNION removes duplicate rows; UNION ALL keeps all rows including duplicates
- C. They are identical
- D. UNION ALL sorts the result; UNION does not
A10. Indexes
Which statement about a B-tree index on orders(customer_id) is most accurate?
- A. It guarantees queries run instantly regardless of the predicate
- B. It can speed up equality and range lookups on customer_id but adds overhead to writes
- C. It removes the need for a WHERE clause
- D. It only helps with ORDER BY, never with filtering
Section B: Write the Query
Write a single standard-SQL query for each of the 8 tasks, using the reference schema. Difficulty increases as you go.
B1. Basic projection and ordering
List the name and country of every customer, sorted alphabetically by name.
B2. Filtering
Return all products in the 'Electronics' category with a unit_price greater than 100, showing product name and price, most expensive first.
B3. Inner join
For every order, show the customer name, the product name, and the quantity. Only include orders that have a matching customer and product.
B4. Left join to find missing rows
List the names of customers who have never placed an order.
B5. Aggregation with GROUP BY
For each product category, compute the total quantity ordered. Show the category and the total, ordered by total descending.
B6. GROUP BY + HAVING
Find customers who have placed more than 5 orders. Show the customer name and their order count.
B7. Subquery
List the products whose unit_price is above the average unit price across all products.
B8. Window function
For each order, show the order_id, customer_id, order_date, and a per-customer sequence number that ranks the customer's orders from earliest to latest order_date.
Answer Key & Explanations
Correct answers for Section A and example queries for Section B
Section A — Multiple Choice Answers
- A1 → B. SQL is logically evaluated FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY, which is why SELECT aliases generally cannot be used in WHERE.
- A2 → B. A LEFT OUTER JOIN keeps every row from the left table (customers) and fills NULLs where no matching order exists.
- A3 → C. NULL is not equal to anything, including NULL, so you must use
IS NULL;= NULLnever returns true. - A4 → B.
COUNT(*)counts rows, whileCOUNT(category)ignores rows where category is NULL. - A5 → A. WHERE filters rows before grouping/aggregation; HAVING filters the resulting groups and may reference aggregates.
- A6 → B. Any non-aggregated SELECT column in a grouped query must appear in the GROUP BY clause in standard SQL.
- A7 → C. Aggregates other than COUNT return NULL over an empty set;
SUMof no rows is NULL (useCOALESCEto get 0). - A8 → A.
SELECT DISTINCT countryand grouping by country both collapse to one row per distinct value. - A9 → B. UNION removes duplicate rows; UNION ALL keeps all rows and is faster because it skips the de-duplication step.
- A10 → B. An index speeds up equality/range lookups and joins on the indexed column but slows down inserts/updates/deletes that must maintain it.
Basic projection and ordering
SELECT name, country
FROM customers
ORDER BY name;
SELECT chooses the two columns and ORDER BY sorts ascending by name (ASC is the default).
Filtering
SELECT product_name, unit_price
FROM products
WHERE category = 'Electronics'
AND unit_price > 100
ORDER BY unit_price DESC;
Two conditions are combined with AND in the WHERE clause; DESC sorts the most expensive products first.
Inner join
SELECT c.name, p.product_name, o.quantity
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
JOIN products AS p ON p.product_id = o.product_id
ORDER BY o.order_id;
Two INNER JOINs link orders to customers and products; only orders with matches on both keys appear.
Left join to find missing rows
SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
The LEFT JOIN keeps all customers; the IS NULL test on the order key isolates customers with no matching order. (NOT IN / NOT EXISTS are valid alternatives.)
Aggregation with GROUP BY
SELECT p.category, SUM(o.quantity) AS total_quantity
FROM orders AS o
JOIN products AS p ON p.product_id = o.product_id
GROUP BY p.category
ORDER BY total_quantity DESC;
Rows are grouped by category and SUM aggregates the quantity within each group; the alias is used in ORDER BY, which runs after SELECT.
GROUP BY + HAVING
SELECT c.name, COUNT(*) AS order_count
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name
HAVING COUNT(*) > 5
ORDER BY order_count DESC;
HAVING filters the groups by an aggregate (the count); WHERE cannot do this because aggregation has not happened yet. Grouping by customer_id keeps the count correct even if two customers share a name.
Subquery
SELECT product_name, unit_price
FROM products
WHERE unit_price > (SELECT AVG(unit_price) FROM products)
ORDER BY unit_price DESC;
The scalar subquery computes the overall average once, and the outer query keeps only products priced above it.
Window function
SELECT order_id,
customer_id,
order_date,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS order_seq
FROM orders;
ROW_NUMBER assigns 1, 2, 3, ... within each customer (PARTITION BY) ordered by order_date, without collapsing rows the way GROUP BY would. Use RANK() or DENSE_RANK() if ties on the same date should share a number.
Related Resources
Continue practicing and deepen your data skills
Python Practice Test
Test your Python fundamentals with multiple-choice questions and coding tasks, complete with a worked answer key.
Start TestMachine Learning Practice Test
Check your understanding of core machine learning concepts and workflows with a self-graded practice test.
Start TestData Science with Python Cheat Sheet
A quick reference for pandas, NumPy, and common data-wrangling patterns that pair well with SQL.
View Cheat SheetData Science Course
Build end-to-end data skills, from SQL and Python to analysis and modeling, in our structured course.
Explore Course