40 SQL Interview Questions & Solution For DBA
40 SQL Interview Questions & Solution For DBA
Asfaw Gedamu
July 2, 2025
40 SQL Interview Questions & Solutions For
DBAs
Your interviewer has 30 minutes. Your mission: write queries that show you can reason in data.
Not just recite syntax.
I pulled together 40 real-world SQL challenges that cover the exact patterns hiring managers
probe for:
Salary & Rank Logic: second-highest salary, department pay gaps, in-department ranking
with RANK()
Revenue Analytics : Pareto 80 percent products, product-level revenue share, YoY growth
| customer_id | order_date |
|-------------|------------|
| 101 | 2023-01-15 |
| 101 | 2023-02-20 |
| 102 | 2023-01-10 |
| 103 | 2023-02-05 |
| 101 | 2023-03-01 |
Statement:
WITH monthly_customers AS (
SELECT DISTINCT
customer_id,
FORMAT(order_date, 'yyyy-MM') AS month
FROM Orders
),
retention AS (
SELECT m1.month,
COUNT(DISTINCT m1.customer_id) AS current_customers,
COUNT(DISTINCT m2.customer_id) AS retained_customers
FROM monthly_customers m1
LEFT JOIN monthly_customers m2 ON m1.customer_id = m2.customer_id
AND m2.month = DATEADD(MONTH, 1,
m1.month)
GROUP BY m1.month
)
SELECT month,
current_customers,
retained_customers,
ROUND(100.0 * retained_customers / NULLIF(current_customers,
0), 2) AS retention_rate
FROM retention
ORDER BY month;
Output:
Output:
| SecondHighestSalary |
|---------------------|
| 75000 |
Input (Department):
| department_id | dept_name |
|---------------|-----------|
| 101 | Sales |
| 102 | Marketing |
Statement:
SELECT e.*
FROM Employee e
LEFT JOIN Department d ON e.department_id = d.department_id
WHERE d.department_id IS NULL;
Output:
Statement:
Input (Sales):
Output:
| product_id | total_revenue |
|------------|---------------|
| 101 | 250 |
| 102 | 100 |
| 103 | 100 |
Statement:
SELECT TOP 3 *
FROM Employee
ORDER BY salary DESC;
Output:
| customer_id | name |
|-------------|--------|
| 101 | Alice |
| 102 | Bob |
| 103 | Carol |
Input (Orders):
| order_id | customer_id |
|----------|-------------|
| 1 | 101 |
| 2 | 102 |
| 3 | 103 |
Input (Returns):
| return_id | customer_id |
|-----------|-------------|
| 1 | 101 |
Statement:
| customer_id |
|-------------|
| 102 |
| 103 |
| order_id | customer_id |
|----------|-------------|
| 1 | 101 |
| 2 | 101 |
| 3 | 102 |
| 4 | 101 |
| 5 | 103 |
Statement:
Output:
| customer_id | order_count |
|-------------|-------------|
| 101 | 3 |
| 102 | 1 |
| 103 | 1 |
SELECT *
FROM Employee
WHERE YEAR(hire_date) = 2023;
Output:
Statement:
Output:
| customer_id | avg_order_value |
|-------------|-----------------|
| 101 | 150 |
| 102 | 75 |
Statement:
FROM Orders
GROUP BY customer_id;
Output:
| customer_id | latest_order_date |
|-------------|-------------------|
| 101 | 2023-03-05 |
| 102 | 2023-01-10 |
| product_id | product_name |
|------------|--------------|
| 101 | Laptop |
| 102 | Phone |
| 103 | Tablet |
| 104 | Monitor |
Input (Sales):
Statement:
SELECT p.product_id
FROM Products p
LEFT JOIN Sales s ON p.product_id = s.product_id
WHERE s.product_id IS NULL;
Output:
| product_id |
|------------|
| 103 |
| 104 |
Statement:
Output:
| product_id | total_qty |
|------------|-----------|
| 101 | 5 |
13. Get the total revenue and the number of orders per
region
Input (Orders):
Statement:
SELECT region,
SUM(total_amount) AS total_revenue,
COUNT(*) AS order_count
FROM Orders
GROUP BY region;
Output:
| order_id | customer_id |
|----------|-------------|
| 1 | 101 |
| 2 | 101 |
| 3 | 101 |
| 4 | 101 |
| 5 | 101 |
| 6 | 101 |
| 7 | 102 |
| 8 | 102 |
| 9 | 103 |
Statement:
Output:
| customer_count |
|----------------|
| 1 |
Statement:
SELECT *
FROM Orders
WHERE total_amount > (SELECT AVG(total_amount) FROM Orders);
Output:
Statement:
SELECT *
FROM Employee
WHERE DATENAME(WEEKDAY, hire_date) IN ('Saturday', 'Sunday');
Output:
Statement:
SELECT *
FROM Employee
WHERE salary BETWEEN 50000 AND 100000;
Output:
Statement:
Output:
Statement:
Output:
Statement:
SELECT customer_id
FROM Orders
WHERE YEAR(order_date) = 2023
GROUP BY customer_id
HAVING COUNT(DISTINCT FORMAT(order_date,'yyyy-MM')) = 12;
Output:
| customer_id |
|-------------|
| 101 |
21. Find moving average of sales over the last 3 days
Input (Orders):
Statement:
SELECT order_date,
AVG(total_amount) OVER (ORDER BY order_date ROWS BETWEEN 2
PRECEDING AND CURRENT ROW) AS moving_avg
FROM Orders;
Output:
| order_date | moving_avg |
|------------|------------|
| 2023-01-01 | 100.00 |
| 2023-01-02 | 125.00 |
| 2023-01-03 | 150.00 |
| 2023-01-04 | 175.00 |
| 2023-01-05 | 166.67 |
22. Identify the first and last order date for each customer
Input (Orders):
Statement:
SELECT customer_id,
MIN(order_date) AS first_order,
MAX(order_date) AS last_order
FROM Orders
GROUP BY customer_id;
Output:
Statement:
WITH TotalRevenue AS (
SELECT SUM(quantity * price) AS total
FROM Sales
)
SELECT s.product_id,
SUM(s.quantity * s.price) AS revenue,
SUM(s.quantity * s.price) * 100 / t.total AS revenue_pct
FROM Sales s
CROSS JOIN TotalRevenue t
GROUP BY s.product_id, t.total;
Output:
| id | order_date |
|----|------------|
| 101| 2023-01-01 |
| 101| 2023-01-02 |
| 101| 2023-01-04 |
| 102| 2023-01-10 |
| 102| 2023-01-11 |
| 103| 2023-01-15 |
Statement:
WITH cte AS (
SELECT id, order_date,
LAG(order_date) OVER (PARTITION BY id ORDER BY order_date)
AS prev_order_date
FROM Orders
)
SELECT id, order_date, prev_order_date
FROM cte
WHERE DATEDIFF(DAY, prev_order_date, order_date) = 1;
Output:
| id | order_date | prev_order_date |
|-----|------------|-----------------|
| 101 | 2023-01-02 | 2023-01-01 |
| 102 | 2023-01-11 | 2023-01-10 |
Statement:
SELECT customer_id
FROM Orders
GROUP BY customer_id
HAVING MAX(order_date) < DATEADD(MONTH, -6, GETDATE());
Output:
| customer_id |
|-------------|
| 103 |
| order_date | total_amount |
|------------|--------------|
| 2023-01-01 | 100 |
| 2023-01-02 | 150 |
| 2023-01-03 | 200 |
| 2023-01-05 | 175 |
Statement:
SELECT order_date,
SUM(total_amount) OVER (ORDER BY order_date) AS
cumulative_revenue
FROM Orders;
Output:
| order_date | cumulative_revenue |
|------------|--------------------|
| 2023-01-01 | 100 |
| 2023-01-02 | 250 |
| 2023-01-03 | 450 |
| 2023-01-05 | 625 |
Statement:
SELECT department_id,
AVG(salary) AS avg_salary
FROM Employee
GROUP BY department_id
ORDER BY avg_salary DESC;
Output:
| department_id | avg_salary |
|---------------|------------|
| 103 | 95000 |
| 101 | 87500 |
| 102 | 77500 |
| order_id | customer_id |
|----------|-------------|
| 1 | 101 |
| 2 | 101 |
| 3 | 101 |
| 4 | 102 |
| 5 | 102 |
| 6 | 103 |
Statement:
WITH customer_orders AS (
SELECT customer_id, COUNT(*) AS order_count
FROM Orders
GROUP BY customer_id
)
SELECT * FROM customer_orders
WHERE order_count > (SELECT AVG(order_count) FROM customer_orders);
Output:
| customer_id | order_count |
|-------------|-------------|
| 101 | 3 |
Statement:
WITH first_orders AS (
SELECT customer_id, MIN(order_date) AS first_order_date
FROM Orders
GROUP BY customer_id
)
SELECT SUM(o.total_amount) AS new_revenue
FROM Orders o
JOIN first_orders f ON o.customer_id = f.customer_id
WHERE o.order_date = f.first_order_date;
Output:
| new_revenue |
|-------------|
| 475 |
| emp_id | department_id |
|--------|---------------|
| 1 | 101 |
| 2 | 101 |
| 3 | 102 |
| 4 | 103 |
| 5 | 103 |
| 6 | 103 |
Statement:
SELECT department_id,
COUNT(*) AS emp_count,
COUNT(*) * 100.0 / (SELECT COUNT(*) FROM Employee) AS pct
FROM Employee
GROUP BY department_id;
Output:
Statement:
SELECT department_id,
MAX(salary) - MIN(salary) AS salary_diff
FROM Employee
GROUP BY department_id;
Output:
| department_id | salary_diff |
|---------------|-------------|
| 101 | 15000 |
| 102 | 3000 |
| 103 | 0 |
Statement:
WITH sales_cte AS (
SELECT product_id, SUM(quantity * price) AS revenue
FROM Sales GROUP BY product_id
),
total_revenue AS (
SELECT SUM(revenue) AS total FROM sales_cte
)
SELECT s.product_id, s.revenue,
SUM(s.revenue) OVER (ORDER BY s.revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS
running_total
FROM sales_cte s, total_revenue t
WHERE SUM(s.revenue) OVER (ORDER BY s.revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) <= t.total *
0.8;
Output:
| customer_id | order_date |
|-------------|------------|
| 101 | 2023-01-01 |
| 101 | 2023-01-10 |
| 101 | 2023-01-25 |
| 102 | 2023-02-05 |
| 102 | 2023-02-20 |
Statement:
WITH cte AS (
SELECT customer_id, order_date,
LAG(order_date) OVER (PARTITION BY customer_id ORDER BY
order_date) AS prev_date
FROM Orders
)
SELECT customer_id,
AVG(DATEDIFF(DAY, prev_date, order_date)) AS avg_gap_days
FROM cte
WHERE prev_date IS NOT NULL
GROUP BY customer_id;
Output:
| customer_id | avg_gap_days |
|-------------|--------------|
| 101 | 12.0 |
| 102 | 15.0 |
34. Show last purchase for each customer with order amount
Input (Orders):
Statement:
WITH ranked_orders AS (
SELECT customer_id, order_id, total_amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY
order_date DESC) AS rn
FROM Orders
)
SELECT customer_id, order_id, total_amount
FROM ranked_orders
WHERE rn = 1;
Output:
| order_date | total_amount |
|------------|--------------|
| 2021-01-15 | 1000 |
| 2021-02-20 | 1500 |
| 2022-01-10 | 2000 |
| 2022-03-05 | 2500 |
| 2023-02-01 | 3000 |
Statement:
Output:
Statement:
WITH ranked_orders AS (
SELECT customer_id, order_id, total_amount,
NTILE(10) OVER (PARTITION BY customer_id ORDER BY
total_amount) AS decile
FROM Orders
)
SELECT customer_id, order_id, total_amount
FROM ranked_orders
WHERE decile = 10;
Output:
| customer_id | order_date |
|-------------|------------|
| 101 | 2023-01-01 |
| 101 | 2023-01-10 |
| 101 | 2023-02-15 |
| 102 | 2023-01-05 |
| 102 | 2023-03-01 |
Statement:
WITH cte AS (
SELECT customer_id, order_date,
LAG(order_date) OVER (PARTITION BY customer_id ORDER BY
order_date) AS prev_order_date
FROM Orders
)
SELECT customer_id, MAX(DATEDIFF(DAY, prev_order_date, order_date)) AS
max_gap
FROM cte
WHERE prev_order_date IS NOT NULL
GROUP BY customer_id;
Output:
| customer_id | max_gap |
|-------------|---------|
| 101 | 36 |
| 102 | 55 |
| customer_id | total_amount |
|-------------|--------------|
| 101 | 100 |
| 101 | 200 |
| 102 | 50 |
| 103 | 300 |
| 104 | 75 |
Statement:
WITH cte AS (
SELECT customer_id, SUM(total_amount) AS total_revenue
FROM Orders
GROUP BY customer_id
)
SELECT customer_id, total_revenue
FROM cte
WHERE total_revenue < (SELECT PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER
BY total_revenue) FROM cte);
Output:
| customer_id | total_revenue |
|-------------|---------------|
| 102 | 50 |
| 104 | 75 |
Statement:
WITH dept_avg AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM Employee
GROUP BY department_id
)
SELECT e.employee_id, e.department_id, e.salary, d.avg_salary
FROM Employee e
JOIN dept_avg d ON e.department_id = d.department_id
WHERE e.salary > d.avg_salary;
Output:
| column1 | column2 |
|---------|---------|
| John | Doe |
| Jane | Smith |
| John | Doe |
| Mike | Johnson |
| Jane | Smith |
Statement:
Output:
Conclusion
These questions cover advanced analytical scenarios including statistical calculations
(percentiles), time-based analysis (retention rates), and complex business metrics (Pareto
principle). Each solution demonstrates practical applications of window functions, CTEs, and
advanced joins that are essential for data analysis roles.