Skip to main content

SQL Query Challenges

Introduction

SQL (Structured Query Language) is a fundamental skill for many technical roles, especially those involving data analysis, backend development, or database management. During technical interviews, candidates are often tested on their ability to write efficient SQL queries to solve various data problems.

This guide will walk you through common SQL query challenges you might encounter in interviews, from basic select statements to complex joins and aggregate functions. Each section will build upon the previous one, helping you develop a comprehensive understanding of SQL query patterns.

Basic SQL Query Structure

Let's start with the fundamental structure of an SQL query:

sql
SELECT column1, column2, ...
FROM table_name
WHERE condition
GROUP BY column1, column2, ...
HAVING condition
ORDER BY column1, column2, ... [ASC|DESC]
LIMIT number;

Each part serves a specific purpose:

  • SELECT: Specifies which columns to retrieve
  • FROM: Indicates which table(s) to query
  • WHERE: Filters rows based on conditions
  • GROUP BY: Groups rows with the same values
  • HAVING: Filters groups based on conditions
  • ORDER BY: Sorts the result set
  • LIMIT: Restricts the number of rows returned

Challenge 1: Basic Filtering and Sorting

Problem: Retrieve the names and salaries of employees who earn more than $70,000, sorted by salary in descending order.

Table Structure:

sql
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(100),
salary DECIMAL(10, 2),
hire_date DATE
);

Solution:

sql
SELECT name, salary
FROM employees
WHERE salary > 70000
ORDER BY salary DESC;

Output Example:

name          | salary
--------------+----------
Sarah Johnson | 95000.00
Michael Smith | 85000.00
Robert Brown | 78000.00
Jennifer Lee | 72500.00

Challenge 2: Aggregation Functions

Problem: Find the average salary by department, but only include departments with more than 5 employees.

Solution:

sql
SELECT 
department,
ROUND(AVG(salary), 2) AS average_salary,
COUNT(*) AS employee_count
FROM
employees
GROUP BY
department
HAVING
COUNT(*) > 5
ORDER BY
average_salary DESC;

Output Example:

department    | average_salary | employee_count
--------------+----------------+----------------
Engineering | 78350.25 | 12
Marketing | 67840.50 | 8
Finance | 65420.75 | 7

Challenge 3: Working with JOINs

Joins are critical for combining data from multiple tables, a common requirement in real-world applications.

Problem: Find all orders with customer information, including customers who haven't placed any orders yet.

Table Structures:

sql
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(100)
);

CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
order_date DATE,
total_amount DECIMAL(10, 2),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

Solution:

sql
SELECT 
c.customer_name,
c.email,
o.order_id,
o.order_date,
o.total_amount
FROM
customers c
LEFT JOIN
orders o ON c.customer_id = o.customer_id
ORDER BY
c.customer_name, o.order_date;

Output Example:

customer_name  | email                | order_id | order_date | total_amount
---------------+----------------------+----------+------------+--------------
Alex Wong | [email protected] | 1003 | 2023-02-15 | 145.80
Alex Wong | [email protected] | 1008 | 2023-03-22 | 67.50
James Miller | [email protected] | NULL | NULL | NULL
Maria Garcia | [email protected] | 1001 | 2023-01-10 | 250.00
Maria Garcia | [email protected] | 1006 | 2023-03-05 | 89.99

Notice that James Miller appears in the result even though he hasn't placed any order yet (NULL values). This demonstrates the LEFT JOIN's ability to include all records from the left table regardless of matches.

Challenge 4: Subqueries

Subqueries allow you to nest one query inside another, enabling more complex data retrieval.

Problem: Find employees who earn more than the average salary in their department.

Solution:

sql
SELECT 
e1.name,
e1.department,
e1.salary
FROM
employees e1
WHERE
e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department = e1.department
)
ORDER BY
e1.department, e1.salary DESC;

Output Example:

name           | department  | salary
---------------+-------------+----------
Sarah Johnson | Engineering | 95000.00
Michael Smith | Engineering | 85000.00
Jennifer Lee | Marketing | 72500.00
David Wilson | Finance | 79000.00

Challenge 5: Window Functions

Window functions perform calculations across a set of table rows related to the current row, making them powerful tools for analytics.

Problem: Rank employees by salary within each department.

Solution:

sql
SELECT 
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank
FROM
employees;

Output Example:

name           | department  | salary    | salary_rank
---------------+-------------+-----------+------------
Sarah Johnson | Engineering | 95000.00 | 1
Michael Smith | Engineering | 85000.00 | 2
Robert Brown | Engineering | 78000.00 | 3
Jennifer Lee | Marketing | 72500.00 | 1
Emily Davis | Marketing | 68000.00 | 2
David Wilson | Finance | 79000.00 | 1
Lisa Martinez | Finance | 65500.00 | 2

Challenge 6: Common Table Expressions (CTEs)

CTEs provide a way to write more readable and maintainable queries by breaking down complex logic into named temporary result sets.

Problem: Find customers who have placed orders with a total value greater than the average order value.

Solution:

sql
WITH AverageOrderValue AS (
SELECT AVG(total_amount) AS avg_value
FROM orders
),
CustomerTotalSpend AS (
SELECT
c.customer_id,
c.customer_name,
SUM(o.total_amount) AS total_spent,
COUNT(o.order_id) AS order_count
FROM
customers c
JOIN
orders o ON c.customer_id = o.customer_id
GROUP BY
c.customer_id, c.customer_name
)

SELECT
cts.customer_name,
cts.total_spent,
cts.order_count,
aov.avg_value AS average_order_value
FROM
CustomerTotalSpend cts,
AverageOrderValue aov
WHERE
cts.total_spent > (cts.order_count * aov.avg_value)
ORDER BY
cts.total_spent DESC;

Output Example:

customer_name  | total_spent | order_count | average_order_value
---------------+-------------+-------------+---------------------
Maria Garcia | 339.99 | 2 | 120.65
Alex Wong | 213.30 | 2 | 120.65

Challenge 7: Date and Time Functions

Handling dates and times is a common requirement in SQL interviews.

Problem: Group orders by month and calculate monthly sales totals for the current year.

Solution:

sql
SELECT 
EXTRACT(MONTH FROM order_date) AS month,
TO_CHAR(order_date, 'Month') AS month_name,
COUNT(*) AS order_count,
SUM(total_amount) AS monthly_total
FROM
orders
WHERE
EXTRACT(YEAR FROM order_date) = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY
EXTRACT(MONTH FROM order_date),
TO_CHAR(order_date, 'Month')
ORDER BY
month;

Output Example:

month | month_name | order_count | monthly_total
------+------------+-------------+--------------
1 | January | 8 | 1240.75
2 | February | 12 | 1875.50
3 | March | 15 | 2340.25

Challenge 8: Handling NULL Values

Dealing with NULL values correctly is essential for accurate query results.

Problem: Find all departments where at least one employee has a missing performance_rating.

Table Structure:

sql
ALTER TABLE employees ADD COLUMN performance_rating DECIMAL(3, 1);

Solution:

sql
SELECT 
department,
COUNT(*) AS total_employees,
SUM(CASE WHEN performance_rating IS NULL THEN 1 ELSE 0 END) AS missing_ratings
FROM
employees
GROUP BY
department
HAVING
SUM(CASE WHEN performance_rating IS NULL THEN 1 ELSE 0 END) > 0
ORDER BY
missing_ratings DESC;

Output Example:

department    | total_employees | missing_ratings
--------------+-----------------+----------------
Sales | 10 | 3
Operations | 7 | 2
Marketing | 8 | 1

Challenge 9: Set Operations

Set operations combine results from multiple queries.

Problem: Find employees who work in both the 'Marketing' and 'Digital' projects.

Table Structures:

sql
CREATE TABLE projects (
project_id INT PRIMARY KEY,
project_name VARCHAR(100)
);

CREATE TABLE employee_projects (
employee_id INT,
project_id INT,
PRIMARY KEY (employee_id, project_id),
FOREIGN KEY (employee_id) REFERENCES employees(id),
FOREIGN KEY (project_id) REFERENCES projects(project_id)
);

Solution:

sql
SELECT 
e.id,
e.name
FROM
employees e
WHERE
e.id IN (
SELECT ep.employee_id
FROM employee_projects ep
JOIN projects p ON ep.project_id = p.project_id
WHERE p.project_name = 'Marketing'
)
AND e.id IN (
SELECT ep.employee_id
FROM employee_projects ep
JOIN projects p ON ep.project_id = p.project_id
WHERE p.project_name = 'Digital'
);

Alternative Solution using INTERSECT:

sql
SELECT 
e.id,
e.name
FROM
employees e
JOIN
employee_projects ep ON e.id = ep.employee_id
JOIN
projects p ON ep.project_id = p.project_id
WHERE
p.project_name = 'Marketing'

INTERSECT

SELECT
e.id,
e.name
FROM
employees e
JOIN
employee_projects ep ON e.id = ep.employee_id
JOIN
projects p ON ep.project_id = p.project_id
WHERE
p.project_name = 'Digital';

Output Example:

id  | name
----+---------------
101 | Jennifer Lee
105 | Michael Smith

Challenge 10: Recursive Queries

Recursive queries are powerful for handling hierarchical or graph-like data structures.

Problem: Display an employee hierarchy starting with the CEO.

Table Structure:

sql
ALTER TABLE employees ADD COLUMN manager_id INT;

Solution:

sql
WITH RECURSIVE EmployeeHierarchy AS (
-- Base case: CEO (employee with no manager)
SELECT
id,
name,
manager_id,
0 AS level,
name AS hierarchy_path
FROM
employees
WHERE
manager_id IS NULL

UNION ALL

-- Recursive case: employees with managers
SELECT
e.id,
e.name,
e.manager_id,
eh.level + 1,
eh.hierarchy_path || ' > ' || e.name
FROM
employees e
JOIN
EmployeeHierarchy eh ON e.manager_id = eh.id
)

SELECT
id,
name,
level,
hierarchy_path
FROM
EmployeeHierarchy
ORDER BY
hierarchy_path;

Output Example:

id  | name           | level | hierarchy_path
----+----------------+-------+----------------------------------
1 | John CEO | 0 | John CEO
2 | Lisa VP | 1 | John CEO > Lisa VP
5 | Sarah Johnson | 2 | John CEO > Lisa VP > Sarah Johnson
3 | David VP | 1 | John CEO > David VP
4 | Michael Smith | 2 | John CEO > David VP > Michael Smith
7 | Robert Brown | 3 | John CEO > David VP > Michael Smith > Robert Brown

Real-world Application Example

Let's tie everything together with a comprehensive example that might appear in an advanced interview.

Scenario: You're analyzing an e-commerce database and need to identify product performance trends.

Problem: Find the top 3 products in each category by sales volume for the last quarter, including comparison with the previous quarter's performance.

Solution:

sql
WITH QuarterlySales AS (
SELECT
p.product_id,
p.product_name,
c.category_name,
DATE_TRUNC('quarter', o.order_date) AS quarter,
SUM(oi.quantity) AS total_quantity,
SUM(oi.quantity * oi.unit_price) AS total_sales
FROM
products p
JOIN
categories c ON p.category_id = c.category_id
JOIN
order_items oi ON p.product_id = oi.product_id
JOIN
orders o ON oi.order_id = o.order_id
WHERE
o.order_date >= DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '6 months'
AND o.order_date < DATE_TRUNC('quarter', CURRENT_DATE) + INTERVAL '3 months'
GROUP BY
p.product_id, p.product_name, c.category_name, DATE_TRUNC('quarter', o.order_date)
),
RankedProducts AS (
SELECT
product_id,
product_name,
category_name,
quarter,
total_quantity,
total_sales,
RANK() OVER (PARTITION BY category_name, quarter ORDER BY total_sales DESC) AS sales_rank
FROM
QuarterlySales
),
CurrentQuarterTop AS (
SELECT
product_id,
product_name,
category_name,
total_quantity AS current_quantity,
total_sales AS current_sales
FROM
RankedProducts
WHERE
quarter = DATE_TRUNC('quarter', CURRENT_DATE)
AND sales_rank <= 3
),
PreviousQuarterData AS (
SELECT
product_id,
total_quantity AS previous_quantity,
total_sales AS previous_sales
FROM
RankedProducts
WHERE
quarter = DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '3 months'
)

SELECT
c.category_name,
c.product_name,
c.current_quantity,
c.current_sales,
p.previous_quantity,
p.previous_sales,
CASE
WHEN p.previous_sales IS NULL THEN 'New Product'
ELSE ROUND((c.current_sales - p.previous_sales) / p.previous_sales * 100, 1) || '%'
END AS sales_growth
FROM
CurrentQuarterTop c
LEFT JOIN
PreviousQuarterData p ON c.product_id = p.product_id
ORDER BY
c.category_name,
c.current_sales DESC;

Output Example:

category_name | product_name        | current_quantity | current_sales | previous_quantity | previous_sales | sales_growth
--------------+---------------------+------------------+---------------+-------------------+----------------+-------------
Electronics | Premium Headphones | 450 | 44950.00 | 410 | 40950.00 | 9.8%
Electronics | Smartphone Pro | 320 | 35840.00 | 280 | 31360.00 | 14.3%
Electronics | Wireless Earbuds | 560 | 22400.00 | NULL | NULL | New Product
Home Goods | Luxury Bedding Set | 210 | 18900.00 | 180 | 16200.00 | 16.7%
Home Goods | Smart Thermostat | 280 | 15680.00 | 240 | 13440.00 | 16.7%
Home Goods | Kitchen Mixer | 175 | 12250.00 | 190 | 13300.00 | -7.9%

Summary

SQL query challenges in interviews test your ability to manipulate and extract insights from relational data. The key skills tested include:

  1. Basic querying: Filtering, sorting, and limiting results
  2. Aggregations: GROUP BY, HAVING, and aggregate functions
  3. Joins: Combining data from multiple tables
  4. Subqueries: Nesting queries for complex operations
  5. Window functions: Performing calculations across related rows
  6. CTEs: Breaking down complex queries into manageable parts
  7. Date/time handling: Working with temporal data
  8. NULL handling: Properly managing missing values
  9. Set operations: Combining results from multiple queries
  10. Recursive queries: Working with hierarchical data

Practice Exercises

To solidify your SQL skills, try these challenges:

  1. Find the median salary by department using window functions.
  2. Calculate the running total of sales by date for each product category.
  3. Identify customers who have purchased all products in a specific category.
  4. Find products that have never been ordered together in the same transaction.
  5. Calculate the average time between orders for each customer.

Additional Resources

Remember that SQL proficiency comes with practice. The more you work with different types of queries and database schemas, the better prepared you'll be for technical interviews.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)