CareerKit

SQL Interview Questions and Answers

SQL questions for developer, analyst and support roles: SELECT basics, joins, GROUP BY, subqueries, CTEs, window functions, indexes and normalisation. Every query result shown was produced by running the query.

12 free answers from our SQL Interview Questions ebook, which has all 80 questions.

SELECT, WHERE and ORDER BY

What does a basic SELECT query do?

SELECT reads rows from a table. The basic shape is:

  • SELECT which columns you want
  • FROM which table
  • WHERE which rows (a filter)
  • ORDER BY in what order
Query
SELECT name, city
FROM employees
WHERE city = 'Bengaluru'
ORDER BY name;
Result
namecity
Arjun MehtaBengaluru
Priya SharmaBengaluru
Rohan GuptaBengaluru

Avoid SELECT * in application code: it fetches columns you don't need, breaks when columns are added or reordered, and prevents some index optimisations. It's fine for quick exploration.

Text values use single quotes ('Bengaluru'). Double quotes are for identifiers like column names in standard SQL and PostgreSQL.

Say it in the interview

SELECT picks the columns, FROM the table, WHERE filters the rows and ORDER BY sorts the result. In application code I list the columns I need instead of SELECT star, and I use single quotes for text values.

In what order does the database actually run the clauses of a query?

We write SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT, but the database logically processes them in this order:

  1. FROM (and JOINs): get the rows
  2. WHERE: filter rows
  3. GROUP BY: form groups
  4. HAVING: filter groups
  5. SELECT: compute the output columns and aliases
  6. DISTINCT: remove duplicates
  7. ORDER BY: sort
  8. LIMIT / OFFSET: cut the result

This explains common errors. A column alias defined in SELECT can be used in ORDER BY (which runs later), but not in WHERE in most databases (which runs earlier):

Query
SELECT name, salary / 100000 AS lakhs
FROM employees
WHERE salary >= 1500000
ORDER BY lakhs DESC, name;
Result
namelakhs
Priya Sharma24
Sneha Reddy18
Meera Nair16
Arjun Mehta15

Writing WHERE lakhs >= 15 fails in PostgreSQL, MySQL and SQL Server, because the alias doesn't exist yet when WHERE runs.

Say it in the interview

Logically the database runs FROM and joins first, then WHERE, GROUP BY, HAVING, then SELECT, DISTINCT, ORDER BY and finally LIMIT. That's why a SELECT alias works in ORDER BY but not in WHERE, and why aggregate conditions go in HAVING.

Joins explained with diagrams

What is a JOIN, and what types are there?

A join combines rows from two tables using a related column, usually a foreign key like employees.department_id → departments.id.

AB INNER JOIN only matches AB LEFT JOIN all of A + matches AB RIGHT JOIN all of B + matches AB FULL OUTER JOIN everything
  • INNER JOIN: only rows that have a match in both tables
  • LEFT JOIN: all rows from the left table, plus matches (NULLs where there's no match)
  • RIGHT JOIN: all rows from the right table, plus matches
  • FULL OUTER JOIN: all rows from both, matched where possible
  • CROSS JOIN: every combination of rows
  • SELF JOIN: a table joined to itself

With A = employees and B = departments, here's how many rows each join gives:

Query
SELECT
  (SELECT COUNT(*) FROM employees e INNER JOIN departments d ON d.id = e.department_id) AS inner_rows,
  (SELECT COUNT(*) FROM employees e LEFT JOIN departments d ON d.id = e.department_id) AS left_rows,
  (SELECT COUNT(*) FROM employees e RIGHT JOIN departments d ON d.id = e.department_id) AS right_rows,
  (SELECT COUNT(*) FROM employees e FULL OUTER JOIN departments d ON d.id = e.department_id) AS full_rows;
Result
inner_rowsleft_rowsright_rowsfull_rows
9101011

Inner gives 9 (Aditya has no department). Left adds Aditya (10). Right adds Marketing, which has no employees (10). Full adds both (11).

Say it in the interview

A join combines rows from two tables on a related column. Inner keeps only matching rows, left keeps all rows from the left table with NULLs where nothing matches, right does the same for the right table, full outer keeps everything from both, cross join gives every combination, and a self join joins a table to itself.

How does an INNER JOIN work?

An INNER JOIN returns one row for every pair of rows where the ON condition is true. Rows without a match on either side are dropped.

Query
SELECT e.name, d.name AS department
FROM employees e
INNER JOIN departments d ON d.id = e.department_id
ORDER BY d.name, e.name;
Result
namedepartment
Arjun MehtaEngineering
Kavya IyerEngineering
Priya SharmaEngineering
Rohan GuptaEngineering
Meera NairFinance
Rahul VermaHR
Ananya DasSales
Sneha ReddySales
Vikram SinghSales

Aditya Joshi (no department) and Marketing (no employees) don't appear. JOIN on its own means INNER JOIN.

Table aliases (e, d) keep queries short, and prefixing columns (e.name, d.name) is required when both tables have a column with the same name; otherwise you get an "ambiguous column" error.

Say it in the interview

An inner join returns a row for each pair of rows that satisfy the ON condition, so unmatched rows on either side disappear, like the employee without a department here. I use short table aliases and prefix columns to avoid ambiguity.

GROUP BY and HAVING

What are aggregate functions?

Aggregate functions take many rows and return one value:

  • COUNT(): number of rows or non-NULL values
  • SUM(): total
  • AVG(): average
  • MIN() / MAX(): smallest and largest
Query
SELECT COUNT(*) AS employees,
       COUNT(department_id) AS with_department,
       SUM(salary) AS total,
       MIN(salary) AS lowest,
       MAX(salary) AS highest,
       AVG(salary) AS average
FROM employees;
Result
employeeswith_departmenttotallowesthighestaverage
1091300000060000024000001300000

The company spends ₹1.3 crore a year on salaries, an average of ₹13 lakh per employee.

All aggregates except COUNT(*) ignore NULLs: COUNT(department_id) is 9 because Aditya's department is NULL. Without GROUP BY, the whole table is one group, so you get one row.

Say it in the interview

Aggregate functions like COUNT, SUM, AVG, MIN and MAX reduce many rows to one value per group, or one value for the whole table without GROUP BY. All of them except COUNT star ignore NULLs.

How does GROUP BY work?

GROUP BY splits rows into groups that share the same value(s), then aggregate functions run once per group.

Headcount and salary cost per department:

Query
SELECT department_id, COUNT(*) AS headcount, SUM(salary) AS total_salary
FROM employees
GROUP BY department_id
ORDER BY department_id;
Result
department_idheadcounttotal_salary
NULL1600000
146100000
233600000
311100000
411600000

All the NULLs form one group of their own (Aditya, without a department).

The rule: every column in SELECT must either be in GROUP BY or inside an aggregate function. SELECT department_id, name, COUNT(*) ... GROUP BY department_id is an error in PostgreSQL and SQL Server (and in MySQL with the default ONLY_FULL_GROUP_BY mode), because there are several names per department and the database can't know which one to show.

Say it in the interview

GROUP BY collects rows with the same values into groups and the aggregates are calculated once per group. Every selected column must be grouped or aggregated, and NULLs form a single group of their own.

Subqueries and CTEs

How do you use a subquery in a WHERE clause?

A subquery is a query inside another query. In WHERE, it often supplies a value to compare against.

Employees earning more than the company average:

Query
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
Result
namesalary
Priya Sharma2400000
Sneha Reddy1800000
Meera Nair1600000
Arjun Mehta1500000
Kavya Iyer1350000

The subquery runs once and returns ₹13,00,000. A subquery that returns one value is called a scalar subquery; if it returned several rows, you'd need IN, ANY or EXISTS instead of >.

You can't write WHERE salary > AVG(salary) directly, because aggregates aren't allowed in WHERE.

Say it in the interview

A subquery in WHERE computes a value or list to compare against, like the average salary here. A scalar subquery must return a single value; for multiple rows I use IN or EXISTS. It's needed because aggregates can't appear directly in WHERE.

What is a correlated subquery?

A correlated subquery refers to a column from the outer query, so it's evaluated for each outer row.

Employees who earn more than the average of their own department:

Query
SELECT e.name, e.department_id, 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_id;
Result
namedepartment_idsalary
Priya Sharma12400000
Sneha Reddy21800000

Engineering's average is ₹15.25 lakh and Sales' is ₹12 lakh. Rahul and Meera are alone in their departments, so they equal their own average, not exceed it.

Correlated subqueries can be slow on big tables if the database really runs them once per row, though optimisers often rewrite them as joins. The same result can be written with a window function: AVG(salary) OVER (PARTITION BY department_id).

Say it in the interview

A correlated subquery references the outer query's row, so it's logically evaluated per row, like comparing each employee to their own department's average. Optimisers often turn them into joins, but on large data I'd consider a join with a grouped subquery or a window function instead.

Indexes and normalisation

What is an index, and how does it make queries faster?

An index is a separate data structure that lets the database find rows without scanning the whole table, like the index at the back of a book.

Most indexes are B-trees: balanced trees that keep values sorted. Finding one value in a million rows takes about 3 or 4 page reads instead of reading every row. Because the values are sorted, B-trees also speed up range queries (BETWEEN, >, <), ORDER BY and MIN/MAX.

CREATE INDEX idx_orders_customer ON orders (customer_id);

-- Now this doesn't need to scan every order:
SELECT * FROM orders WHERE customer_id = 3;

Other index types: hash indexes (equality only), GIN in PostgreSQL (for JSON, arrays and full-text search), and full-text indexes for word search.

Check whether your index is used with EXPLAIN: a "Seq Scan" (PostgreSQL) or "type: ALL" (MySQL) on a big table means a full table scan.

Say it in the interview

An index is a separate sorted structure, usually a B-tree, that lets the database jump to matching rows in a few page reads instead of scanning the table. B-trees help equality, ranges, sorting and min/max. I verify that queries actually use the index with EXPLAIN.

What is the difference between a clustered and a non-clustered index?

  • A clustered index determines the physical order of the rows: the table data itself is stored in the index's order. A table can have only one. In SQL Server and MySQL (InnoDB), the primary key is the clustered index by default.
  • A non-clustered (secondary) index is a separate structure holding the indexed values plus a pointer to the row (in InnoDB, the primary key value). A table can have many.
-- SQL Server syntax
CREATE CLUSTERED INDEX ix_orders_date ON orders (order_date);
CREATE NONCLUSTERED INDEX ix_orders_customer ON orders (customer_id);

A lookup through a non-clustered index needs an extra step to fetch the row, unless the index contains every column the query needs. That's called a covering index (SQL Server and PostgreSQL support INCLUDE (amount) for this).

PostgreSQL doesn't keep tables permanently clustered; its tables are "heaps", and every index is secondary.

Say it in the interview

A clustered index stores the table rows themselves in index order, so there's only one per table, usually the primary key in SQL Server and MySQL. Non-clustered indexes are separate structures pointing back to the rows, so there can be many, and a covering index that includes all needed columns avoids the extra lookup.

20 practice queries with answers

List the employees hired in 2023, newest first.

Query
SELECT name, hire_date
FROM employees
WHERE hire_date >= '2023-01-01'
  AND hire_date < '2024-01-01'
ORDER BY hire_date DESC;
Result
namehire_date
Rohan Gupta2023-08-01
Ananya Das2023-02-13

A half-open date range (>= the start, < the next year) works for both dates and timestamps, and can use an index on hire_date, unlike YEAR(hire_date) = 2023.

Say it in the interview

I filter with a half-open range from the first of January 2023 to before the first of January 2024, which works for timestamps too and can use an index, then sort by hire date descending.

Show customers who signed up in February 2024 with their number of orders.

Query
SELECT c.name, c.signup_date, COUNT(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.signup_date >= '2024-02-01'
  AND c.signup_date < '2024-03-01'
GROUP BY c.id, c.name, c.signup_date
ORDER BY c.signup_date;
Result
namesignup_dateorders
Karthik Rao2024-02-023
Neha Agarwal2024-02-201

The LEFT JOIN and COUNT(o.id) mean a customer with no orders would still appear, with 0.

Say it in the interview

I filter customers by the signup date range, left join their orders so customers without orders are kept, group by customer and count the order ids.

All 80 questions in the ebook

Linked questions are answered free on this page; the rest are in the ebook.

SELECT, WHERE and ORDER BY

  1. What does a basic SELECT query do?
  2. In what order does the database actually run the clauses of a query?
  3. How do AND and OR work together in a WHERE clause?
  4. How do you filter a range with BETWEEN?
  5. How does the IN operator work?
  6. How do you search for patterns with LIKE?
  7. How do you find rows where a column is NULL?
  8. How does NULL behave in calculations, and what does COALESCE do?
  9. How do you sort by more than one column?
  10. How do you get the top N rows?
  11. What does DISTINCT do?
  12. How do you write conditional logic with CASE?
  13. How do you calculate new columns in a query?
  14. What are some useful string functions?

Joins explained with diagrams

  1. What is a JOIN, and what types are there?
  2. How does an INNER JOIN work?
  3. How does a LEFT JOIN work?
  4. How do you find rows that have no match in another table?
  5. What is the difference between RIGHT JOIN and LEFT JOIN?
  6. What is a FULL OUTER JOIN?
  7. What is a self join?
  8. What is the difference between putting a condition in ON and in WHERE?
  9. How do you join more than two tables?
  10. What is a CROSS JOIN, and when is it useful?
  11. Why does a join sometimes return more rows than expected?
  12. What is the difference between UNION and UNION ALL?
  13. What do INTERSECT and EXCEPT do?
  14. How does the database execute a join internally?

GROUP BY and HAVING

  1. What are aggregate functions?
  2. How does GROUP BY work?
  3. How do you include groups that have no rows, like a department with no employees?
  4. What is the difference between WHERE and HAVING?
  5. How do you count values that appear more than once?
  6. What is the difference between COUNT(*), COUNT(column) and COUNT(DISTINCT column)?
  7. How do you turn rows into columns (conditional aggregation)?
  8. How do you group by month?
  9. How do NULLs affect AVG and other aggregates?
  10. How do you find which values are repeated, like employees with the same salary?
  11. How do you add a grand total row to a grouped report?
  12. How do you calculate each group's percentage of the total?

Subqueries and CTEs

  1. How do you use a subquery in a WHERE clause?
  2. What is a correlated subquery?
  3. How do you find the second highest salary?
  4. How do you find the Nth highest salary, handling ties?
  5. What is the difference between IN and EXISTS?
  6. Why can NOT IN return no rows at all?
  7. What is a CTE, and why use one?
  8. What is a recursive CTE?
  9. What are window functions, and how do ROW_NUMBER, RANK and DENSE_RANK differ?
  10. What does PARTITION BY do in a window function?
  11. How do you calculate a running total?
  12. How do you compare a row with the previous row?

Indexes and normalisation

  1. What is an index, and how does it make queries faster?
  2. What is the difference between a clustered and a non-clustered index?
  3. How does a composite index work, and when won't an index be used?
  4. What are the costs of indexes, and which columns should you index?
  5. What are primary keys, foreign keys and other constraints?
  6. What is normalisation? Explain 1NF, 2NF and 3NF.
  7. What is denormalisation, and when is it used?
  8. What is a transaction, and what does ACID mean?

20 practice queries with answers

  1. List the employees hired in 2023, newest first.
  2. Show customers who signed up in February 2024 with their number of orders.
  3. Find the number of delivered orders, total revenue and average order value.
  4. Find the customer who has spent the most on delivered orders.
  5. Which department has the highest total salary cost?
  6. Rohan gets a raise to ₹16 lakh. Who now earns more than their manager?
  7. Find customers whose orders have all been delivered.
  8. Show each customer's first order.
  9. Find customers who ordered in more than one month.
  10. Find the second highest salary in each department.
  11. A bug inserted duplicate customers. Delete the duplicates, keeping the oldest record of each.
  12. Show the average delivered order value by customer city.
  13. Rank customers by total spend, excluding cancelled orders.
  14. Show new customer signups per month and the running total of customers.
  15. List pending orders placed before 1 March 2024, with the customer's name and city.
  16. Compare each Sales employee's salary with their department average.
  17. Find departments whose average salary is above the company average.
  18. For each of Karthik's orders, show the date of the next one.
  19. Find the most common order amount (the mode), including ties.
  20. Build a department summary: headcount and top earner, including empty departments.