0% found this document useful (0 votes)
1 views16 pages

Top 25 SQL Questions

The document contains a comprehensive set of SQL queries and concepts, organized into three difficulty levels: medium, challenging, and advanced. Each query is accompanied by explanations of the SQL concepts involved, cross questions for deeper understanding, and tips for practical application. The queries cover various SQL functionalities such as JOINs, GROUP BY, window functions, CTEs, and more, aimed at enhancing SQL proficiency.

Uploaded by

ashoksamrat9289
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views16 pages

Top 25 SQL Questions

The document contains a comprehensive set of SQL queries and concepts, organized into three difficulty levels: medium, challenging, and advanced. Each query is accompanied by explanations of the SQL concepts involved, cross questions for deeper understanding, and tips for practical application. The queries cover various SQL functionalities such as JOINs, GROUP BY, window functions, CTEs, and more, aimed at enhancing SQL proficiency.

Uploaded by

ashoksamrat9289
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Here are all 25 answers:

SET 1 — MEDIUM

Q1 [JOINs] Write a query to find all customers who have placed at least one order.

SELECT c.customer_name,
COUNT(o.order_id) AS total_orders
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_name
ORDER BY total_orders DESC;

Concept: INNER JOIN returns only rows that match in both tables. COUNT(o.order_id)
counts orders per customer — always count the foreign key not *.

Cross questions:

● What is the difference between INNER JOIN and LEFT JOIN?


● What if a customer has 0 orders — does INNER JOIN show them?
● What does COUNT(*) vs COUNT(column) return differently?

Tip: COUNT(*) counts all rows including NULLs. COUNT(column) skips NULLs. Always be
explicit about which you mean.

Q2 [LEFT JOIN + NULL] Find all customers who have never placed an order.

SELECT c.customer_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

Concept: LEFT JOIN keeps all customers even those with no orders. When no match
exists, orders columns are NULL. WHERE o.order_id IS NULL isolates non-matching
customers.

Cross questions:

● Why can't you use WHERE o.order_id = NULL?


● What is the difference between IS NULL and = NULL?
● Could you solve this with NOT IN or NOT EXISTS? Which is faster?
Tip: LEFT JOIN + WHERE right_table.key IS NULL is one of the most tested SQL patterns.
Know it cold.

Q3 [GROUP BY + HAVING] Find departments where average salary is greater than


60,000.

SELECT department,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 60000
ORDER BY avg_salary DESC;

Concept: HAVING filters groups after aggregation. WHERE filters rows before aggregation.
You cannot use WHERE with aggregate functions.

Cross questions:

● What is the difference between WHERE and HAVING?


● Can you use a column alias in HAVING?
● What is the order of SQL clause execution?

Tip: Order of execution — FROM → WHERE → GROUP BY → HAVING → SELECT →

ORDER BY. Know this. It explains every SQL confusion.

Q4 [SUBQUERY] Find employees who earn more than the average salary of their
department

SELECT employee_name, salary, department


FROM employees e
WHERE salary > (
SELECT AVG(salary)
FROM employees
WHERE department = [Link]
);

Concept: A correlated subquery runs once per row of the outer query. It references the
outer query's current row via [Link].

Cross questions:

● What is the difference between a correlated and non-correlated subquery?


● Could you solve this with a window function instead? Which is more efficient?
● What happens if the subquery returns NULL?

Tip: Correlated subqueries are powerful but slow on large datasets. Show you know both
the subquery and window function approach.

Q5 [CASE WHEN] Classify employees as High, Medium or Low earner.

SELECT employee_name,
salary,
CASE
WHEN salary > 80000 THEN 'High'
WHEN salary BETWEEN 50000 AND 80000 THEN 'Medium'
ELSE 'Low'
END AS salary_band
FROM employees;

Concept: CASE WHEN evaluates conditions in order and returns the first match. ELSE
handles anything that does not match any condition.

Cross questions:

● What if you forgot the ELSE clause?


● Can you use CASE WHEN inside GROUP BY?
● Can you use CASE WHEN inside ORDER BY?

Tip: CASE WHEN inside SUM() creates conditional aggregates — SUM(CASE WHEN
status='paid' THEN amount ELSE 0 END). This is extremely powerful and leads directly to
Q16.

Q6 [WINDOW — ROW_NUMBER] Assign a row number to each employee within


their department ordered by salary descending.

SELECT employee_name,
department,
salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS row_num
FROM employees;
Concept: PARTITION BY resets the row number for each department. ORDER BY salary
DESC numbers the highest earner as 1. ROW_NUMBER always gives unique numbers
even for ties.

Cross questions:

● What is the difference between ROW_NUMBER, RANK and DENSE_RANK?


● How would you use this to find the top earner in each department?
● What happens if two employees have the same salary?

Tip: Wrap this in a CTE and add WHERE row_num = 1 to get the top earner per
department. One of the most asked patterns in analytics interviews.

Q7 [RANK vs DENSE_RANK] Show the difference between RANK() and


DENSE_RANK().

SELECT employee_name,
salary,
RANK() OVER (ORDER BY salary DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_num,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;

Concept: If salaries are 90k, 90k, 80k:

● RANK gives 1, 1, 3 — skips after tie


● DENSE_RANK gives 1, 1, 2 — no skip
● ROW_NUMBER gives 1, 2, 3 — always unique

Cross questions:

● When would you use RANK vs DENSE_RANK in a real scenario?


● Find the second highest salary — which function would you use?
● What is the difference between OVER() and OVER(PARTITION BY
department)?

Tip: The 1,1,3 vs 1,1,2 example is the single most asked SQL concept in analytics
interviews. Memorise it.

Q8 [DATE FUNCTIONS] Find orders in the last 30 days. Extract month and year.

-- Last 30 days
SELECT order_id, order_date
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days';

-- Extract month and year


SELECT order_id,
EXTRACT(MONTH FROM order_date) AS order_month,
EXTRACT(YEAR FROM order_date) AS order_year
FROM orders;

Concept: CURRENT_DATE returns today. INTERVAL subtracts time periods. EXTRACT


pulls specific parts from a date.

Cross questions:

● How do you calculate the number of days between two dates?


● What is the difference between DATE and DATETIME?
● How would you group orders by week?

Tip: Always clarify which SQL dialect — MySQL uses DATEDIFF, PostgreSQL uses
INTERVAL, SQL Server uses DATEADD. Mention this to show awareness.

Q9 [STRING FUNCTIONS] Find [Link]. Trim spaces from names.

SELECT TRIM(customer_name) AS clean_name,


email
FROM customers
WHERE email LIKE '%@[Link]';

Concept: LIKE with % matches any characters. TRIM removes leading and trailing spaces.
LTRIM removes left only, RTRIM removes right only.

Cross questions:

● What is the difference between LIKE and ILIKE?


● How would you extract just the domain from an email address?
● What is the performance impact of LIKE '%value%' with wildcard on both
sides?

Tip: LIKE with a leading wildcard cannot use an index and causes a full table scan. Always
mention this in optimisation discussions.

Q10 [SELF JOIN] Find employees who earn more than their manager.

SELECT e.employee_name,
[Link] AS employee_salary,
m.employee_name AS manager_name,
[Link] AS manager_salary
FROM employees e
JOIN employees m ON e.manager_id = m.employee_id
WHERE [Link] > [Link];

Concept: A self join joins a table to itself using aliases. e is the employee, m is the manager
— same table joined twice.

Cross questions:

● What happens to the CEO who has no manager?


● How would you show all employees including those with no manager?
● What is a recursive CTE and when would you use it instead?

Tip: Use LEFT JOIN instead of JOIN if you want to include employees with no manager.
Common follow-up.

SET 2 — CHALLENGING

Q11 [LAG/LEAD] Month-over-month revenue growth.

WITH monthly_revenue AS (
SELECT DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY 1
)
SELECT month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100, 2
) AS mom_growth_pct
FROM monthly_revenue
ORDER BY month;

Concept: LAG() accesses the previous row's value. MoM % = (current - previous) / previous
* 100. First month shows NULL — no previous month exists.

Cross questions:
● What does LEAD() do differently from LAG()?
● How would you handle the NULL in the first month?
● What is the difference between LAG(col, 1) and LAG(col, 2)?

Tip: LAG/LEAD MoM growth is tested in almost every senior analytics interview. Practice
until it is second nature.

Q12 [RUNNING TOTAL] Running total of sales by date.

SELECT sale_date,
daily_sales,
SUM(daily_sales) OVER (
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM daily_sales
ORDER BY sale_date;

Concept: SUM() as a window function with ORDER BY accumulates values from the first
row to the current row. The frame clause makes this explicit.

Cross questions:

● What is the difference between ROWS and RANGE in window frame?


● How would you calculate a running total that resets each month?
● How would you calculate a 7-day rolling sum instead?

Tip: Without ORDER BY inside OVER(), SUM() gives the grand total for every row — a
common mistake.

Q13 [CTE] Top 3 highest paid employees per department.

WITH ranked_employees AS (
SELECT employee_name,
department,
salary,
DENSE_RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS salary_rank
FROM employees
)
SELECT employee_name, department, salary, salary_rank
FROM ranked_employees
WHERE salary_rank <= 3;

Concept: CTE creates a named intermediate result. You cannot use window functions
directly in WHERE — the CTE wraps the window function so you can filter on it.

Cross questions:

● Why can't you use WHERE salary_rank <= 3 without a CTE?


● What is the difference between a CTE and a subquery?
● When would you chain multiple CTEs together?

Tip: Always use CTEs over nested subqueries in interviews. They show structured, readable
thinking.

Q14 [MULTIPLE JOINs] Order details with customer name and product name.

SELECT o.order_id,
c.customer_name,
p.product_name,
[Link],
o.order_date
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN products p ON o.product_id = p.product_id
ORDER BY o.order_date DESC;

Concept: Multiple JOINs chain tables together. Orders is the fact table. Customers and
Products are dimension tables.

Cross questions:

● What is a fact table vs a dimension table?


● How would you detect if a JOIN caused row duplication?
● What is a star schema vs a snowflake schema?

Tip: Always check row counts before and after multiple joins. Fan-out from unexpected
duplicates is a very common real-world bug.

Q15 [DEDUPLICATION] Keep only the most recent row per customer.

WITH ranked_transactions AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY transaction_date DESC
) AS rn
FROM transactions
)
SELECT *
FROM ranked_transactions
WHERE rn = 1;

Concept: PARTITION BY customer_id groups duplicates. ORDER BY transaction_date


DESC puts the most recent row first as rn=1. WHERE rn=1 keeps only the latest.

Cross questions:

● How would you DELETE duplicates from the original table?


● What if you want to keep the oldest record instead?
● How would you find the duplicates before deciding which to keep?

Tip: This ROW_NUMBER deduplication pattern is asked in almost every data engineering
interview. Know it cold.

Q16 [CONDITIONAL AGGREGATION] Total sales and quarterly breakdown in one


row per product

SELECT product_name,
SUM(amount) AS total_sales,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 1
THEN amount ELSE 0 END) AS q1_sales,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 2
THEN amount ELSE 0 END) AS q2_sales,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 3
THEN amount ELSE 0 END) AS q3_sales,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 4
THEN amount ELSE 0 END) AS q4_sales
FROM sales
GROUP BY product_name;

Concept: SUM(CASE WHEN condition THEN value ELSE 0 END) is conditional


aggregation. ELSE 0 ensures non-matching rows contribute 0 not NULL.

Cross questions:

● What is the difference between this and using PIVOT?


● What does COUNT(CASE WHEN...) do differently from SUM(CASE
WHEN...)?
● How would you add a YoY comparison using this technique?

Tip: SUM(CASE WHEN) works in every database. PIVOT syntax varies. Always prefer
CASE WHEN in interviews.

Q17 [FAN-OUT] Demonstrate and fix the fan-out problem.

-- Detect fan-out
SELECT COUNT(*) FROM orders;
SELECT COUNT(*) FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id;
-- If second count is higher -- fan-out!

-- Fix: aggregate BEFORE joining


WITH order_totals AS (
SELECT order_id, SUM(amount) AS total_amount
FROM order_items
GROUP BY order_id
)
SELECT o.order_id, c.customer_name, ot.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_totals ot ON o.order_id = ot.order_id;

Concept: Fan-out occurs when JOIN multiplies rows due to one-to-many relationships. Fix
by pre-aggregating the many-side table before joining.

Cross questions:

● How do you detect fan-out in a result set?


● Why does fan-out cause SUM() to give wrong results?
● What is a many-to-many relationship and how do you handle it?

Tip: Mentioning fan-out proactively in an interview shows you have worked with real data at
scale. It impresses senior interviewers.

Q18 [NTILE] Divide customers into 4 spend quartiles.

SELECT customer_id,
customer_name,
total_spend,
NTILE(4) OVER (ORDER BY total_spend ASC) AS spend_quartile
FROM (
SELECT customer_id, customer_name,
SUM(amount) AS total_spend
FROM orders
GROUP BY customer_id, customer_name
) customer_spend
ORDER BY total_spend DESC;

Concept: NTILE(4) divides customers into 4 equal groups. Quartile 1 = lowest spenders,
Quartile 4 = highest spenders.

Cross questions:

● What is the difference between NTILE and PERCENT_RANK?


● How would you find the exact 90th percentile value?
● How would you label each quartile as Bronze, Silver, Gold, Platinum?

Tip: Combine NTILE with CASE WHEN to assign meaningful tier labels to each bucket.

SET 3 — ADVANCED

Q19 [RECURSIVE CTE] All employees under a given manager — all levels.

WITH RECURSIVE org_hierarchy AS (


-- Anchor: start with the given manager
SELECT employee_id, employee_name, manager_id, 1 AS level
FROM employees
WHERE employee_id = :manager_id

UNION ALL

-- Recursive: find all direct reports of current level


SELECT e.employee_id, e.employee_name, e.manager_id, [Link] + 1
FROM employees e
JOIN org_hierarchy h ON e.manager_id = h.employee_id
)
SELECT employee_id, employee_name, level
FROM org_hierarchy
ORDER BY level, employee_name;
Concept: Anchor query returns the root. UNION ALL combines with recursive member.
Recursion stops when no more employees match. level tracks depth.

Cross questions:

● How do you prevent infinite loops in a recursive CTE?


● What is the maximum recursion depth in your SQL dialect?
● How would you display this as an indented org chart?

Tip: Recursive CTEs are tested in senior data engineering interviews. The org hierarchy
example is the most common use case — practice it.

Q20 [PIVOT] One row per product with monthly sales columns.

SELECT product_name,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 1
THEN amount ELSE 0 END) AS jan,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 2
THEN amount ELSE 0 END) AS feb,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 3
THEN amount ELSE 0 END) AS mar,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 4
THEN amount ELSE 0 END) AS apr,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 5
THEN amount ELSE 0 END) AS may,
SUM(CASE WHEN EXTRACT(MONTH FROM sale_date) = 6
THEN amount ELSE 0 END) AS jun
FROM sales
GROUP BY product_name;

Concept: Conditional aggregation creates one column per month. Works in all databases —
no native PIVOT keyword needed.

Cross questions:

● What is the native PIVOT syntax in SQL Server?


● How would you unpivot — columns back to rows?
● What if the number of months is dynamic?

Tip: Dynamic pivoting requires dynamic SQL. Mention that for truly dynamic cases,
Python/Pandas is more appropriate. Shows business thinking.

Q21 [GAPS AND ISLANDS] Start and end dates of each consecutive login streak.
WITH login_data AS (
SELECT user_id,
login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY login_date
) AS rn
FROM user_logins
),
island_id AS (
SELECT user_id,
login_date,
login_date - INTERVAL '1 day' * rn AS island
FROM login_data
)
SELECT user_id,
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_length
FROM island_id
GROUP BY user_id, island
ORDER BY user_id, streak_start;

Concept: Subtracting row number from date gives the same constant for consecutive days.
A gap causes this constant to change — creating a new island.

Cross questions:

● How would you find users with a streak of 7+ consecutive days?


● What if the table has duplicate entries per day?
● How does this work if dates are not stored as DATE type?

Tip: The date-minus-row-number trick is the most elegant gaps and islands solution.
Interviewers are genuinely impressed when you know it.

Q22 [QUERY OPTIMISATION] 5 techniques for a slow query on 50M rows.

-- 1. Add index on filter/join column


CREATE INDEX idx_orders_date ON orders(order_date);

-- 2. Partition pruning — query with partition key in WHERE


SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';
-- 3. Avoid SELECT * — select only needed columns
SELECT order_id, customer_id, amount
FROM orders WHERE order_date > '2024-01-01';

-- 4. Pre-aggregate before joining


WITH agg AS (
SELECT customer_id, SUM(amount) total
FROM orders GROUP BY customer_id
)
SELECT [Link], [Link]
FROM customers c JOIN agg ON [Link] = agg.customer_id;

-- 5. Check query plan


EXPLAIN ANALYZE SELECT ...;

Concept: Optimisation order — diagnose first with EXPLAIN, then indexes, then query
rewrite. Never jump to solutions before diagnosing.

Cross questions:

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


● When would an index actually slow a query down?
● What is query plan caching and when does it cause problems?

Tip: Always structure your optimisation answer: check the query plan first, then indexes,
then rewrite. This structured approach impresses interviewers.

Q23 [MOVING AVERAGE] 7-day moving average of daily active users.

SELECT activity_date,
daily_active_users,
AVG(daily_active_users) OVER (
ORDER BY activity_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS rolling_7day_avg
FROM dau_metrics
ORDER BY activity_date;

Concept: ROWS BETWEEN 6 PRECEDING AND CURRENT ROW = 7 rows total. AVG
over this window smooths out day-of-week effects.

Cross questions:

● What is the difference between ROWS and RANGE in the frame clause?
● How would you exclude the current row from the average?
● How would you calculate a 30-day moving average?

Tip: Mention that the first 6 days will have partial windows — fewer than 7 days available.
Shows you think about edge cases.

Q24 [FIRST/LAST VALUE] First and most recent product purchased per customer.

SELECT DISTINCT
customer_id,
FIRST_VALUE(product_name) OVER (
PARTITION BY customer_id
ORDER BY purchase_date ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED
FOLLOWING
) AS first_product,
LAST_VALUE(product_name) OVER (
PARTITION BY customer_id
ORDER BY purchase_date ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED
FOLLOWING
) AS last_product
FROM purchases;

Concept: LAST_VALUE needs UNBOUNDED FOLLOWING — without it, the default frame
stops at the current row and gives wrong results.

Cross questions:

● Why does LAST_VALUE give wrong results without the explicit frame?
● How would you use a CTE approach instead?
● What if a customer has only one purchase?

Tip: LAST_VALUE is notoriously tricky because of the default frame. Always specify ROWS
BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

Q25 [FULL OUTER JOIN + COALESCE]Compare revenue across two years


including products in only one year

WITH revenue_2023 AS (
SELECT product_id, SUM(amount) AS rev_2023
FROM sales WHERE EXTRACT(YEAR FROM sale_date) = 2023
GROUP BY product_id
),
revenue_2024 AS (
SELECT product_id, SUM(amount) AS rev_2024
FROM sales WHERE EXTRACT(YEAR FROM sale_date) = 2024
GROUP BY product_id
)
SELECT
COALESCE(a.product_id, b.product_id) AS product_id,
COALESCE(a.rev_2023, 0) AS revenue_2023,
COALESCE(b.rev_2024, 0) AS revenue_2024,
COALESCE(b.rev_2024, 0)
- COALESCE(a.rev_2023, 0) AS yoy_change
FROM revenue_2023 a
FULL OUTER JOIN revenue_2024 b ON a.product_id = b.product_id
ORDER BY yoy_change DESC;

Concept: FULL OUTER JOIN includes products from both years. COALESCE replaces
NULL with 0 for products that exist in only one year.

Cross questions:

● What is the difference between FULL OUTER JOIN, LEFT JOIN and INNER
JOIN?
● When would COALESCE give a different result from ISNULL?
● How would you extend this to compare 3 years?

Tip: FULL OUTER JOIN + COALESCE is the standard YoY comparison pattern. Asked
heavily in finance and e-commerce analytics interviews.

You might also like