← Blog
INTERVIEW PREP — 30 Q&A — INDIA 2026

SQL Interview Questions for Data Analysts
India 2026 — 30 Q&A from Beginner to Advanced

SQL is the #1 technical filter in Indian data analyst interviews. This guide covers every pattern tested at IT companies, startups, and MNCs — from basic SELECT to window functions, CTEs, and real sessionisation problems — with written answers and runnable SQL.

Basic SELECT, FilteringJOINsGROUP BY, AggregationsWindow FunctionsCTEsPerformance, NULL Handling
BeginnerIntermediateAdvanced— difficulty levels, as tested in Indian interviews

Basic SELECT, Filtering & Sorting

1Write a query to find all employees in the "Sales" department whose salary is above ₹50,000.Beginner

Use WHERE with AND to combine conditions. Always filter before sorting.

SELECT employee_id, name, salary
FROM employees
WHERE department = 'Sales'
  AND salary > 50000
ORDER BY salary DESC;
2What is the difference between WHERE and HAVING?Beginner

WHERE filters rows before aggregation — it operates on individual rows. HAVING filters groups after GROUP BY has been applied — it operates on aggregated values. You cannot use aggregate functions (SUM, COUNT, AVG) in a WHERE clause.

-- WHERE: filters rows before grouping
SELECT department, COUNT(*) AS emp_count
FROM employees
WHERE status = 'Active'        -- filters individual rows first
GROUP BY department
HAVING COUNT(*) > 5;           -- filters groups after aggregation
3Find all orders placed in the month of March 2025.Beginner

Use date functions to extract month and year, or use BETWEEN with date literals. BETWEEN is inclusive on both ends.

-- Option 1: date functions
SELECT * FROM orders
WHERE MONTH(order_date) = 3
  AND YEAR(order_date) = 2025;

-- Option 2: BETWEEN (more portable)
SELECT * FROM orders
WHERE order_date BETWEEN '2025-03-01' AND '2025-03-31';
4What does DISTINCT do and when should you use it?Beginner

DISTINCT removes duplicate rows from the result set. Use it when you need unique values — e.g., list of unique cities where customers are located. Avoid using SELECT DISTINCT as a lazy fix for bad JOINs producing duplicates — fix the JOIN instead.

SELECT DISTINCT city FROM customers;
-- Returns one row per unique city
5Write a query using LIKE to find all product names that start with "Pro".Beginner

% matches any sequence of characters. _ matches exactly one character.

SELECT product_name FROM products
WHERE product_name LIKE 'Pro%';

-- Ends with 'Plus': WHERE product_name LIKE '%Plus'
-- Contains 'Pro': WHERE product_name LIKE '%Pro%'

JOINs

1Explain INNER JOIN, LEFT JOIN, RIGHT JOIN, and FULL OUTER JOIN with a real example.Intermediate

INNER JOIN returns rows where there is a match in BOTH tables. LEFT JOIN returns all rows from the left table and matched rows from the right (NULLs where no match). RIGHT JOIN is the mirror. FULL OUTER JOIN returns all rows from both tables — NULLs where there is no match on either side.

-- Customers and their orders (some customers have no orders)
SELECT c.name, o.order_id, o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;
-- Customers with no orders appear with NULL order_id and amount
2Find customers who have NEVER placed an order.Intermediate

This is a classic LEFT JOIN + IS NULL pattern — one of the most commonly asked JOIN questions in Indian interviews.

SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
3What is a SELF JOIN? Write a query to find employees and their managers.Intermediate

A self join joins a table to itself by aliasing it with two different names. Used for hierarchical data where a foreign key references the same table's primary key.

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
-- LEFT JOIN ensures employees with no manager (CEO) also appear
4Find products that were sold in 2024 but NOT in 2025.Advanced

Use LEFT JOIN with IS NULL, or use EXCEPT (NOT IN with a subquery also works but is slower on large tables).

-- Method 1: LEFT JOIN + IS NULL (recommended)
SELECT DISTINCT s24.product_id
FROM sales s24
LEFT JOIN sales s25
  ON s24.product_id = s25.product_id
  AND YEAR(s25.sale_date) = 2025
WHERE YEAR(s24.sale_date) = 2024
  AND s25.product_id IS NULL;

-- Method 2: EXCEPT
SELECT product_id FROM sales WHERE YEAR(sale_date) = 2024
EXCEPT
SELECT product_id FROM sales WHERE YEAR(sale_date) = 2025;
5Write a query to get the total sales per product along with the product category name from a separate categories table.Advanced

A standard multi-table JOIN with aggregation — tests whether the candidate can combine JOIN and GROUP BY correctly.

SELECT c.category_name,
       p.product_name,
       SUM(s.amount) AS total_sales
FROM sales s
JOIN products p ON s.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
GROUP BY c.category_name, p.product_name
ORDER BY total_sales DESC;

GROUP BY, Aggregations & Subqueries

1Find the department with the highest average salary.Intermediate

Aggregate first, then use ORDER BY + LIMIT (or TOP in SQL Server). Do not use a subquery here — it is slower and unnecessary.

SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC
LIMIT 1;          -- use TOP 1 in SQL Server
2Find all customers who placed more than 3 orders.Intermediate

COUNT in GROUP BY with HAVING — the HAVING filter runs after aggregation.

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 3
ORDER BY order_count DESC;
3Find the second highest salary from the employees table.Intermediate

Classic interview question — multiple valid approaches. The subquery approach works on all databases. In SQL Server, use OFFSET-FETCH. With window functions, use DENSE_RANK.

-- Approach 1: Subquery (universal)
SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

-- Approach 2: DENSE_RANK (preferred — handles ties correctly)
SELECT salary FROM (
  SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
) t
WHERE rnk = 2;
4Find duplicate records in a table based on email address.Intermediate

Group by the duplicate column and count — any group with COUNT > 1 is a duplicate.

SELECT email, COUNT(*) AS occurrences
FROM customers
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY occurrences DESC;

-- To see the full rows for duplicates:
SELECT * FROM customers
WHERE email IN (
  SELECT email FROM customers
  GROUP BY email HAVING COUNT(*) > 1
);
5Find the top 3 products by sales amount in each category.Advanced

"Top N per group" is one of the most asked advanced SQL patterns in India. Use ROW_NUMBER() or RANK() with PARTITION BY category.

SELECT category_id, product_id, total_sales, rnk
FROM (
  SELECT category_id,
         product_id,
         SUM(amount) AS total_sales,
         RANK() OVER (
           PARTITION BY category_id
           ORDER BY SUM(amount) DESC
         ) AS rnk
  FROM sales
  GROUP BY category_id, product_id
) ranked
WHERE rnk <= 3
ORDER BY category_id, rnk;

Window Functions

1What is the difference between ROW_NUMBER(), RANK(), and DENSE_RANK()?Intermediate

All three assign a sequential number within a window, but differ on how ties are handled. ROW_NUMBER gives unique numbers even to ties (arbitrary order for ties). RANK skips numbers after ties (1,2,2,4). DENSE_RANK never skips (1,2,2,3).

SELECT name, salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
  RANK()       OVER (ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;
-- For two employees both earning 80000:
-- row_num: 1,2 | rnk: 1,1 (next=3) | dense_rnk: 1,1 (next=2)
2Calculate a running total of sales by date.Intermediate

SUM() OVER with ORDER BY creates a cumulative (running) total — very common in financial and e-commerce analytics.

SELECT sale_date,
       amount,
       SUM(amount) OVER (
         ORDER BY sale_date
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_total
FROM daily_sales
ORDER BY sale_date;
3Calculate the month-over-month revenue change using LAG.Intermediate

LAG() returns the value from a previous row in the partition. LEAD() does the same for the next row. Essential for period-over-period comparisons.

SELECT month,
       revenue,
       LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
       revenue - LAG(revenue, 1) OVER (ORDER BY month) AS mom_change
FROM monthly_sales;
4Assign each customer to a sales quartile (Q1 to Q4) based on their total spend.Advanced

NTILE(n) divides rows into n equal buckets. Use NTILE(4) for quartiles.

SELECT customer_id,
       total_spend,
       NTILE(4) OVER (ORDER BY total_spend) AS quartile
FROM (
  SELECT customer_id, SUM(amount) AS total_spend
  FROM orders
  GROUP BY customer_id
) spend_summary;
5De-duplicate a table — keep only the most recent record per customer.Advanced

ROW_NUMBER() with PARTITION BY customer and ORDER BY date DESC assigns 1 to the newest record per customer. Filter for row_num = 1.

WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC
    ) AS rn
  FROM customers_raw
)
SELECT * FROM ranked WHERE rn = 1;
6Calculate the 3-day moving average of daily sales.Advanced

Use ROWS BETWEEN to define a rolling window of the current row plus 2 preceding rows.

SELECT sale_date,
       amount,
       AVG(amount) OVER (
         ORDER BY sale_date
         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ) AS moving_avg_3day
FROM daily_sales;

CTEs & Subqueries

1What is a CTE? How is it different from a subquery?Intermediate

A CTE (Common Table Expression) is a named temporary result set defined with WITH. It is identical in performance to a subquery in most databases, but far more readable for multi-step queries. CTEs can also be recursive (for hierarchical data). Prefer CTEs over nested subqueries when the query has more than 2 steps.

WITH high_value_customers AS (
  SELECT customer_id, SUM(amount) AS total_spend
  FROM orders
  GROUP BY customer_id
  HAVING SUM(amount) > 100000
)
SELECT c.name, h.total_spend
FROM customers c
JOIN high_value_customers h ON c.customer_id = h.customer_id;
2Find the cities where average order value is above the national average.Intermediate

Requires a subquery or CTE to first compute the national average, then compare city averages against it.

WITH city_avg AS (
  SELECT city,
         AVG(amount) AS avg_order_value
  FROM orders o
  JOIN customers c ON o.customer_id = c.customer_id
  GROUP BY city
),
national_avg AS (
  SELECT AVG(amount) AS nat_avg FROM orders
)
SELECT ca.city, ca.avg_order_value
FROM city_avg ca, national_avg na
WHERE ca.avg_order_value > na.nat_avg
ORDER BY ca.avg_order_value DESC;
3Write a recursive CTE to display all levels of an employee hierarchy.Advanced

Recursive CTEs have two parts: the anchor (base case) and the recursive member joined to the CTE itself. Used for tree structures like org charts, category trees, and bill of materials.

WITH RECURSIVE org_chart AS (
  -- Anchor: top-level employees (no manager)
  SELECT employee_id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive: join employees to their manager row
  SELECT e.employee_id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.employee_id
)
SELECT level, name FROM org_chart
ORDER BY level, name;
4Find customers who placed orders in every month of 2024.Advanced

Count distinct months per customer and compare to 12 (all months of 2024).

SELECT customer_id
FROM orders
WHERE YEAR(order_date) = 2024
GROUP BY customer_id
HAVING COUNT(DISTINCT MONTH(order_date)) = 12;

Performance, NULL Handling & Real Scenarios

1How do NULLs behave in SQL? What is the result of NULL = NULL?Intermediate

NULL represents an unknown value. Any comparison with NULL using = returns NULL (not TRUE or FALSE) — NULL = NULL is NULL. Use IS NULL and IS NOT NULL to check for NULLs. In aggregate functions, NULLs are ignored (SUM, AVG, COUNT(*) counts NULLs but COUNT(column) does not). In JOINs, NULLs never match.

-- WRONG: WHERE manager_id = NULL   → returns nothing
-- RIGHT:
SELECT * FROM employees WHERE manager_id IS NULL;

-- COALESCE replaces NULL with a default value
SELECT name, COALESCE(phone, 'No phone') AS phone
FROM customers;
2What is the difference between DELETE, TRUNCATE, and DROP?Intermediate

DELETE removes specific rows (filterable with WHERE), is logged and can be rolled back. TRUNCATE removes all rows instantly, is minimally logged, cannot be rolled back in most databases, and resets identity columns. DROP removes the entire table including its structure. Use DELETE for selective removal, TRUNCATE to empty a table entirely, DROP only when removing the table permanently.

3A query is running slowly. What steps do you take to optimise it?Advanced

(1) Use EXPLAIN / EXPLAIN ANALYZE to see the query plan and identify full table scans. (2) Check if the columns in WHERE, JOIN ON, and ORDER BY have indexes — add them if not. (3) Avoid using functions on indexed columns in WHERE (e.g., WHERE YEAR(date) = 2024 cannot use an index on date — use a range filter instead). (4) Replace correlated subqueries with JOINs. (5) Use CTEs or temp tables to materialise expensive intermediate results. (6) Avoid SELECT * — retrieve only the columns you need.

-- Slow: function on indexed column prevents index use
WHERE YEAR(order_date) = 2025

-- Fast: range filter uses the index
WHERE order_date >= '2025-01-01'
  AND order_date <  '2026-01-01'
4Write a query to pivot monthly sales into columns (Jan, Feb, Mar … Dec) for each product.Advanced

Pivoting uses conditional aggregation — SUM with CASE WHEN for each month. Most databases do not have a native PIVOT operator; this pattern works everywhere.

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,
  -- ... repeat for all 12 months
  SUM(CASE WHEN MONTH(sale_date) = 12 THEN amount ELSE 0 END) AS Dec
FROM sales
WHERE YEAR(sale_date) = 2025
GROUP BY product_id;
5Find sessions where the gap between consecutive events for the same user is more than 30 minutes (sessionisation).Advanced

Use LAG() to get the previous event timestamp, compute the gap, then use a CTE to assign session IDs. Classic e-commerce / product analytics question.

WITH event_gaps AS (
  SELECT user_id, event_time,
    LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS prev_time,
    DATEDIFF(MINUTE,
      LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time),
      event_time
    ) AS gap_minutes
  FROM events
),
session_flags AS (
  SELECT *,
    CASE WHEN gap_minutes > 30 OR gap_minutes IS NULL THEN 1 ELSE 0 END AS new_session
  FROM event_gaps
)
SELECT user_id, event_time, SUM(new_session) OVER (
  PARTITION BY user_id ORDER BY event_time
) AS session_id
FROM session_flags;

Frequently Asked Questions

What SQL topics are tested in data analyst interviews in India?

Indian data analyst interviews consistently test: JOINs (INNER, LEFT, RIGHT, FULL OUTER — often with tricky NULL behaviour), GROUP BY with HAVING, subqueries and CTEs, window functions (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM OVER PARTITION BY), and practical business scenarios like "find the second highest salary", "find customers who ordered in both periods", or "find duplicate records". Window functions are the most commonly tested advanced topic at mid-level companies.

How should I prepare for the SQL round in an Indian data analyst interview?

Prepare by: (1) mastering all JOIN types and practising with NULL-heavy datasets where LEFT JOIN behaviour is non-obvious; (2) writing at least 20 GROUP BY + HAVING queries from scratch; (3) learning window functions — especially ROW_NUMBER() for deduplication and LAG/LEAD for period-over-period comparisons; (4) practising CTEs for multi-step problems; (5) solving real interview problems like "find top N per group", "running total", and "customers who never ordered". Most Indian SQL rounds are 2–4 practical questions in 30–45 minutes.

What is the difference between RANK and DENSE_RANK in SQL?

Both RANK() and DENSE_RANK() are window functions that assign a rank to each row within a partition. The difference is how they handle ties. RANK() leaves gaps after ties — if two rows tie for rank 2, the next rank is 4 (skipping 3). DENSE_RANK() does not leave gaps — if two rows tie for rank 2, the next rank is 3. Use DENSE_RANK() when you want continuous ranking without gaps, which is the common requirement for "find the Nth highest salary" problems.

Is SQL enough to get a data analyst job in India in 2026?

SQL is necessary but not sufficient. Strong SQL skill gets you past the technical screening round at most Indian companies, but you also need: Excel (for basic data work and business communication), Power BI or Tableau (for visualisation — Power BI dominates in India), and increasingly Python basics (for data cleaning at scale). SQL is the #1 technical filter — companies use it to eliminate candidates who cannot do the job — but Power BI is what you will actually use most often once hired.

EVIKA ACADEMY · NOIDA SECTOR 51 · SQL TRAINING + INTERVIEW PREP

Write SQL that clears the technical round

Live SQL training on real datasets. Mock interview rounds included. Free demo first.

Book Free Demo →