SQL Interview — 50 Questions & Answers
Data Analytics Preparation Guide | Easy to Hard
■ EASY (Q1–Q15) — Fundamentals
Q1. Retrieve all columns from employees.
SELECT * FROM employees;
■ Use * to select all columns. Avoid in production; list columns explicitly.
Q2. Select only name and salary from employees.
SELECT name, salary FROM employees;
■ Always specify column names for clarity and performance.
Q3. Find employees with salary > 50000.
SELECT * FROM employees WHERE salary > 50000;
■ WHERE filters rows before returning results.
Q4. Retrieve employees from department 'HR'.
SELECT * FROM employees WHERE department = 'HR';
■ String values must be enclosed in single quotes.
Q5. Sort employees by salary descending.
SELECT * FROM employees ORDER BY salary DESC;
■ Use ASC for ascending (default), DESC for descending.
Q6. Find total number of employees.
SELECT COUNT(*) AS total_employees FROM employees;
■ COUNT(*) counts all rows including NULLs.
Q7. Get maximum salary.
SELECT MAX(salary) AS max_salary FROM employees;
■ Aggregate functions: MAX, MIN, SUM, AVG, COUNT.
Q8. Find minimum and average salary.
SELECT MIN(salary) AS min_salary, AVG(salary) AS avg_salary FROM employees;
■ Multiple aggregates can be used in one SELECT.
Q9. Retrieve distinct department names.
SELECT DISTINCT department FROM employees;
■ DISTINCT removes duplicate values from results.
Q10. Find employees whose name starts with 'A'.
SELECT * FROM employees WHERE name LIKE 'A%';
■ % = any number of chars, _ = exactly one char.
Q11. Get employees hired between 2020-01-01 and 2022-12-31.
SELECT * FROM employees WHERE hire_date BETWEEN '2020-01-01' AND '2022-12-31';
■ BETWEEN is inclusive on both ends.
Q12. Count employees in each department.
SELECT department, COUNT(*) AS emp_count FROM employees GROUP BY department;
■ GROUP BY groups rows; use with aggregate functions.
Q13. Find employees where email is NULL.
SELECT * FROM employees WHERE email IS NULL;
■ Never use = NULL. Always use IS NULL or IS NOT NULL.
Q14. Retrieve top 5 highest paid employees.
SELECT * FROM employees ORDER BY salary DESC LIMIT 5;
■ Use TOP 5 in SQL Server, ROWNUM in Oracle.
Q15. Show salary as monthly_pay using alias.
SELECT name, salary AS monthly_pay FROM employees;
■ AS keyword creates an alias. AS is optional but recommended.
■ MEDIUM (Q16–Q35) — Joins, Grouping & Filtering
Q16. INNER JOIN employees and departments on department_id.
SELECT [Link], d.department_name FROM employees e INNER JOIN departments d ON
e.department_id = d.department_id;
■ INNER JOIN returns only rows with matching keys in both tables.
Q17. Get all employees including those with no department (LEFT JOIN).
SELECT [Link], d.department_name FROM employees e LEFT JOIN departments d ON e.department_id
= d.department_id;
■ LEFT JOIN keeps all rows from left table; NULLs where no match.
Q18. Find departments that have no employees.
SELECT d.department_name FROM departments d LEFT JOIN employees e ON d.department_id =
e.department_id WHERE e.department_id IS NULL;
■ Filter with IS NULL after LEFT JOIN to find unmatched rows.
Q19. Get total salary spent per department.
SELECT department, SUM(salary) AS total_salary FROM employees GROUP BY department;
■ SUM with GROUP BY aggregates per group.
Q20. Find departments with average salary > 60000.
SELECT department, AVG(salary) AS avg_sal FROM employees GROUP BY department HAVING
AVG(salary) > 60000;
■ HAVING filters after GROUP BY. WHERE filters before grouping.
Q21. Find the second highest salary.
SELECT MAX(salary) AS second_highest FROM employees WHERE salary < (SELECT MAX(salary)
FROM employees);
■ Alternative: SELECT DISTINCT salary ORDER BY salary DESC LIMIT 1 OFFSET 1
Q22. Employees earning more than their department's average salary.
SELECT [Link], [Link], [Link] FROM employees e WHERE [Link] > ( SELECT
AVG(salary) FROM employees WHERE department = [Link] );
■ Correlated subquery: inner query references outer query's row.
Q23. Find duplicate email addresses.
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1;
■ HAVING COUNT(*) > 1 catches duplicates.
Q24. Customers with more than 3 orders.
SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id HAVING COUNT(*)
> 3;
■ Common pattern: GROUP BY + HAVING for conditional aggregation.
Q25. Label employees as High/Medium/Low using CASE WHEN.
SELECT name, salary, CASE WHEN salary > 80000 THEN 'High' WHEN salary > 50000 THEN
'Medium' ELSE 'Low' END AS salary_band FROM employees;
■ CASE WHEN is SQL's if-else. Evaluated top to bottom.
Q26. Find employees who share salary with at least one other.
SELECT * FROM employees WHERE salary IN ( SELECT salary FROM employees GROUP BY salary
HAVING COUNT(*) > 1 );
■ IN with subquery is a clean way to filter matching sets.
Q27. Employees in the department with highest average salary.
SELECT * FROM employees WHERE department = ( SELECT department FROM employees GROUP BY
department ORDER BY AVG(salary) DESC LIMIT 1 );
■ Subquery first finds the department, outer query filters rows.
Q28. 3rd to 5th highest salaries.
SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 3 OFFSET 2;
■ OFFSET 2 skips the first 2 rows (ranks 1 and 2).
Q29. Join 3 tables: orders, customers, products.
SELECT [Link], o.order_id, p.product_name FROM orders o JOIN customers c ON o.customer_id =
c.customer_id JOIN products p ON o.product_id = p.product_id;
■ Chain multiple JOINs; each adds another table.
Q30. Customers who ordered in both 2023 and 2024.
SELECT customer_id FROM orders WHERE YEAR(order_date) = 2023 INTERSECT SELECT customer_id
FROM orders WHERE YEAR(order_date) = 2024;
■ INTERSECT returns rows common to both queries (MySQL: use subquery with IN).
Q31. Running total of sales ordered by date.
SELECT order_date, amount, SUM(amount) OVER ( ORDER BY order_date ) AS running_total FROM
orders;
■ SUM() OVER (ORDER BY ...) computes a cumulative sum.
Q32. Month-wise total revenue.
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS revenue FROM orders GROUP
BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month;
■ Use TO_CHAR in Oracle, FORMAT in SQL Server.
Q33. Find employees grouped by same manager_id.
SELECT manager_id, COUNT(*) AS direct_reports FROM employees WHERE manager_id IS NOT NULL
GROUP BY manager_id ORDER BY direct_reports DESC;
■ Useful for org-structure analysis.
Q34. Replace NULL bonus with 0 using COALESCE.
SELECT name, COALESCE(bonus, 0) AS bonus FROM employees;
■ COALESCE returns the first non-NULL value from the list.
Q35. Self-join: pairs of employees in the same department.
SELECT [Link] AS emp1, [Link] AS emp2, [Link] FROM employees a JOIN employees b ON
[Link] = [Link] AND a.employee_id < b.employee_id;
■ [Link] < [Link] prevents duplicate pairs and self-pairing.
■ HARD (Q36–Q50) — Window Functions, CTEs & Advanced
Q36. ROW_NUMBER() to rank employees by salary per department.
SELECT name, department, salary, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary
DESC ) AS rn FROM employees;
■ ROW_NUMBER gives unique sequential ranks; no ties.
Q37. RANK() vs DENSE_RANK() — write both.
-- RANK: gaps after ties SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rnk FROM
employees; -- DENSE_RANK: no gaps after ties SELECT name, salary, DENSE_RANK() OVER (ORDER
BY salary DESC) AS dense_rnk FROM employees;
■ RANK: 1,2,2,4 | DENSE_RANK: 1,2,2,3 — no gap after tie.
Q38. LEAD() and LAG() for month-over-month sales difference.
SELECT month, sales, LAG(sales) OVER (ORDER BY month) AS prev_month, LEAD(sales) OVER (ORDER
BY month) AS next_month, sales - LAG(sales) OVER (ORDER BY month) AS mom_diff FROM
monthly_sales;
■ LAG looks back; LEAD looks forward in the ordered window.
Q39. CTE to find top earner per department.
WITH ranked AS ( SELECT name, department, salary, RANK() OVER ( PARTITION BY department
ORDER BY salary DESC ) AS rnk FROM employees ) SELECT name, department, salary FROM ranked
WHERE rnk = 1;
■ CTEs (WITH clause) make complex queries more readable.
Q40. Recursive CTE for employee hierarchy.
WITH RECURSIVE emp_tree AS ( -- Anchor: top-level managers SELECT employee_id, name,
manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL -- Recursive:
subordinates SELECT e.employee_id, [Link], e.manager_id, [Link] + 1 FROM employees e JOIN
emp_tree et ON e.manager_id = et.employee_id ) SELECT * FROM emp_tree;
■ Recursive CTEs are ideal for tree/hierarchical data.
Q41. Cumulative sum of sales partitioned by region.
SELECT region, sale_date, amount, SUM(amount) OVER ( PARTITION BY region ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_sales FROM sales;
■ PARTITION BY resets the running total for each region.
Q42. Customers who ordered every month in 2024.
SELECT customer_id FROM orders WHERE YEAR(order_date) = 2024 GROUP BY customer_id HAVING
COUNT(DISTINCT MONTH(order_date)) = 12;
■ COUNT DISTINCT months = 12 means every month was covered.
Q43. Detect gaps in sequential order IDs.
SELECT order_id + 1 AS gap_start FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM orders WHERE
order_id = o.order_id + 1 ) AND order_id < (SELECT MAX(order_id) FROM orders);
■ Alternative: use LAG to compare consecutive IDs.
Q44. Pivot: monthly sales as columns per product.
SELECT product_id, SUM(CASE WHEN MONTH(sale_date)=1 THEN amount ELSE 0 END) AS Jan, SUM(CASE
WHEN MONTH(sale_date)=2 THEN amount ELSE 0 END) AS Feb, SUM(CASE WHEN MONTH(sale_date)=3
THEN amount ELSE 0 END) AS Mar FROM sales GROUP BY product_id;
■ Conditional SUM with CASE WHEN is the standard pivot pattern.
Q45. First and last order date per customer using window functions.
SELECT DISTINCT customer_id, FIRST_VALUE(order_date) OVER ( PARTITION BY customer_id ORDER
BY order_date ) AS first_order, LAST_VALUE(order_date) OVER ( PARTITION BY customer_id ORDER
BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS last_order FROM
orders;
■ LAST_VALUE needs ROWS BETWEEN ... UNBOUNDED FOLLOWING to work correctly.
Q46. Month-over-month revenue growth %.
WITH monthly AS ( SELECT DATE_FORMAT(order_date,'%Y-%m') AS month, SUM(amount) AS revenue
FROM orders GROUP BY 1 ) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS
prev_revenue, ROUND( (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER
(ORDER BY month) * 100, 2 ) AS growth_pct FROM monthly;
■ Combine CTE + LAG for clean MoM growth calculation.
Q47. Top 3 products by sales in each category.
WITH ranked AS ( SELECT category, product_id, SUM(amount) AS total_sales, DENSE_RANK() OVER
( PARTITION BY category ORDER BY SUM(amount) DESC ) AS rnk FROM sales GROUP BY category,
product_id ) SELECT category, product_id, total_sales FROM ranked WHERE rnk <= 3;
■ DENSE_RANK ensures ties don't knock out a product from top 3.
Q48. Users who logged in on consecutive days.
WITH daily AS ( SELECT DISTINCT user_id, CAST(login_date AS DATE) AS dt, DATEADD(DAY,
-ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ), login_date) AS grp FROM
logins ) SELECT user_id, MIN(dt) AS streak_start, MAX(dt) AS streak_end, COUNT(*) AS
consecutive_days FROM daily GROUP BY user_id, grp HAVING COUNT(*) >= 2;
■ Subtracting row number from date creates equal 'grp' for consecutive dates.
Q49. 7-day moving average of daily sales.
SELECT sale_date, daily_total, AVG(daily_total) OVER ( ORDER BY sale_date ROWS BETWEEN 6
PRECEDING AND CURRENT ROW ) AS moving_avg_7d FROM ( SELECT sale_date, SUM(amount) AS
daily_total FROM sales GROUP BY sale_date ) daily_sales;
■ ROWS BETWEEN 6 PRECEDING AND CURRENT ROW = 7-day window.
Q50. Find accounts where balance went negative using cumulative sum.
WITH running AS ( SELECT account_id, txn_date, amount, SUM(amount) OVER ( PARTITION BY
account_id ORDER BY txn_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS balance
FROM transactions ) SELECT DISTINCT account_id FROM running WHERE balance < 0;
■ Cumulative SUM over ordered transactions simulates a running balance.
Best of luck with your Data Analytics Interview! ■ Practice each query on real data for best results.