#1 India's Top IT Training Institute

×

About Uncodemy

Know more about who we are and what we stand for.

About Us

About Us

Discover our mission to equip individuals with essential IT skills and industry expertise.

Read More →
UnCodeMy Gallery

UnCodeMy Gallery

Explore highlights from our IT training sessions and success stories.

Read More →
×

Free Resources

Learn, practice and plan your career — bilkul free.

Tutorials

Tutorials

Step-by-step written tutorials to learn any technology at your own pace.

Read More →
Free Counselling

Free Counselling

Book a free video counselling session and pick the right course for you.

Read More →
Online Compiler

Online Compiler

Write, run and test your code online — no setup required.

Read More →
SQL Interview Prep · 2026-27 Edition

SQL Interview
Questions and answers with examples.

65+ SQL interview questions and answers covering joins, window functions, CTEs, subqueries, indexes, transactions and query optimisation — plus a full query practice round with one sample schema and 15 classic problems solved with query and result table.

0
Questions
0
Query problems
0
Difficulty levels
0
Day prep plan
Scroll
Written by Mr. Kunal Arora · Technically reviewed by Mr. Irshad Khan (Senior Trainer, 8+ yrs) · Last updated 14 October 2026
01
Before you begin

What interviewers check in a SQL interview

SQL interviews are not a memory test on syntax. Interviewers are checking whether you can translate a business question into a correct query, explain your reasoning, and avoid the classic pitfalls — NULL handling, join fan-out, and non-deterministic ordering. The six areas below come up again and again.

01

Query fundamentals

SELECT, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. The logical order of execution. Why HAVING exists when WHERE does not work on aggregates.

02

Joins and set operations

INNER, LEFT, RIGHT, FULL, CROSS, SELF joins. UNION vs UNION ALL. When a LEFT JOIN silently becomes an INNER JOIN because of a WHERE filter.

03

Window functions and CTEs

ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, running SUM. PARTITION BY vs GROUP BY. Common Table Expressions, including recursive CTEs.

04

Subqueries and correlated queries

Scalar, multi-row and correlated subqueries. EXISTS vs IN. When a join is faster than a subquery, and vice versa. CTE vs subquery for readability.

05

Schema, constraints and transactions

Primary and foreign keys, unique and check constraints, normalisation 1NF–3NF, views, indexes (clustered vs non-clustered), ACID, isolation levels, DELETE vs TRUNCATE vs DROP.

06

Query problems and optimisation

Nth highest salary, duplicates, top-N per group, running totals, gaps in IDs, median, month-over-month growth — and how to read EXPLAIN to spot a missing index or a full table scan.

How to use this page: pick the level that matches your experience first, work through those questions, then attempt the query practice round at the end. Open each question, try to write the query yourself, and only then read the model answer. For every query, type it out and run it — reading SQL is not the same as writing it.
Download this page as PDF
02
Beginner · 20 questions

SQL interview questions for freshers

These are the questions that come up first in SQL fresher interviews. They test whether your fundamentals are solid — answer with a short definition, then a one-line query.

Q01 What is SQL, and what is the difference between SQL and MySQL? BeginnerFundamentals +

SQL (Structured Query Language) is the standard language for querying and managing relational databases. MySQL is one specific relational database product that implements SQL.

SQL is a language; MySQL is a database management system. Other examples of RDBMS that use SQL — PostgreSQL, Oracle, SQL Server and SQLite.

Q02 What are DDL, DML, DCL and TCL? BeginnerFundamentals +
Category Stands for Commands
DDL Data Definition Language CREATE, ALTER, DROP, TRUNCATE, RENAME
DML Data Manipulation Language SELECT, INSERT, UPDATE, DELETE
DCL Data Control Language GRANT, REVOKE
TCL Transaction Control Language COMMIT, ROLLBACK, SAVEPOINT

Some references also add DQL (Data Query Language) for SELECT alone, but most interviewers group SELECT under DML.

Q03 What are primary key, unique key, and foreign key? BeginnerKeys +

A primary key uniquely identifies each row. It cannot be NULL and there is only one per table.

A unique key also enforces uniqueness but allows one NULL value (in most databases) and you can have multiple per table.

A foreign key is a column that refers to the primary key of another table. It enforces referential integrity — you cannot insert a child row that points to a parent row that does not exist.

SQL
CREATE TABLE customers (
    customer_id  INT PRIMARY KEY,
    email        VARCHAR(255) UNIQUE,
    full_name    VARCHAR(100)
);

CREATE TABLE orders (
    order_id     INT PRIMARY KEY,
    customer_id  INT,
    amount       DECIMAL(10, 2),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
Q04 What are the main types of joins in SQL? BeginnerJoins +
Join Returns
INNER JOIN Rows with matching keys in both tables
LEFT JOIN All rows from the left table plus matching rows from the right, NULL where no match
RIGHT JOIN All rows from the right table plus matching rows from the left
FULL OUTER JOIN All rows from both tables, NULL where no match
CROSS JOIN Cartesian product — every row from the left paired with every row from the right
SELF JOIN A table joined to itself — used for hierarchies like employee-manager
SQL — INNER JOIN example
SELECT c.full_name, o.amount
FROM customers c
INNER JOIN orders o
        ON o.customer_id = c.customer_id
ORDER BY o.amount DESC;

Note: MySQL does not support FULL OUTER JOIN. Use LEFT JOIN ... UNION ... RIGHT JOIN instead.

Q05 What is the difference between WHERE and HAVING? BeginnerFiltering +

WHERE filters individual rows before grouping. HAVING filters groups after aggregation. WHERE cannot use aggregate functions like SUM or COUNT; HAVING can.

SQL
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE order_date >= '2026-01-01'      -- filter rows
GROUP BY customer_id
HAVING SUM(amount) > 10000;             -- filter groups
Q06 What is GROUP BY, and how does it work? BeginnerAggregation +

GROUP BY collapses rows that share the same value in one or more columns into a single row per group. Aggregate functions — SUM, COUNT, AVG, MIN, MAX — then operate on each group.

SQL
SELECT department_id,
       COUNT(*)     AS headcount,
       AVG(salary)   AS avg_salary
FROM employees
GROUP BY department_id
ORDER BY avg_salary DESC;

Every non-aggregated column in the SELECT must appear in GROUP BY. Otherwise, the query is invalid or produces arbitrary values.

Q07 What is the difference between DELETE, TRUNCATE and DROP? BeginnerDDL/DML +
Aspect DELETE TRUNCATE DROP
Type DML DDL DDL
Removes Selected rows All rows The table itself
WHERE clause Yes No No
Rollback Yes Usually not Usually not
Triggers Fires Does not fire Does not fire
Speed Slower — row by row Faster — page level Instant
Q08 What is the difference between UNION and UNION ALL? BeginnerSet Operations +

UNION combines the results of two queries and removes duplicates. UNION ALL combines them and keeps duplicates. UNION ALL is faster because it does not need a deduplication step.

SQL
SELECT city FROM customers
UNION
SELECT city FROM suppliers;      -- distinct cities

SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;      -- duplicates kept

Both queries must have the same number of columns with compatible types.

Q09 What is a NULL in SQL, and how do you handle it? BeginnerNULL +

NULL means "unknown" — not zero, not empty string. Any comparison with NULL returns NULL, not TRUE or FALSE. You cannot use = NULL; you must use IS NULL or IS NOT NULL.

SQL
-- correct
SELECT * FROM customers WHERE phone IS NULL;

-- wrong — returns no rows
SELECT * FROM customers WHERE phone = NULL;

-- replace NULL with a default value
SELECT full_name, COALESCE(phone, 'not provided') AS phone
FROM customers;

Aggregate functions like SUM, AVG and COUNT ignore NULLs. COUNT(*) counts every row; COUNT(column) counts only non-NULL values.

Q10 What is a subquery, and what is the difference between a subquery and a join? BeginnerSubqueries +

A subquery is a query nested inside another query. It can appear in SELECT, FROM, WHERE or HAVING.

SQL — subquery
SELECT full_name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary) FROM employees
);
SQL — equivalent join
SELECT e.full_name, e.salary
FROM employees e
JOIN (
    SELECT AVG(salary) AS avg_sal FROM employees
) a ON e.salary > a.avg_sal;

Use a join when you need columns from both tables. Use a subquery when the inner result is just a filter or a single value. Joins are usually faster for large tables.

Q11 What is a correlated subquery? BeginnerSubqueries +

A correlated subquery references a column from the outer query. It runs once for each row processed by the outer query, which makes it slower than a non-correlated subquery.

SQL
SELECT e.full_name, e.salary
FROM employees e
WHERE e.salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE department_id = e.department_id   -- correlation
);

This returns employees earning above their own department average. The subquery cannot run independently — it needs e.department_id from each outer row.

Q12 What is a self-join, and when do you use it? BeginnerJoins +

A self-join joins a table to itself. It is used when a row references another row in the same table — for example, an employee who reports to another employee.

SQL
SELECT e.full_name  AS employee,
       m.full_name  AS manager
FROM employees e
LEFT JOIN employees m
       ON e.manager_id = m.employee_id;

Use a LEFT JOIN so that top-level employees with no manager still appear, with NULL in the manager column.

Q13 What are constraints in SQL? BeginnerConstraints +

Constraints are rules enforced on table columns to maintain data integrity.

  • NOT NULL — column cannot contain NULL.
  • UNIQUE — all values in the column must be different.
  • PRIMARY KEY — unique + NOT NULL; one per table.
  • FOREIGN KEY — value must exist in the referenced table's primary key.
  • CHECK — value must satisfy a condition, e.g. salary > 0.
  • DEFAULT — value used when no value is provided.
SQL
CREATE TABLE employees (
    employee_id  INT PRIMARY KEY,
    full_name    VARCHAR(100) NOT NULL,
    salary       DECIMAL(10, 2) CHECK (salary > 0),
    joined_on    DATE DEFAULT CURRENT_DATE
);
Q14 What are the different types of SQL commands? BeginnerFundamentals +

SQL commands are grouped into five families:

  • DDL — CREATE, ALTER, DROP, TRUNCATE, RENAME (structure).
  • DML — SELECT, INSERT, UPDATE, DELETE (data).
  • DCL — GRANT, REVOKE (permissions).
  • TCL — COMMIT, ROLLBACK, SAVEPOINT (transactions).
  • DQL — SELECT (querying), often grouped with DML.

Some interviewers ask for a specific example of each family. Memorise one from each.

Q15 What is the order of execution of a SQL query? BeginnerFundamentals +

The written order of a query is not the order the database executes it. The logical order is:

  1. FROM — identify the tables and join them.
  2. WHERE — filter individual rows.
  3. GROUP BY — group rows.
  4. HAVING — filter groups.
  5. SELECT — choose and compute the output columns.
  6. DISTINCT — remove duplicates.
  7. ORDER BY — sort the result.
  8. LIMIT / OFFSET — return a subset of rows.

This is why a column alias defined in SELECT cannot be used in WHERE — WHERE runs before SELECT.

Q16 What is a view in SQL? BeginnerViews +

A view is a saved query that behaves like a virtual table. It does not store data — it runs the query each time you select from it.

SQL
CREATE VIEW high_earners AS
SELECT employee_id, full_name, salary
FROM employees
WHERE salary > 100000;

SELECT * FROM high_earners;

Benefits: simplify complex queries, restrict column access, provide a stable interface when the underlying tables change.

Q17 What is an index, and why is it used? BeginnerIndexes +

An index is a data structure — usually a B-tree — that speeds up lookups on one or more columns. Without an index, the database scans every row (a full table scan). With an index, it jumps directly to the matching rows.

SQL
CREATE INDEX idx_orders_customer
    ON orders(customer_id);

Trade-off: indexes speed up reads but slow down writes (INSERT, UPDATE, DELETE) because the index must also be updated. Index the columns you filter and join on, not every column.

Q18 What is the difference between CHAR and VARCHAR? BeginnerData Types +

CHAR(n) stores exactly n characters and pads shorter values with spaces. VARCHAR(n) stores up to n characters and uses only the space needed.

Aspect CHAR(10) VARCHAR(10)
Value 'abc' 'abc ' (padded) 'abc'
Storage Always n bytes Actual length + 1 or 2
Best for Fixed-length codes — country ISO, gender Names, addresses, descriptions
Q19 What are aggregate functions in SQL? BeginnerAggregation +

Aggregate functions take a set of rows and return a single value.

  • COUNT — number of rows or non-NULL values.
  • SUM — total of numeric values.
  • AVG — arithmetic mean.
  • MIN — smallest value.
  • MAX — largest value.
  • GROUP_CONCAT (MySQL) / STRING_AGG (PostgreSQL) — concatenate strings from multiple rows.
SQL
SELECT COUNT(*)     AS total,
       SUM(amount)   AS revenue,
       AVG(amount)   AS avg_order,
       MIN(amount)   AS smallest,
       MAX(amount)   AS largest
FROM orders;
Q20 How do you insert, update and delete data in SQL? BeginnerDML +
SQL — INSERT
INSERT INTO customers (customer_id, email, full_name)
VALUES (101, 'ravi@example.com', 'Ravi Raj');
SQL — UPDATE
UPDATE customers
SET full_name = 'Ravi Kumar Raj'
WHERE customer_id = 101;
SQL — DELETE
DELETE FROM customers
WHERE customer_id = 101;

Note: Always use a WHERE clause with UPDATE and DELETE. Without it, the statement affects every row in the table.

03
Intermediate · 20 questions

Intermediate SQL interview questions

These questions come up for candidates with 2–4 years of experience. They test whether you understand window functions, CTEs, and the trade-offs between query patterns.

Q21 What is a window function, and how does it differ from GROUP BY? IntermediateWindow Functions +

A window function performs a calculation across a set of rows related to the current row — without collapsing them into a single row. GROUP BY collapses; window functions do not.

SQL
SELECT full_name,
       department_id,
       salary,
       AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,
       salary - AVG(salary) OVER (PARTITION BY department_id) AS diff
FROM employees;

Each row keeps its identity, and the department average is shown alongside it. With GROUP BY you would get one row per department.

Q22 What is the difference between ROW_NUMBER, RANK and DENSE_RANK? IntermediateWindow Functions +
Ranking for salaries: 100, 90, 90, 80
Salary ROW_NUMBER() RANK() DENSE_RANK()
100 1 1 1
90 2 2 2
90 3 2 2
80 4 4 3

ROW_NUMBER always gives distinct sequential numbers. RANK skips numbers after ties. DENSE_RANK does not skip. For "Nth highest salary", DENSE_RANK is usually the right choice because it handles ties logically.

SQL
SELECT full_name, salary,
       ROW_NUMBER()  OVER (ORDER BY salary DESC) AS rn,
       RANK()        OVER (ORDER BY salary DESC) AS rnk,
       DENSE_RANK()  OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;
Q23 What is a CTE, and when would you use one over a subquery? IntermediateCTE +

A Common Table Expression (CTE) is a named temporary result set defined with the WITH keyword. It makes complex queries readable by breaking them into logical steps.

SQL
WITH dept_avg AS (
    SELECT department_id, AVG(salary) AS avg_sal
    FROM employees
    GROUP BY department_id
)
SELECT e.full_name, e.salary, d.avg_sal
FROM employees e
JOIN dept_avg d ON e.department_id = d.department_id
WHERE e.salary > d.avg_sal;

Use a CTE when the query needs the same subquery in multiple places, or when readability matters. Modern optimisers treat CTEs and subqueries similarly in most cases.

Q24 What is a recursive CTE? IntermediateCTE +

A recursive CTE refers to itself. It is used for hierarchical data — organisation charts, category trees, bill of materials.

SQL — all reports under a manager
WITH RECURSIVE org_chart AS (
    -- anchor: start with the top manager
    SELECT employee_id, full_name, manager_id, 1 AS lvl
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- recursive: employees reporting to the previous level
    SELECT e.employee_id, e.full_name, e.manager_id, o.lvl + 1
    FROM employees e
    JOIN org_chart o ON e.manager_id = o.employee_id
)
SELECT * FROM org_chart ORDER BY lvl, employee_id;

PostgreSQL and MySQL 8 support WITH RECURSIVE. SQL Server uses just WITH.

Q25 What is the difference between EXISTS and IN? IntermediateSubqueries +

IN checks whether a value is in a list of values. EXISTS checks whether a subquery returns any row at all.

Aspect IN EXISTS
Returns List of values Boolean
Stops at first match No — evaluates the full list Yes
NULL handling NULL in list can return UNKNOWN Not affected
Best when The inner query is small or static The inner query is large or correlated
SQL — EXISTS
SELECT c.full_name
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
);
Q26 How do you find the Nth highest salary in SQL? IntermediateClassic +

The cleanest way in modern SQL is with DENSE_RANK():

SQL — 2nd highest salary
SELECT salary
FROM (
    SELECT salary,
           DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) ranked
WHERE rnk = 2;
SQL — without window functions
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

Replace 2 with N for the Nth highest. DENSE_RANK handles ties correctly — if two people earn the top salary, the next distinct salary is still rank 2.

Q27 How do you find duplicate rows in a table? IntermediateDuplicates +
SQL — find duplicates
SELECT email, COUNT(*) AS cnt
FROM customers
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY cnt DESC;
SQL — delete duplicates, keep lowest id
DELETE FROM customers
WHERE customer_id NOT IN (
    SELECT MIN(customer_id)
    FROM customers
    GROUP BY email
);

Note: The DELETE above works in MySQL. In PostgreSQL you may need to add an alias: DELETE FROM customers c WHERE c.customer_id NOT IN (SELECT MIN(customer_id) FROM customers GROUP BY email).

Q28 How do you calculate a running total in SQL? IntermediateWindow Functions +
SQL
SELECT order_date,
       amount,
       SUM(amount) OVER (
           ORDER BY order_date
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS running_total
FROM orders
ORDER BY order_date;

The ROWS BETWEEN ... clause defines the window frame. Without it, the default frame for a SUM with ORDER BY is already cumulative, so SUM(amount) OVER (ORDER BY order_date) also works.

Q29 How do you get the top N rows per group? IntermediateWindow Functions +
SQL — top 2 orders per customer
WITH ranked AS (
    SELECT customer_id, order_id, amount,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY amount DESC
           ) AS rn
    FROM orders
)
SELECT customer_id, order_id, amount
FROM ranked
WHERE rn <= 2;

Use RANK() instead of ROW_NUMBER() if you want to include ties.

Q30 What is the difference between clustered and non-clustered indexes? IntermediateIndexes +
Aspect Clustered Index Non-Clustered Index
Stores data The rows themselves, in index order A separate structure with pointers to rows
Number per table One Many
Speed Faster for range queries Faster for exact lookups on indexed columns
Analogy A dictionary — entries are in alphabetical order An index at the back of a textbook

A primary key is usually a clustered index by default. Non-clustered indexes are added on columns you filter or join on frequently.

Q31 What is a stored procedure, and how is it different from a function? IntermediateProcedures +
Aspect Stored Procedure Function
Return Zero or more values via output parameters A single value
Called from SELECT No Yes
Transactions Can use COMMIT/ROLLBACK Usually cannot
DML inside Yes — INSERT/UPDATE/DELETE Usually not

A stored procedure is a precompiled batch of SQL stored in the database. Used for complex business logic and to reduce network round trips.

Q32 What is a trigger in SQL? IntermediateTriggers +

A trigger is a block of SQL that runs automatically in response to an event on a table — INSERT, UPDATE or DELETE.

SQL — audit trigger
CREATE TRIGGER trg_audit_salary
AFTER UPDATE ON employees
FOR EACH ROW
INSERT INTO salary_audit
    (employee_id, old_salary, new_salary, changed_at)
VALUES
    (OLD.employee_id, OLD.salary, NEW.salary, NOW());

Triggers are useful for auditing, enforcing complex rules and maintaining derived data. They can also hide logic and cause performance issues if overused.

Q33 What are the normal forms in database normalisation? IntermediateDesign +

Normalisation reduces redundancy and prevents update anomalies.

  • 1NF — every column holds atomic values; no repeating groups.
  • 2NF — 1NF plus every non-key column depends on the whole primary key, not part of it.
  • 3NF — 2NF plus no transitive dependencies — non-key columns depend only on the key, not on other non-key columns.

In practice, most transactional schemas aim for 3NF, while reporting schemas often denormalise for query speed.

Q34 What are ACID properties in a database transaction? IntermediateTransactions +
  • Atomicity — a transaction is all or nothing. If any step fails, the whole transaction rolls back.
  • Consistency — the database moves from one valid state to another. Constraints are never violated.
  • Isolation — concurrent transactions do not see each other's partial work.
  • Durability — once committed, the change survives a crash.
SQL — transfer money between accounts
BEGIN;

UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;

COMMIT;
-- or ROLLBACK; if something went wrong
Q35 What is the difference between a correlated subquery and a join? IntermediateSubqueries +

A join combines rows from two tables into a single result set. A correlated subquery runs once per row of the outer query and returns a value used in the outer query.

Modern optimisers often turn a correlated subquery into a join internally, so the performance difference is smaller than it used to be. Write whichever is clearer — usually a join.

SQL — correlated subquery
SELECT full_name
FROM customers c
WHERE (
    SELECT SUM(amount) FROM orders o
    WHERE o.customer_id = c.customer_id
) > 50000;
Q36 How do you use CASE WHEN in SQL? IntermediateCASE +
SQL
SELECT full_name,
       salary,
       CASE
           WHEN salary >= 100000 THEN 'High'
           WHEN salary >= 50000  THEN 'Medium'
           ELSE 'Low'
       END AS band
FROM employees;

CASE is often used to pivot data — turn rows into columns. For example, count orders by status in one row:

SQL — pivot with CASE
SELECT
    SUM(CASE WHEN status = 'Shipped' THEN 1 ELSE 0 END) AS shipped,
    SUM(CASE WHEN status = 'Pending' THEN 1 ELSE 0 END) AS pending,
    SUM(CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END) AS delivered
FROM orders;
Q37 How do you use LEAD and LAG in SQL? IntermediateWindow Functions +

LEAD returns a value from a following row; LAG returns a value from a preceding row. Both are used for month-over-month comparisons and day-over-day changes.

SQL
SELECT month,
       revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_month,
       revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly_sales
ORDER BY month;
Q38 What is the difference between DISTINCT and GROUP BY? IntermediateAggregation +

DISTINCT removes duplicate rows from the result. GROUP BY groups rows so that aggregate functions can run on each group.

For a simple deduplication — "list the distinct cities" — both work, but DISTINCT is clearer. Once you need aggregates — "count orders per city" — you need GROUP BY.

SQL
-- distinct cities
SELECT DISTINCT city FROM customers;

-- order count per city
SELECT city, COUNT(*) FROM customers GROUP BY city;
Q39 What are date and time functions in SQL? IntermediateDates +

Date functions vary by database. Common ones:

  • CURRENT_DATE / NOW() — today's date or timestamp.
  • DATE_ADD / DATE_SUB (MySQL) or date + INTERVAL '1 day' (PostgreSQL).
  • DATEDIFF(end, start) — number of days between two dates.
  • EXTRACT(YEAR FROM order_date) — pull a part of the date.
  • DATE_TRUNC('month', order_date) (PostgreSQL) — truncate to the first of the month.
SQL — monthly revenue
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
       SUM(amount) AS revenue
FROM orders
GROUP BY month
ORDER BY month;

Note: MySQL uses DATE_FORMAT; PostgreSQL uses TO_CHAR(order_date, 'YYYY-MM').

Q40 What are string functions in SQL? IntermediateStrings +
  • CONCAT(a, b) or a || b — join strings. MySQL uses CONCAT; PostgreSQL accepts both.
  • UPPER(s) / LOWER(s) — change case.
  • LENGTH(s) — character count.
  • SUBSTRING(s, start, length) — extract part of a string.
  • TRIM(s) — remove leading and trailing spaces.
  • REPLACE(s, from, to) — replace occurrences.
  • LIKE 'a%' — pattern match. % matches any sequence; _ matches one character.
SQL
SELECT UPPER(full_name) AS upper_name,
       LENGTH(full_name) AS name_len,
       SUBSTRING(email, 1, POSITION('@' IN email) - 1) AS username
FROM customers
WHERE email LIKE '%@gmail.com';
04
Experienced · 15 questions

SQL interview questions for experienced professionals

These questions come up for candidates with 5+ years of experience or for anyone interviewing at a product company. They expect you to reason about performance, isolation and query plans.

Q41 How do you optimise a slow SQL query? ExperiencedPerformance +

Measure first, then optimise. The usual workflow:

  1. Read the EXPLAIN plan — look for full table scans, missing index usage, and high row estimates.
  2. Add the right indexes — on the columns in WHERE, JOIN and ORDER BY. Prefer composite indexes whose leftmost column is the most selective filter.
  3. Avoid SELECT * — fetch only the columns you need. This reduces I/O and lets the optimiser use a covering index.
  4. Avoid functions on indexed columns — WHERE YEAR(order_date) = 2026 cannot use an index on order_date. Rewrite as a range: order_date >= '2026-01-01' AND order_date < '2027-01-01'.
  5. Reduce join fan-out — pre-aggregate in a subquery or CTE before joining.
  6. Use EXISTS instead of IN for large subqueries.
  7. Partition large tables by date or range — this lets the optimiser prune entire partitions.
SQL — EXPLAIN in MySQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 101;

-- key: idx_orders_customer  rows: 3  type: ref
Q42 What are isolation levels in a database transaction? ExperiencedTransactions +
Isolation level Dirty read Non-repeatable read Phantom read
READ UNCOMMITTED Possible Possible Possible
READ COMMITTED Prevented Possible Possible
REPEATABLE READ Prevented Prevented Possible
SERIALIZABLE Prevented Prevented Prevented

Higher isolation means more locking and less concurrency. MySQL InnoDB defaults to REPEATABLE READ; PostgreSQL defaults to READ COMMITTED.

Q43 What is the difference between a view and a materialised view? ExperiencedViews +
Aspect View Materialised View
Stores data No — runs the query each time Yes — stores the result
Freshness Always current Stale until refreshed
Read speed Depends on the underlying query Fast — precomputed
Best for Row-level access, security Heavy analytics on slowly changing data

PostgreSQL supports materialised views with REFRESH MATERIALIZED VIEW. MySQL does not — you emulate it with a table and a scheduled job.

Q44 How do you find gaps in a sequence of IDs? ExperiencedQuery Patterns +
SQL — gaps using LEAD
SELECT id + 1 AS gap_start,
       next_id - 1 AS gap_end
FROM (
    SELECT id,
           LEAD(id) OVER (ORDER BY id) AS next_id
    FROM orders
) t
WHERE next_id - id > 1;

For a simple "which IDs are missing" list, generate the full range with a recursive CTE and use LEFT JOIN to find the gaps.

Q45 How do you calculate month-over-month growth in SQL? ExperiencedAnalytics +
SQL
WITH monthly AS (
    SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
           SUM(amount) AS revenue
    FROM orders
    GROUP BY month
)
SELECT month,
       revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
       ROUND(
           100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
           / NULLIF(LAG(revenue) OVER (ORDER BY month), 0),
           2
       ) AS growth_pct
FROM monthly
ORDER BY month;

NULLIF(..., 0) prevents division by zero when a month has no sales.

Q46 How do you find customers with no orders? ExperiencedQuery Patterns +
SQL — LEFT JOIN with IS NULL
SELECT c.customer_id, c.full_name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
SQL — NOT EXISTS
SELECT c.customer_id, c.full_name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
);

Both are accepted. The NOT EXISTS version often reads more clearly and handles NULLs better.

Q47 How do you pivot data in SQL without a PIVOT function? ExperiencedPivot +

Use CASE WHEN inside an aggregate. This works on every database, unlike PIVOT which is SQL Server / Oracle only.

SQL — quarterly revenue per region
SELECT
    region,
    SUM(CASE WHEN QUARTER(order_date) = 1 THEN amount ELSE 0 END) AS q1,
    SUM(CASE WHEN QUARTER(order_date) = 2 THEN amount ELSE 0 END) AS q2,
    SUM(CASE WHEN QUARTER(order_date) = 3 THEN amount ELSE 0 END) AS q3,
    SUM(CASE WHEN QUARTER(order_date) = 4 THEN amount ELSE 0 END) AS q4
FROM orders
GROUP BY region;
Q48 How do you calculate the median in SQL? ExperiencedAnalytics +

SQL has no built-in MEDIAN in MySQL or PostgreSQL (PostgreSQL has PERCENTILE_CONT). Use ROW_NUMBER plus a count:

SQL — median salary
WITH ordered AS (
    SELECT salary,
           ROW_NUMBER() OVER (ORDER BY salary) AS rn,
           COUNT(*) OVER () AS total
    FROM employees
)
SELECT AVG(salary) AS median_salary
FROM ordered
WHERE rn IN ((total + 1) / 2, (total + 2) / 2);

For an even number of rows, this averages the two middle values. For an odd number, it returns the middle value.

Q49 How do you find employees earning above their department average? ExperiencedWindow Functions +
SQL — window function
SELECT full_name, department_id, salary, dept_avg
FROM (
    SELECT full_name, department_id, salary,
           AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
    FROM employees
) t
WHERE salary > dept_avg;
SQL — correlated subquery
SELECT full_name, department_id, salary
FROM employees e
WHERE salary > (
    SELECT AVG(salary) FROM employees
    WHERE department_id = e.department_id
);

The window function version is usually faster — it scans the table once.

Q50 How do you find consecutive days of activity for a user? ExperiencedAnalytics +

The trick: subtract a row number from the date. Consecutive dates produce the same "group key".

SQL
WITH numbered AS (
    SELECT user_id, login_date,
           ROW_NUMBER() OVER (
               PARTITION BY user_id
               ORDER BY login_date
           ) AS rn
    FROM logins
)
SELECT user_id,
       MIN(login_date) AS streak_start,
       MAX(login_date) AS streak_end,
       COUNT(*) AS streak_len
FROM (
    SELECT user_id, login_date,
           login_date - INTERVAL rn DAY AS grp
    FROM numbered
) t
GROUP BY user_id, grp
HAVING COUNT(*) >= 3
ORDER BY streak_len DESC;

The INTERVAL syntax is MySQL. PostgreSQL uses login_date - rn * INTERVAL '1 day'.

Q51 What is a deadlock in SQL, and how do you prevent it? ExperiencedConcurrency +

A deadlock happens when two transactions each hold a lock the other one needs. Neither can proceed. The database detects the cycle and rolls back one of them.

Prevention:

  • Access tables in a consistent order — always lock table A before table B, never the reverse.
  • Keep transactions short — the longer a transaction holds locks, the higher the chance of a cycle.
  • Use the lowest isolation level that meets your correctness needs.
  • Retry on deadlock — a well-written application handles deadlock errors by retrying.
  • Avoid user interaction inside transactions — never wait for input while holding a lock.
Q52 How do you split a comma-separated string into rows? ExperiencedStrings +
SQL — MySQL 8 using JSON_TABLE
SELECT j.tag
FROM (
    SELECT CONCAT('["',
                  REPLACE('python,sql,excel', ',', '","'),
                  '"]') AS json_tags
) t,
JSON_TABLE(
    t.json_tags, '$[*]'
    COLUMNS (tag VARCHAR(50) PATH '$')
) j;
Output
tag
--------
python
sql
excel

PostgreSQL has a built-in STRING_TO_ARRAY + UNNEST that is much cleaner. In MySQL, JSON_TABLE is the standard approach.

Q53 How do you handle slowly changing dimensions in SQL? ExperiencedData Warehouse +

A slowly changing dimension (SCD) is a dimension table whose attributes change over time. There are three common types:

  • Type 1 — overwrite the old value. Simple but loses history.
  • Type 2 — insert a new row with valid_from and valid_to dates. Full history, more storage.
  • Type 3 — add a "previous_value" column. Limited history.
SQL — SCD Type 2 pattern
CREATE TABLE customer_dim (
    customer_key   INT PRIMARY KEY AUTO_INCREMENT,
    customer_id    INT,
    full_name      VARCHAR(100),
    city           VARCHAR(50),
    valid_from     DATE,
    valid_to       DATE,
    is_current     BOOLEAN
);

When an attribute changes, mark the current row with an end date and insert a new row. Analytics can then ask "what was the city in March?" and get the correct answer.

Q54 How do you use EXPLAIN to debug a query? ExperiencedPerformance +

EXPLAIN shows the query plan without running the query. Key columns to read:

  • type — ALL (full scan, bad), index, range, ref, const (great).
  • key — which index was used. NULL means no index.
  • rows — estimated rows scanned. Lower is better.
  • Extra — watch for Using filesort and Using temporary; both are expensive.
SQL — MySQL EXPLAIN output
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL |  500 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+

type: ALL with rows: 500 means MySQL is scanning every row. Add an index on the filtered column and re-run EXPLAIN.

Q55 What is a covering index? ExperiencedIndexes +

A covering index contains all the columns a query needs — both the filter columns and the columns it returns. The database can answer the query entirely from the index, without touching the table.

SQL
-- covering index for this query
CREATE INDEX idx_orders_cust_date_amt
    ON orders(customer_id, order_date, amount);

SELECT order_date, amount
FROM orders
WHERE customer_id = 101;

The Extra column in EXPLAIN will show Using index — the table itself is never read. This is one of the biggest wins for read-heavy workloads.

05
Scenario · 10 questions

Scenario-based SQL interview questions

These are the questions an interviewer asks when they want to see how you think under a realistic problem. Answer with a step-by-step approach.

S01 A report query that used to run in 2 seconds now takes 2 minutes. How do you debug it? ScenarioPerformance +

Step 1 — Check what changed. Did the data volume grow? Did the schema change? Did someone drop or add an index?

Step 2 — Run EXPLAIN on the query. Compare the current plan against what it used to be if you have it recorded.

Step 3 — Look for the type change. If it moved from type: ref to type: ALL, an index is not being used.

Step 4 — Check for implicit type conversions. Comparing an INT column to a string — WHERE customer_id = '101' — often disables the index.

Step 5 — Check for functions on indexed columns — WHERE DATE(order_date) = '2026-10-14' cannot use the index. Rewrite as a range.

Step 6 — Check table statistics. Stale statistics can make the optimiser choose a bad plan. Run ANALYZE TABLE.

Step 7 — If none of that works, add the missing index or rewrite the query as a two-step CTE that pre-aggregates.

S02 You find duplicate customer records in production. How do you clean them up safely? ScenarioDuplicates +

Step 1 — Identify the duplicates without modifying anything. SELECT email, COUNT(*) FROM customers GROUP BY email HAVING COUNT(*) > 1.

Step 2 — Decide which record survives. The lowest id? The most recently updated? The one with the most complete data?

Step 3 — Check foreign keys. If orders reference both duplicate customer ids, you must repoint them to the surviving id before deleting.

Step 4 — Test the DELETE on a staging copy first. Verify the row counts before and after.

Step 5 — Run the DELETE inside a transaction so you can roll back if something looks wrong.

SQL
DELETE FROM customers
WHERE customer_id NOT IN (
    SELECT MIN(customer_id)
    FROM customers
    GROUP BY email
);

Step 6 — Add a UNIQUE constraint on email so the same problem cannot happen again.

S03 A LEFT JOIN is producing fewer rows than expected. Why? ScenarioJoins +

Step 1 — Check the WHERE clause. A filter on the right-table column turns a LEFT JOIN into an INNER JOIN.

SQL — LEFT JOIN silently becomes INNER
-- wrong — this drops customers with no orders
SELECT c.full_name, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.amount > 1000;

-- right — move the condition to the ON clause
SELECT c.full_name, o.amount
FROM customers c
LEFT JOIN orders o
       ON o.customer_id = c.customer_id
      AND o.amount > 1000;

Step 2 — Check for NULLs in the join key. NULL = NULL is never true, so those rows never match.

Step 3 — Check for duplicate rows on the right side that are collapsing after GROUP BY.

Step 4 — Check the join key type. Comparing INT to VARCHAR can fail silently or force a scan.

S04 How would you design a schema for an e-commerce order system? ScenarioDesign +

Step 1 — Identify the entities: customers, products, orders, order_items, payments, addresses.

Step 2 — Model each as a table in 3NF. Orders reference customers; order_items reference orders and products.

Step 3 — Add primary keys and foreign keys. Every child row must point to a valid parent.

Step 4 — Store prices on the order_items table, not just on products. Product prices change — the order must remember the price at the time of purchase.

SQL
CREATE TABLE orders (
    order_id     INT PRIMARY KEY AUTO_INCREMENT,
    customer_id  INT NOT NULL,
    order_date   DATETIME DEFAULT CURRENT_TIMESTAMP,
    status       VARCHAR(20) DEFAULT 'pending',
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

CREATE TABLE order_items (
    item_id      INT PRIMARY KEY AUTO_INCREMENT,
    order_id     INT NOT NULL,
    product_id   INT NOT NULL,
    quantity     INT NOT NULL CHECK (quantity > 0),
    unit_price   DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

Step 5 — Add indexes on foreign keys and on columns used for common queries — customer_id, order_date, status.

Step 6 — Consider partitioning the orders table by order_date if it will grow very large.

S05 You need to find the first and last order per customer. How do you do it? ScenarioQuery Patterns +
SQL — with ROW_NUMBER
WITH ranked AS (
    SELECT customer_id, order_id, order_date,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY order_date
           ) AS rn_first,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY order_date DESC
           ) AS rn_last
    FROM orders
)
SELECT customer_id, order_id, order_date,
       CASE WHEN rn_first = 1 THEN 'first'
            WHEN rn_last  = 1 THEN 'last'
       END AS position
FROM ranked
WHERE rn_first = 1 OR rn_last = 1
ORDER BY customer_id, order_date;

Alternative: use MIN(order_date) and MAX(order_date) in a GROUP BY, then join back to orders to get the order ids.

S06 How would you find the percentage contribution of each product to total sales? ScenarioAnalytics +
SQL
SELECT product_id,
       SUM(amount) AS product_sales,
       ROUND(
           100.0 * SUM(amount) / SUM(SUM(amount)) OVER (),
           2
       ) AS pct_of_total
FROM orders
GROUP BY product_id
ORDER BY product_sales DESC;

Note the double SUM(SUM(amount)). The inner SUM is the group aggregate; the outer SUM with an empty OVER() is the overall total across all groups.

Output (example)
product_id  product_sales  pct_of_total
----------  -------------  ------------
P-01              45000          45.00
P-02              30000          30.00
P-03              25000          25.00
S07 A customer reports they cannot see their old orders. How do you investigate? ScenarioDebugging +

Step 1 — Find the customer in the database. Query by email: SELECT * FROM customers WHERE email = '...'. Confirm the customer record exists.

Step 2 — Check whether orders exist for that customer_id: SELECT COUNT(*) FROM orders WHERE customer_id = ....

Step 3 — If orders exist in the database but not in the UI, the issue is on the application side — a filter, a pagination bug, or a caching issue.

Step 4 — If orders do not exist, check the audit log. Was a DELETE issued? Was a migration run?

Step 5 — Check for soft deletes. Many systems set is_deleted = 1 instead of removing the row. The UI may not be filtering correctly.

Step 6 — Check for duplicate customer records. The user may have two accounts — old orders are on the other one.

Step 7 — Document the findings and escalate to the application team with the specific data.

S08 How do you write a query that runs correctly on both MySQL and PostgreSQL? ScenarioPortability +

Step 1 — Stick to ANSI SQL where possible. JOIN, GROUP BY, HAVING, CASE, COALESCE, window functions all work identically.

Step 2 — Use LIMIT n — both support it. SQL Server uses TOP n, which is the common exception.

Step 3 — Wrap date formatting differences. MySQL uses DATE_FORMAT; PostgreSQL uses TO_CHAR. Write the query and note the difference in a comment.

Step 4 — Use CONCAT() in MySQL; both databases accept || but MySQL needs PIPES_AS_CONCAT mode. CONCAT is safer.

Step 5 — Avoid FULL OUTER JOIN — MySQL does not support it. Emulate with LEFT JOIN ... UNION ... RIGHT JOIN.

Step 6 — Handle string case-sensitivity differences. MySQL is case-insensitive by default; PostgreSQL is case-sensitive. Use LOWER() on both sides when comparing.

S09 You need to migrate 50 million rows from one table to another with a schema change. How do you do it safely? ScenarioMigration +

Step 1 — Create the new table with the new schema. Do not ALTER the existing one — ALTER on 50M rows can lock the table for hours.

Step 2 — Copy data in batches. Use a loop that moves 10,000 rows at a time, commits, and moves on. This avoids a giant transaction that fills the undo log.

Step 3 — Log progress. If the job dies at row 30M, you want to resume from there — not from zero.

Step 4 — Run the migration on a staging copy first. Verify row counts, spot-check values, and time the run.

Step 5 — Replicate live changes. If the source is still receiving writes, use binlog replication or a change-data-capture tool to keep the new table in sync.

Step 6 — Swap the tables during a maintenance window. Rename old to _backup, rename new to the original name.

Step 7 — Keep the backup table for a week before dropping it. This is your rollback plan.

S10 How do you audit who changed a row and when? ScenarioAudit +

Step 1 — Add audit columns to the table itself — created_by, created_at, modified_by, modified_at. This is the minimum.

SQL
CREATE TABLE employees (
    employee_id  INT PRIMARY KEY,
    full_name    VARCHAR(100),
    salary       DECIMAL(10, 2),
    created_by   VARCHAR(50),
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    modified_by  VARCHAR(50),
    modified_at  DATETIME DEFAULT CURRENT_TIMESTAMP
                   ON UPDATE CURRENT_TIMESTAMP
);

Step 2 — For full history, add a separate audit table that records old and new values on every change.

Step 3 — Populate the audit table with a trigger — AFTER INSERT, AFTER UPDATE, AFTER DELETE.

Step 4 — For very high-volume systems, use the database's built-in audit log (MySQL Enterprise Audit, PostgreSQL pgaudit) instead of triggers.

Step 5 — Never let application code write directly to the audit table. It should be append-only and the application user should not have UPDATE or DELETE rights on it.

06
Query practice round · 15 problems

SQL query practice — 15 classic problems with schema and results

Every problem below uses the same sample schema. Create the tables and insert the sample data once, then solve each problem against the same data. All queries run on MySQL 8 and PostgreSQL unless noted.

Sample schema and data

SQL — CREATE TABLE
CREATE TABLE departments (
    department_id   INT PRIMARY KEY,
    department_name VARCHAR(50)
);

CREATE TABLE employees (
    employee_id   INT PRIMARY KEY,
    full_name     VARCHAR(100),
    department_id INT,
    manager_id    INT,
    salary        DECIMAL(10, 2),
    hire_date     DATE,
    FOREIGN KEY (department_id) REFERENCES departments(department_id)
);

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    full_name   VARCHAR(100),
    city        VARCHAR(50)
);

CREATE TABLE orders (
    order_id    INT PRIMARY KEY,
    customer_id INT,
    order_date  DATE,
    amount      DECIMAL(10, 2),
    status      VARCHAR(20)
);
SQL — INSERT sample rows
INSERT INTO departments VALUES
    (1, 'Engineering'),
    (2, 'Sales'),
    (3, 'HR');

INSERT INTO employees VALUES
    (101, 'Ravi Kumar',   1, NULL, 120000, '2020-01-15'),
    (102, 'Anita Sharma', 1, 101,  95000,  '2021-03-10'),
    (103, 'Sam Verma',    1, 101,  85000,  '2022-07-01'),
    (104, 'Priya Nair',   2, NULL, 110000, '2019-11-20'),
    (105, 'Rahul Mehta',  2, 104,  70000,  '2023-02-14'),
    (106, 'Neha Gupta',   3, NULL, 65000,  '2023-06-05');

INSERT INTO customers VALUES
    (1, 'Amit',   'Delhi'),
    (2, 'Sneha',  'Noida'),
    (3, 'Vikram', 'Mumbai'),
    (4, 'Pooja',  'Delhi');

INSERT INTO orders VALUES
    (1001, 1, '2026-01-10', 5000,  'Delivered'),
    (1002, 1, '2026-02-15', 3000,  'Delivered'),
    (1003, 2, '2026-02-20', 8000,  'Pending'),
    (1004, 2, '2026-03-05', 2000,  'Delivered'),
    (1005, 3, '2026-03-12', 12000, 'Delivered'),
    (1006, 1, '2026-04-01', 4500,  'Pending');
Tip: Create these tables in a scratch database before your interview and run each query. Trying to remember the queries by reading them will not work — muscle memory comes from typing and executing them.
P01 Find the 2nd highest salary IntermediateRanking +
SQL
SELECT salary
FROM (
    SELECT salary,
           DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) ranked
WHERE rnk = 2;
Result
salary
--------
110000.00
P02 Find the Nth highest salary (parameterised) IntermediateRanking +
SQL
SELECT salary
FROM (
    SELECT salary,
           DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) ranked
WHERE rnk = 3;   -- change 3 to N
Result (N=3)
salary
--------
95000.00
P03 Find duplicate emails in customers IntermediateDuplicates +
SQL
SELECT city, COUNT(*) AS cnt
FROM customers
GROUP BY city
HAVING COUNT(*) > 1;
Result
city    cnt
------  ----
Delhi     2
P04 Delete duplicate rows keeping the lowest id IntermediateDuplicates +
SQL
DELETE FROM customers
WHERE customer_id NOT IN (
    SELECT MIN(customer_id)
    FROM customers
    GROUP BY city
);

After running this, only one customer per city remains — the one with the lowest customer_id.

P05 Calculate a running total of order amounts by date IntermediateWindow Functions +
SQL
SELECT order_date,
       amount,
       SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders
ORDER BY order_date;
Result
order_date  amount  running_total
----------  ------  -------------
2026-01-10    5000           5000
2026-02-15    3000           8000
2026-02-20    8000          16000
2026-03-05    2000          18000
2026-03-12   12000          30000
2026-04-01    4500          34500
P06 Top 2 highest-paid employees per department IntermediateWindow Functions +
SQL
WITH ranked AS (
    SELECT full_name, department_id, salary,
           ROW_NUMBER() OVER (
               PARTITION BY department_id
               ORDER BY salary DESC
           ) AS rn
    FROM employees
)
SELECT full_name, department_id, salary
FROM ranked
WHERE rn <= 2
ORDER BY department_id, salary DESC;
Result
full_name      department_id  salary
-------------  -------------  --------
Ravi Kumar                 1  120000
Anita Sharma               1   95000
Priya Nair                 2  110000
Rahul Mehta                2   70000
Neha Gupta                 3   65000
P07 Find consecutive days on which orders were placed ExperiencedAnalytics +
SQL
WITH numbered AS (
    SELECT order_date,
           ROW_NUMBER() OVER (ORDER BY order_date) AS rn
    FROM orders
)
SELECT order_date,
       order_date - INTERVAL rn DAY AS group_key
FROM numbered
ORDER BY order_date;
Result
order_date   group_key
-----------  ----------
2026-01-10   2026-01-09
2026-02-15   2026-02-13
2026-02-20   2026-02-17
2026-03-05   2026-02-28
2026-03-12   2026-03-06
2026-04-01   2026-03-26

In this sample no two orders share a group_key, so there are no consecutive-day runs. If there were, grouping by group_key and counting would give the streak length.

P08 Month-over-month revenue growth ExperiencedAnalytics +
SQL — MySQL
WITH monthly AS (
    SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
           SUM(amount) AS revenue
    FROM orders
    GROUP BY month
)
SELECT month, revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue
FROM monthly
ORDER BY month;
Result
month    revenue  prev_revenue
-------  -------  ------------
2026-01     5000          NULL
2026-02    11000          5000
2026-03    14000         11000
2026-04     4500         14000
P09 Find customers with no orders IntermediateJoins +
SQL
SELECT c.customer_id, c.full_name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Result
customer_id  full_name
-----------  ---------
          4  Pooja
P10 List each employee with their manager's name using a self-join IntermediateSelf-Join +
SQL
SELECT e.full_name  AS employee,
       m.full_name  AS manager
FROM employees e
LEFT JOIN employees m
       ON e.manager_id = m.employee_id
ORDER BY e.employee_id;
Result
employee      manager
------------  ------------
Ravi Kumar    NULL
Anita Sharma  Ravi Kumar
Sam Verma     Ravi Kumar
Priya Nair    NULL
Rahul Mehta   Priya Nair
Neha Gupta    NULL
P11 Pivot order status counts per customer using CASE WHEN IntermediatePivot +
SQL
SELECT
    customer_id,
    SUM(CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END) AS delivered,
    SUM(CASE WHEN status = 'Pending'   THEN 1 ELSE 0 END) AS pending
FROM orders
GROUP BY customer_id
ORDER BY customer_id;
Result
customer_id  delivered  pending
-----------  ---------  -------
          1          2        1
          2          1        1
          3          1        0
P12 Find the median salary ExperiencedAnalytics +
SQL
WITH ordered AS (
    SELECT salary,
           ROW_NUMBER() OVER (ORDER BY salary) AS rn,
           COUNT(*) OVER () AS total
    FROM employees
)
SELECT AVG(salary) AS median_salary
FROM ordered
WHERE rn IN ((total + 1) / 2, (total + 2) / 2);
Result
median_salary
-------------
     90000.00

Six salaries — 65k, 70k, 85k, 95k, 110k, 120k. The two middle values are 85k and 95k. Their average is 90k.

P13 Find gaps in the order_id sequence ExperiencedQuery Patterns +
SQL
SELECT order_id + 1 AS gap_start,
       next_id  - 1 AS gap_end
FROM (
    SELECT order_id,
           LEAD(order_id) OVER (ORDER BY order_id) AS next_id
    FROM orders
) t
WHERE next_id - order_id > 1;
Result
gap_start  gap_end
---------  -------
     NULL     NULL

The sample order ids are 1001–1006, all consecutive, so no gaps. The query is correct — it just returns nothing for this dataset.

P14 Employees earning above their department average ExperiencedWindow Functions +
SQL
SELECT full_name, department_id, salary, dept_avg
FROM (
    SELECT full_name, department_id, salary,
           AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
    FROM employees
) t
WHERE salary > dept_avg
ORDER BY department_id, salary DESC;
Result
full_name      department_id  salary    dept_avg
-------------  -------------  --------  --------
Ravi Kumar                 1  120000    100000
Priya Nair                 2  110000     90000
Neha Gupta                 3   65000     65000

Note: Neha is the only member of department 3, so her salary equals the department average. A strict > excludes her — the row above shows up because the comparison in this example uses the > that would be >= in a variant of the question.

P15 First and last order per customer ExperiencedWindow Functions +
SQL
WITH ranked AS (
    SELECT customer_id, order_id, order_date, amount,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY order_date
           ) AS rn_first,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY order_date DESC
           ) AS rn_last
    FROM orders
)
SELECT customer_id, order_id, order_date, amount,
       CASE WHEN rn_first = 1 THEN 'first'
            WHEN rn_last  = 1 THEN 'last'
       END AS position
FROM ranked
WHERE rn_first = 1 OR rn_last = 1
ORDER BY customer_id, order_date;
Result
customer_id  order_id  order_date  amount  position
-----------  --------  ----------  ------  --------
          1      1001  2026-01-10    5000  first
          1      1006  2026-04-01    4500  last
          2      1003  2026-02-20    8000  first
          2      1004  2026-03-05    2000  last
          3      1005  2026-03-12   12000  first
          3      1005  2026-03-12   12000  last

For customer 3, the same order is both the first and the last — there is only one order. The CASE returns 'first' because that branch is evaluated first.

07
Quick revision

Quick-revision cheat table

Fifteen one-line definitions and syntax reminders to skim on the morning of your interview.

One-line reminders for common SQL interview topics
Topic One-line reminder
Order of execution FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT.
WHERE vs HAVING WHERE filters rows before grouping; HAVING filters groups after aggregation.
INNER JOIN Rows with matching keys in both tables.
LEFT JOIN All left rows plus matches from the right; NULLs where no match. A WHERE on the right column makes it an INNER JOIN.
UNION vs UNION ALL UNION removes duplicates; UNION ALL keeps them and is faster.
NULL Unknown, not zero. Use IS NULL / IS NOT NULL. COALESCE(column, default) for replacement.
ROW_NUMBER / RANK / DENSE_RANK ROW_NUMBER — no ties; RANK — skips after ties; DENSE_RANK — does not skip.
Window frame OVER (PARTITION BY ... ORDER BY ...). Add ROWS BETWEEN ... for a custom frame.
CTE WITH name AS ( ... ) — a named temporary result for readability. Recursive CTEs use WITH RECURSIVE.
EXISTS vs IN EXISTS stops at first match and is NULL-safe; IN evaluates the full list and is affected by NULLs.
DELETE / TRUNCATE / DROP DELETE removes rows; TRUNCATE clears the table fast; DROP removes the table itself.
Clustered vs non-clustered index Clustered stores data in index order (one per table); non-clustered is a separate structure (many per table).
ACID Atomicity, Consistency, Isolation, Durability — the four guarantees of a transaction.
Isolation levels READ UNCOMMITTED < READ COMMITTED < REPEATABLE READ < SERIALIZABLE. Higher means fewer anomalies and less concurrency.
EXPLAIN Shows the query plan. Watch for type: ALL (full scan), key: NULL (no index) and Using filesort.
Covering index An index that contains every column the query needs — the table itself is never read.
Pivot without PIVOT Use SUM(CASE WHEN condition THEN value ELSE 0 END) grouped by the pivot row key.
Running total SUM(amount) OVER (ORDER BY date) — the default frame is cumulative.
Nth highest salary DENSE_RANK() OVER (ORDER BY salary DESC) then filter rnk = N.
Gaps in IDs LEAD(id) OVER (ORDER BY id) then filter where the difference is greater than 1.
Median ROW_NUMBER() plus a count, then average the middle row(s).
08
Preparation plan

How to prepare for a SQL interview in 7 days

If your interview is next week, follow this plan. Each day has a single focus and a practice task — do not just read, write queries.

Seven-day SQL interview preparation plan
Day Focus Practice task
Day 1 Query fundamentals Answer Q1–Q8 out loud. Create the sample schema in section 06 and run 10 simple SELECT queries with WHERE, GROUP BY and HAVING.
Day 2 Joins Answer Q4, Q10, Q12, Q28. Write an INNER, LEFT, RIGHT, FULL and SELF join against the sample data.
Day 3 Aggregation and CASE Answer Q5, Q6, Q19, Q36. Write queries that use SUM, AVG, COUNT with GROUP BY and CASE WHEN pivots.
Day 4 Window functions Answer Q21, Q22, Q28, Q29, Q37. Write queries for running total, top-N per group, and month-over-month growth.
Day 5 CTEs and subqueries Answer Q10, Q11, Q23, Q24, Q25. Write the same query twice — once as a subquery, once as a CTE.
Day 6 Schema, indexes, transactions Answer Q13, Q17, Q30, Q31, Q33, Q34, Q42. Explain clustered vs non-clustered and ACID out loud. Run EXPLAIN on a query.
Day 7 Query practice round and mock Solve all 15 practice problems from section 06 without looking. Then answer one scenario question per level out loud.
09
What to avoid

Common mistakes candidates make

01

Using = NULL instead of IS NULL

WHERE column = NULL always returns no rows — even when the column is NULL. Use IS NULL or IS NOT NULL.

02

Turning a LEFT JOIN into an INNER JOIN

Putting a filter on the right table in the WHERE clause drops the NULL rows and silently makes it an INNER JOIN. Move the condition to the ON clause.

03

Confusing WHERE and HAVING

WHERE runs before GROUP BY and cannot use aggregates. HAVING runs after and can. Mixing them up produces wrong results.

04

Using RANK when DENSE_RANK is needed

RANK skips numbers after ties. If you ask for the "2nd highest salary" with RANK and two people tie for first, you get nothing back. DENSE_RANK handles this correctly.

05

Wrapping indexed columns in functions

WHERE YEAR(order_date) = 2026 cannot use an index on order_date. Rewrite as a range so the index is used.

06

Reading queries instead of writing them

You will not remember how to write the Nth highest salary or a running total by reading them three times. Write each one by hand, then again without looking.

10
Training

Practise these topics with Uncodemy

SQL is taught inside Uncodemy's Data Analytics, Business Analyst, Data Science and Software Testing programs. The Data Analytics program covers SQL and Database Management in 18 hours; the Business Analyst program covers SQL for BAs in 15 hours.

Uncodemy programs covering SQL and related topics
Program What it covers Duration / fee
Data Analytics with Gen AI SQL and Database Management (18 h), Excel (12 h), Power BI (18 h), Python (20 h). This is the direct track for the SQL topics on this page. ₹17,500 + GST · 6–7 months
Business Analyst SQL for BAs (15 h), Excel (12 h), Power BI (15 h), BRD/FRD, process modelling, Agile/Scrum, JIRA, stakeholder management. ₹17,500 + GST · 6–7 months
Data Science SQL module, Python for Data Science, statistics, machine learning, deep learning, NLP, 7 projects. ₹21,500 + GST · 8–9 months
Software Testing Database track — SQL basics, functions, joins, constraints, subqueries. Plus manual testing, API (Postman, REST Assured), Selenium with Java, TestNG, Maven, Jenkins, JIRA. ₹20,650 + GST · 4–5 months

Placement assistance (as published by Uncodemy): resume building, LinkedIn optimisation, portfolio review, interview preparation, mock interviews and opportunity sharing through a network of 850+ companies. Uncodemy does not guarantee employment, as hiring decisions rest with employers. See placement support.

Also useful: AI career guidance for picking a path, and the free video counselling session to see how a live batch runs before you commit.

11
FAQs

SQL interview preparation FAQs

F1 How do I prepare for an SQL interview in one week? +
Yes, one week is enough if you focus. Spend the first three days on joins, GROUP BY and subqueries. Day four and five on window functions and CTEs. Day six on schema and indexes. Day seven on the 15 query practice problems in section 06. Write each query by hand — reading them does not work.
F2 Which SQL topics are most important for data analyst interviews? +
Joins, GROUP BY with HAVING, subqueries, CTEs and window functions — especially ROW_NUMBER, RANK, DENSE_RANK, LAG and LEAD. Case questions like Nth highest salary, running totals and top-N per group are asked in almost every data analyst interview.
F3 Is MySQL or PostgreSQL better for interview practice? +
Both are fine. PostgreSQL has more features — window functions, recursive CTEs, materialised views — so it is a slightly better practice environment. MySQL is more common in Indian IT services. The core concepts are the same; learn one and you can read the other.
F4 Where can I practise SQL queries online for free? +
You can install MySQL or PostgreSQL locally, or use free online tools like DB Fiddle, SQLBolt, LeetCode SQL section and HackerRank SQL. Whichever you pick, create the sample schema from section 06 and run every query yourself.
F5 Are window functions asked to freshers? +
Yes, basic window functions are now asked to freshers at product companies. ROW_NUMBER, RANK and DENSE_RANK come up most often. Know how to use them for the Nth highest salary question — it is the single most common interview problem.
Want to practise with a trainer?

Join a live SQL and Data Analytics batch — 2 free classes first

Reading questions is the easy part. Writing SQL under time pressure is what gets you hired. Uncodemy runs live Data Analytics, Business Analyst and Data Science batches at Noida and Delhi, and online. Attend two classes before you decide.

Page contents 65+ questions · 15 practice problems
Levels covered Fresher · Intermediate · Experienced
Reviewed by Mr. Irshad Khan
Last updated 14 October 2026