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.
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.
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.
Window functions and CTEs
ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, running SUM. PARTITION BY vs GROUP BY. Common Table Expressions, including recursive CTEs.
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.
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.
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.
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? +
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.
select = SELECT).
Table and column names depend on the database — MySQL on Linux is
case-sensitive for table names by default, PostgreSQL folds unquoted
identifiers to lowercase.
Q02 What are DDL, DML, DCL and TCL? +
| 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? +
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.
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? +
| 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 |
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? +
WHERE filters individual rows before grouping. HAVING filters groups after aggregation. WHERE cannot use aggregate functions like SUM or COUNT; HAVING can.
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? +
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.
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? +
| 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? +
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.
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? +
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.
-- 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? +
A subquery is a query nested inside another query. It can appear in SELECT, FROM, WHERE or HAVING.
SELECT full_name, salary
FROM employees
WHERE salary > (
SELECT AVG(salary) FROM employees
);
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? +
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.
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? +
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.
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? +
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.
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? +
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? +
The written order of a query is not the order the database executes it. The logical order is:
- FROM — identify the tables and join them.
- WHERE — filter individual rows.
- GROUP BY — group rows.
- HAVING — filter groups.
- SELECT — choose and compute the output columns.
- DISTINCT — remove duplicates.
- ORDER BY — sort the result.
- 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? +
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.
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? +
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.
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? +
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? +
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.
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? +
INSERT INTO customers (customer_id, email, full_name)
VALUES (101, 'ravi@example.com', 'Ravi Raj');
UPDATE customers
SET full_name = 'Ravi Kumar Raj'
WHERE customer_id = 101;
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.
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? +
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.
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? +
| 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.
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? +
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.
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? +
A recursive CTE refers to itself. It is used for hierarchical data — organisation charts, category trees, bill of materials.
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? +
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 |
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? +
The cleanest way in modern SQL is with
DENSE_RANK():
SELECT salary
FROM (
SELECT salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 2;
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? +
SELECT email, COUNT(*) AS cnt
FROM customers
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY cnt DESC;
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? +
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? +
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? +
| 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? +
| 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? +
A trigger is a block of SQL that runs automatically in response to an event on a table — INSERT, UPDATE or DELETE.
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? +
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? +
- 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.
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? +
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.
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? +
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:
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? +
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.
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? +
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.
-- 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? +
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.
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? +
- 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.
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';
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? +
Measure first, then optimise. The usual workflow:
- Read the EXPLAIN plan — look for full table scans, missing index usage, and high row estimates.
- Add the right indexes — on the columns in WHERE, JOIN and ORDER BY. Prefer composite indexes whose leftmost column is the most selective filter.
- Avoid SELECT * — fetch only the columns you need. This reduces I/O and lets the optimiser use a covering index.
- Avoid functions on indexed columns —
WHERE YEAR(order_date) = 2026cannot use an index onorder_date. Rewrite as a range:order_date >= '2026-01-01' AND order_date < '2027-01-01'. - Reduce join fan-out — pre-aggregate in a subquery or CTE before joining.
- Use EXISTS instead of IN for large subqueries.
- Partition large tables by date or range — this lets the optimiser prune entire partitions.
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? +
| 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? +
| 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? +
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? +
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? +
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;
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? +
Use CASE WHEN inside an
aggregate. This works on every database,
unlike PIVOT which is
SQL Server / Oracle only.
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? +
SQL has no built-in MEDIAN in MySQL or
PostgreSQL (PostgreSQL has
PERCENTILE_CONT). Use
ROW_NUMBER plus a count:
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? +
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;
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? +
The trick: subtract a row number from the date. Consecutive dates produce the same "group key".
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? +
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? +
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;
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? +
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.
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? +
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.
NULLmeans no index. - rows — estimated rows scanned. Lower is better.
- Extra — watch for
Using filesortandUsing temporary; both are expensive.
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| 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? +
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.
-- 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.
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? +
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? +
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.
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? +
Step 1 — Check the WHERE clause. A filter on the right-table column turns a LEFT JOIN into an INNER JOIN.
-- 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? +
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.
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? +
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? +
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.
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? +
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? +
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? +
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? +
Step 1 — Add audit columns to the table itself — created_by,
created_at, modified_by, modified_at. This is
the minimum.
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.
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
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)
);
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');
P01 Find the 2nd highest salary +
SELECT salary
FROM (
SELECT salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 2;
salary -------- 110000.00
P02 Find the Nth highest salary (parameterised) +
SELECT salary
FROM (
SELECT salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 3; -- change 3 to N
salary -------- 95000.00
P03 Find duplicate emails in customers +
SELECT city, COUNT(*) AS cnt
FROM customers
GROUP BY city
HAVING COUNT(*) > 1;
city cnt ------ ---- Delhi 2
P04 Delete duplicate rows keeping the lowest id +
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 +
SELECT order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders
ORDER BY order_date;
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 +
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;
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 +
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;
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 +
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;
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 +
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;
customer_id full_name
----------- ---------
4 Pooja
P10 List each employee with their manager's name using a self-join +
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;
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 +
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;
customer_id delivered pending
----------- --------- -------
1 2 1
2 1 1
3 1 0
P12 Find the 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);
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 +
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;
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 +
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;
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 +
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;
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.
Quick-revision cheat table
Fifteen one-line definitions and syntax reminders to skim on the morning of your interview.
| 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). |
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.
| 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. |
Common mistakes candidates make
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.
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.
Confusing WHERE and HAVING
WHERE runs before GROUP BY and cannot use aggregates. HAVING runs after and can. Mixing them up produces wrong results.
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.
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.
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.
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.
| 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.