Knowing SQL isn't enough to pass a SQL round: interviewers also score how you clarify the problem, reason out loud, check edge cases and talk about performance. This guide gives you a 5-step framework for every question, the phrases to use when stuck, and a full 45-minute mock SQL interview with model answers.

What Interviewers Evaluate

The Interviewer Is Evaluating Two Things Simultaneously

1. CAN you solve the SQL problem?

2. WOULD you be a good engineer to work with?

You can score high on #2 even with an imperfect query — by communicating well.

You can score LOW on #2 with a correct query — by being silent and giving no context.

The goal: narrate your thinking process AS you write the query.

Advertisement

The 5-Step Framework for Every SQL Problem

StepWhat to Say / DoTime
1. ClarifyRestate the problem. Ask about edge cases, NULLs, duplicates, time range, result size.60 sec
2. State approach"I'll use a LEFT JOIN + IS NULL anti-join pattern here." Name the technique.30 sec
3. Write incrementallyStart with FROM + JOIN. Then WHERE. Then SELECT. Then GROUP BY/HAVING.3-5 min
4. Walk throughExplain what each part does with a tiny example. Trace 2-3 rows mentally.1 min
5. Optimise"This runs in O(N log N) due to the sort in RANK(). I could add an index on (dept, salary) to speed this up."1 min

Clarifying Questions to Ask Every Time

Always Ask These Before Writing Any SQL

1. "Should I include or exclude NULL values in the join column?"

2. "What should I return if there is no result? NULL, empty set, or a default value?"

3. "Are there duplicate rows in the input tables? Should I deduplicate first?"

4. "Is this for the current period only or all historical data?"

5. "If two employees tie for the same salary, should I return both or just one?"

6. "Which database are we using? MySQL, PostgreSQL, or SQL Server?" (affects syntax choices)

7. "Can order amounts be NULL? Can they be negative (refunds)?"

Asking these questions shows SENIOR-LEVEL thinking.

Most junior candidates just start writing without clarifying — interviewers notice this.

What to Say When You Are Stuck

Stuck? Say These Things (Do NOT Go Silent)

"Let me think about this for a moment..." (30 seconds of silence is OK)

"I know I need to compare each row against a group aggregate — I'm thinking either a

correlated subquery or a window function. Let me go with the window function approach

since it's more readable and avoids multiple scans."

"I'm getting the right count for most departments but I think my HAVING clause might be

filtering out departments with zero employees. Let me add a LEFT JOIN instead of INNER JOIN."

"I have a working solution but it uses a correlated subquery which is O(N²).

Would you like me to optimize it using a window function?"

"I'd normally test this on sample data — can I trace through 2-3 rows out loud to verify?"

Optimization Discussion — What Interviewers Want to Hear

-- After writing your solution, say:

-- "This query does a full table scan on employees (N rows) and joins to departments.
--  If employee table has 10M rows, I'd add a composite index on (dept_id, salary)
--  to support the PARTITION BY + ORDER BY in the window function."

-- "The RANK() window function sorts within each partition — O(N log N) overall.
--  For a one-time report this is fine. For a live dashboard, I'd materialise
--  this as a scheduled view refreshed every hour."

-- "I used NOT EXISTS instead of NOT IN because the orders.customer_id column
--  allows NULLs and NOT IN with NULLs returns zero rows — a silent bug."

-- "This correlated subquery runs once per row — O(N²) complexity.
--  I can rewrite it as a window function to make it O(N log N)."

Handling Follow-Up Questions

Follow-up QuestionHow to Answer
"Can you do this without a subquery?"Show the JOIN or window function equivalent. Explain the trade-off.
"What if the table has 100 million rows?"Discuss indexes, partitioning, LIMIT early filtering, materialized views.
"What index would you add?"Index on columns in WHERE, JOIN ON, ORDER BY. Explain composite index column order.
"Is this query correct if amounts can be NULL?"Check SUM/AVG (ignore NULLs), COUNT (skips NULLs), COALESCE to handle.
"How would this query change for PostgreSQL?"LIMIT same, FETCH FIRST n ROWS ONLY alternative, ILIKE for case-insensitive, RETURNING in DML.
"What is the time complexity?"JOIN = O(N log N) with index. Full scan = O(N). Nested loop = O(N²). Window sort = O(N log N).
"Can we cache this result?"For reports: materialized view with scheduled refresh. For APIs: Redis cache with TTL.

Common Mistakes That Cost Candidates the Job

Never Do These in a SQL Interview

1. Going silent for more than 2 minutes without saying anything.

2. Writing SELECT * in your solution query (always name specific columns).

3. Using NOT IN without checking for NULLs in the subquery.

4. Forgetting DISTINCT when using INNER JOIN for a semi-join.

5. Using = NULL instead of IS NULL.

6. Not mentioning edge cases unless the interviewer asks.

7. Presenting only one approach without discussing alternatives.

8. Not adding ORDER BY to a "top N" query (result is undefined without it).

9. Hardcoding dates instead of using relative date functions.

10. Forgetting that HAVING operates on GROUPS — writing column conditions in HAVING.

Mock Interview: A Full 45-Minute SQL Round

Instructions

Simulate a real FAANG SQL interview. Set a timer. Write your answers BEFORE looking at solutions.

Format: 5 minutes warm-up → 15 minutes easy → 15 minutes medium → 10 minutes hard.

Scoring: Correct answer (3pts) + Handles edge cases (2pts) + Discusses optimization (2pts) = 7pts max per question.

Pass mark: 28/42 points (67%). FAANG hire bar: 35+/42 (83%).

Warm-Up (5 min): Database Design

Question 1 (5 min)

Design a schema for a food delivery app (like Swiggy/Zomato).

Name the core tables, their primary keys, and the key relationships.

You do NOT need to write full CREATE TABLE statements — just describe the design.

Model Answer

Tables: users (user_id PK), restaurants (restaurant_id PK, location, cuisine),

menu_items (item_id PK, restaurant_id FK, name, price, is_available),

orders (order_id PK, user_id FK, restaurant_id FK, status, placed_at, delivered_at),

order_items (order_id FK, item_id FK, quantity, unit_price — composite PK),

delivery_agents (agent_id PK, name, rating),

deliveries (delivery_id PK, order_id FK UNIQUE, agent_id FK, picked_at, delivered_at)

Relationships: user → orders (1:N), restaurant → orders (1:N),

order → order_items (1:N), order → delivery (1:1), agent → deliveries (1:N)

Bonus points for: SCD Type 2 on menu_items prices, indexing on (restaurant_id, is_available),

ENUM for order status, DECIMAL for prices (not FLOAT).

Easy Questions (15 min): 3 Questions × 5 min

Question 2 (5 min)

Table: orders (order_id, customer_id, amount, status, order_date)

Write a query to find customers who placed more than 3 orders in 2025

AND whose total spending in 2025 exceeded ₹10,000.

Return: customer_id, order_count, total_spent. Sort by total_spent descending.

-- Answer: GROUP BY + HAVING with two conditions
SELECT customer_id,
       COUNT(order_id)   AS order_count,
       SUM(amount)       AS total_spent
FROM orders
WHERE YEAR(order_date) = 2025
  AND status != 'cancelled'       -- edge case: should we count cancelled orders?
GROUP BY customer_id
HAVING COUNT(order_id) > 3
   AND SUM(amount) > 10000
ORDER BY total_spent DESC;

-- Edge cases to mention:
-- 1. Include or exclude cancelled orders? (Ask the interviewer!)
-- 2. Is order_date a DATE or DATETIME? (YEAR() works for both)
-- 3. What if amount can be NULL? (SUM ignores NULLs — is that correct?)
Question 3 (5 min)

Tables: employees (emp_id, name, dept_id, salary, manager_id)

departments (dept_id, dept_name)

Find the name and salary of the highest-paid employee in each department.

If two employees tie for the highest salary in a department, return BOTH.

-- Answer: Window function approach (handles ties correctly)
SELECT e.name, d.dept_name, e.salary
FROM (
    SELECT emp_id, name, dept_id, salary,
           RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk
    FROM employees
) e
JOIN departments d ON e.dept_id = d.dept_id
WHERE rnk = 1
ORDER BY d.dept_name;

-- Why RANK not ROW_NUMBER?
-- ROW_NUMBER gives only 1 result per dept even for ties.
-- RANK gives same rank to ties — both tied employees get rank=1.

-- Alternative: subquery approach
SELECT e.name, d.dept_name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE (e.dept_id, e.salary) IN (
    SELECT dept_id, MAX(salary) FROM employees GROUP BY dept_id
);
Question 4 (5 min)

Table: logins (user_id, login_date)

Find users who logged in on at least 3 CONSECUTIVE days.

Return: user_id, streak_start, streak_end, consecutive_days.

-- Answer: Gaps and Islands — ROW_NUMBER subtraction technique
WITH deduped AS (
    SELECT DISTINCT user_id, login_date FROM logins
),
grouped AS (
    SELECT user_id, login_date,
           DATE_SUB(login_date,
               INTERVAL ROW_NUMBER() OVER (
                   PARTITION BY user_id ORDER BY login_date
               ) DAY) AS grp
    FROM deduped
)
SELECT user_id,
       MIN(login_date) AS streak_start,
       MAX(login_date) AS streak_end,
       COUNT(*)        AS consecutive_days
FROM grouped
GROUP BY user_id, grp
HAVING COUNT(*) >= 3
ORDER BY user_id, streak_start;

Medium Questions (15 min): 2 Questions × 7.5 min

Question 5 (7.5 min)

Tables: orders (order_id, customer_id, order_date, amount)

Write a query to calculate for each month:

- Total revenue

- Previous month revenue

- Month-over-month growth percentage

- Running total revenue (cumulative from start of year)

WITH monthly AS (
    SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
           SUM(amount) AS revenue
    FROM orders
    GROUP BY DATE_FORMAT(order_date, '%Y-%m')
)
SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
    ROUND(
        (revenue - LAG(revenue) OVER (ORDER BY month))
        / NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100,
    2) AS mom_growth_pct,
    SUM(revenue) OVER (
        ORDER BY month
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_revenue
FROM monthly
ORDER BY month;

-- Key points:
-- NULLIF prevents divide-by-zero for first month (prev=NULL)
-- ROWS BETWEEN UNBOUNDED PRECEDING ensures correct running total
-- LAG() called twice — mention could use CTE to compute once for cleanliness
Question 6 (7.5 min)

Tables: user_events (user_id, event_type, event_date)

event_type: 'signup', 'first_purchase', 'second_purchase', 'churned'

Write a funnel analysis query that shows for each month's signup cohort:

- How many users signed up

- How many made their first purchase (conversion rate)

- How many made a second purchase (repeat rate)

- How many churned (churn rate)

WITH cohorts AS (
    SELECT user_id,
           DATE_FORMAT(event_date, '%Y-%m') AS cohort_month
    FROM user_events
    WHERE event_type = 'signup'
)
SELECT
    c.cohort_month,
    COUNT(DISTINCT c.user_id)                   AS signups,
    COUNT(DISTINCT CASE WHEN e.event_type = 'first_purchase'
          THEN e.user_id END)                   AS first_purchase,
    COUNT(DISTINCT CASE WHEN e.event_type = 'second_purchase'
          THEN e.user_id END)                   AS second_purchase,
    COUNT(DISTINCT CASE WHEN e.event_type = 'churned'
          THEN e.user_id END)                   AS churned,
    ROUND(COUNT(DISTINCT CASE WHEN e.event_type = 'first_purchase'
          THEN e.user_id END) * 100.0
          / NULLIF(COUNT(DISTINCT c.user_id), 0), 2) AS conversion_pct,
    ROUND(COUNT(DISTINCT CASE WHEN e.event_type = 'churned'
          THEN e.user_id END) * 100.0
          / NULLIF(COUNT(DISTINCT c.user_id), 0), 2) AS churn_pct
FROM cohorts c
LEFT JOIN user_events e ON c.user_id = e.user_id
GROUP BY c.cohort_month
ORDER BY c.cohort_month;

Hard Question (10 min)

Question 7 (10 min)

Table: transactions (txn_id, account_id, txn_type ['credit'|'debit'], amount, txn_date)

For each account, write a query that shows:

- The running balance after each transaction (in chronological order)

- Flag any transaction that caused the balance to go NEGATIVE

- The date the account first went negative (if ever)

WITH signed_amounts AS (
    -- Convert debit to negative amount
    SELECT txn_id, account_id, txn_date, txn_type, amount,
           CASE WHEN txn_type = 'credit' THEN amount ELSE -amount END AS signed_amt
    FROM transactions
),
running AS (
    SELECT txn_id, account_id, txn_date, txn_type, amount, signed_amt,
           SUM(signed_amt) OVER (
               PARTITION BY account_id
               ORDER BY txn_date, txn_id  -- txn_id breaks ties on same date
               ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
           ) AS running_balance
    FROM signed_amounts
)
SELECT
    txn_id,
    account_id,
    txn_date,
    txn_type,
    amount,
    running_balance,
    CASE WHEN running_balance < 0 THEN '⚠️ OVERDRAFT' ELSE 'OK' END AS status,
    FIRST_VALUE(CASE WHEN running_balance < 0 THEN txn_date END) OVER (
        PARTITION BY account_id
        ORDER BY txn_date, txn_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS first_overdraft_date
FROM running
ORDER BY account_id, txn_date, txn_id;
Scoring Guide

Q1 Design (7pts): Core tables (2), keys & relationships (2), bonus for indexing/SCD (3)

Q2 GROUP+HAVING (7pts): Correct query (3), excluded cancelled (2), NULL handling mentioned (2)

Q3 Rank/Ties (7pts): Correct result (3), handles ties with RANK not ROW_NUMBER (2), alt approach (2)

Q4 Gaps/Islands (7pts): Names the pattern (1), correct grp key (2), HAVING >= 3 (2), dedup (2)

Q5 MoM + Running (7pts): LAG correct (2), NULLIF for divide by zero (2), ROWS BETWEEN (2), readable (1)

Q6 Funnel (7pts): Cohort CTE (2), CASE WHEN counting (2), NULLIF for rates (2), LEFT JOIN (1)

Q7 Running Balance (7pts): Signed amounts (2), ROWS BETWEEN window (2), FIRST_VALUE overdraft (3)

FAQs

How do you prepare for a SQL interview?

Practise the core patterns (joins, aggregation, window functions, CTEs, dates), solve problems out loud with a timer, and rehearse a framework: clarify, plan, write, test edge cases, then discuss optimisation.

What should I ask before writing a SQL query in an interview?

Ask about duplicates and NULLs, how ties should be handled, the expected output format, which dialect is used, and the data size.

What if I get stuck in a SQL interview?

Say what you know, solve a simpler version first, and describe the approach in steps; interviewers reward structured reasoning even when the final query isn't perfect.