Data, backend & analytics candidates

SQL Interview Questions

SQL interviews test whether you can read a problem, pick the right join, aggregate correctly, and reason about performance, not just recite syntax. Each question below pairs a short explanation with the edge case and follow-up interviewers ask, then you can drill it in a scored MockWise interview.

  • Joins to window functions
  • Edge cases & performance
  • Practice as a scored interview

How to use this hub

  • Every entry pairs the answer anatomy (explanation, why asked, misconception, trade-off, follow-up) with runnable-shaped code. Read it, then say the reasoning out loud at 60 to 90 seconds without the page open.
  • SQL rounds grade the sequence, not the syntax: grain first, NULL semantics stated, then the query. Practice narrating those two sentences before any SELECT.
  • Difficulty tags mark ladder position: clear Beginner entries before touching Advanced; the follow-up, not the first answer, is what defines the level.
  • Close each session by re-running your worst question cold. SQL habits (dedupe, ties, ORDER BY in a frame) decay without deliberate repetition.

What SQL rounds grade, by stage of the loop

Screen and OA

Joins, GROUP BY, HAVING, DISTINCT and dedup, one window function. Speed and correctness under partial specs; the trap is always grain. A join that fans rows out silently makes every later aggregate wrong.

Interview loop

Cohorts and retention, funnels, running totals, top-N with ties, EXISTS-vs-IN semantics, NULL and three-valued-logic traps. Interviewers ask for the plan before the query and listen for tie and boundary handling unprompted.

Senior and staff

Performance reasoning: what the index serves, when a covering index wins, window vs self-join at 10M rows, how the plan changes with skew. The grade is knowing which measurement you would take before which rewrite.

The three sentences interviewers listen for

  • “The grain of this result is one row per X.” Stated before writing anything; grain bugs are the most common single failure in live SQL.
  • “NULLs and ties behave like this in my answer.” The exact semantics of the construct you chose, not a general apology about NULLs.
  • “At 10M rows this part is the cost, and this is how I would verify it.” Index, plan, or EXPLAIN named with confidence and restraint.

Example questions and ideal structure

Beginner

What is the difference between WHERE and HAVING?

WHERE filters rows before grouping; HAVING filters groups after aggregation. Cut rows early with WHERE, then filter aggregate results like COUNT or SUM with HAVING.

SELECT dept, COUNT(*) AS c
FROM emp
WHERE status = 'active'
GROUP BY dept
HAVING COUNT(*) > 5;

Common mistake: You can put COUNT() in WHERE. Aggregate filters belong in HAVING.

Trade-off: Filter with WHERE whenever possible so the grouping step touches fewer rows.

Follow-up to expect: Which runs first, and why does order matter for performance?

Beginner

INNER JOIN vs LEFT JOIN: which rows come back?

INNER JOIN returns only matched rows from both tables; LEFT JOIN returns every left-table row with NULLs where the right side has no match.

SELECT o.id, p.name
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id;

Common mistake: A LEFT JOIN quietly turns into an inner join if you filter the right table on a non-NULL value in WHERE. Move that test into ON.

Trade-off: Use LEFT JOIN to surface missing matches; use INNER JOIN to require them.

Follow-up to expect: Find orders with no payment: which join and filter do you use?

Intermediate

How do NULLs affect COUNT, AVG, and comparisons?

Aggregates (except COUNT(*)) skip NULLs, so AVG and SUM ignore missing values. Any comparison to NULL yields unknown, not true, so those rows fail a WHERE filter.

SELECT COUNT(*) AS n_all, COUNT(col) AS n_valued, AVG(col)
FROM t
WHERE col <> 5;   -- NULL rows are excluded here too

Common mistake: COUNT(col) equals COUNT(*). It does not when col is NULL. And col = NULL is never true; use col IS NULL.

Trade-off: Decide deliberately whether NULLs should count; COALESCE to a sentinel only when that is the real intent.

Follow-up to expect: How would you also keep the NULL rows when filtering col <> 5?

Intermediate

How do you find the second-highest salary, handling ties?

The MAX(salary) < MAX trick misses duplicates and empty groups. A window function ranks distinct salaries so ties share a rank; DENSE_RANK returns the correct second-highest value with ties.

SELECT salary FROM (
  SELECT salary,
         DENSE_RANK() OVER (ORDER BY salary DESC) AS r
  FROM emp
) x WHERE r = 2;

Common mistake: MAX-of-two-MAXs is a clean second place. It is silently wrong on duplicate salaries and when no row matches.

Trade-off: Use DENSE_RANK for the second-highest distinct value, ROW_NUMBER for the second row.

Follow-up to expect: Now return the second-highest salary per department.

Intermediate

What does SELECT DISTINCT do, and what does it cost?

DISTINCT removes duplicate result rows by building a sort or hash set of them. That is correct, but it can add a hidden dedup step a plain projection avoids.

Common mistake: DISTINCT "fixes" duplicate rows from a bad join. The real fix is the join grain, not hiding rows.

Trade-off: Prefer fixing the JOIN; use EXISTS for a semi-join instead of DISTINCT to test membership.

Follow-up to expect: You used DISTINCT to hide fan-out. How do you remove the duplication instead?

Advanced

How would you write a running total per customer?

A windowed aggregate with a frame gives a running sum with no self-join: SUM over the partition ordered by day accumulates rows up to the current one.

SELECT customer_id, day,
  SUM(amount) OVER (
    PARTITION BY customer_id
    ORDER BY day
  ) AS running_total
FROM orders;

Common mistake: Dropping ORDER BY inside OVER still totals per row. Without it every row shows the whole-partition total.

Trade-off: Window functions beat correlated subqueries and self-joins for readability and often performance.

Follow-up to expect: Turn this into a 7-day moving average. How does the frame change?

Advanced

Why does WHERE col = NULL return nothing, and how do you test it?

SQL uses three-valued logic (true, false, unknown). Comparing to NULL yields unknown, not true, so the row is filtered out. Use IS NULL / IS NOT NULL, and watch NOT IN with NULLs.

SELECT * FROM t WHERE col IS NULL;    -- correct
SELECT * FROM t WHERE col = NULL;     -- returns nothing

Common mistake: col <> 5 keeps NULL rows. It does not, because unknown fails the filter.

Trade-off: Prefer NOT EXISTS over NOT IN when the subquery can return NULLs, to avoid silently empty results.

Follow-up to expect: A NOT IN whose subquery returns one NULL yields nothing. Why?

Intermediate

A table has duplicate rows for the same key. Find them, then delete all but one.

Two queries, one interview: find duplicates by key with HAVING COUNT(*) > 1 (grain stated first), then dedupe with ROW_NUMBER over (PARTITION BY key ORDER BY a tie-stable column) and delete rows with rn > 1, or keep the min(id). The grade lives in choosing what “one row” means.

WITH ranked AS (
  SELECT id, ROW_NUMBER() OVER (
    PARTITION BY email ORDER BY created_at, id
  ) rn FROM users
)
DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1);

Why it matters: Real data is duplicated more often than it is missing; every analytics and backend loop touches it eventually.

Common mistake: That DISTINCT on the fly fixes the root cause, and that the dedupe is safe without a transaction, a backup, or at least a SELECT of what will be deleted.

Trade-off: Dedupe-at-write (unique index) vs dedupe-at-read (view/row_number) vs batch cleanup. Name the one you would ship and what each costs.

Follow-up to expect: Why does the ORDER BY inside the window need a unique tiebreaker?

Beginner

UNION vs UNION ALL: when does the difference decide your query?

UNION dedupes by sorting or hashing the combined result (an extra pass, unknown-cost) and collapses intentional duplicates; UNION ALL concatenates. Analytics queries that counted distinct events twice lose to the dedupe cost; report queries that need duplicate collapse lose to correctness.

Why it matters: It is a one-line question that opens the discussion the interviewer actually wants: do you know what your result grain and duplicates are?

Common mistake: Defaulting to UNION for safety. Hiding duplicates is not handling them; and the mirror error of UNION ALL on two branchy joins that double-count.

Trade-off: If you need both dedupe and performance, the answer is often GROUP BY key explicitly. Visible, optimizable, and explainable.

Follow-up to expect: Two branches, 3M rows each, 10 duplicates. Which do you pick and why is the answer not always UNION?

Advanced

Write weekly signup-cohort retention for 8 weeks.

Two-step modeling: assign each user a signup-week cohort and each activity an event-week number, then join on user and GROUP BY (cohort, week) with COUNT(DISTINCT user). The classic self-join-free shape is a fact join to a cohort dimension.

WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('week', signup_at) AS cw
  FROM users
),
acts AS (
  SELECT user_id, DATE_TRUNC('week', seen_at) AS ew
  FROM sessions GROUP BY 1, 2
)
SELECT c.cw, a.ew - c.cw AS wk,
       COUNT(DISTINCT a.user_id) AS retained
FROM cohorts c JOIN acts a USING (user_id)
GROUP BY 1, 2;

Why it matters: The retention question is where analyst loops verify you think in grains and date functions, not in copy-pasted window snippets.

Common mistake: Counting events instead of distinct users (one churned power-user becomes 40% of the row), and letting date-truncation timezone assumptions shift cohorts silently.

Trade-off: The matrix wants zeros for no-activity cells. Cross-join cohorts x weeks as the frame or let the BI layer backfill; say which and why.

Follow-up to expect: Now add a rolling 28-day active version. What changes in the join?

How to prepare

  • State the join grain and NULL semantics out loud before writing SQL. Interviewers score the reasoning, not the syntax.
  • For top-N and running totals, reach for window functions and be ready to compare them with a self-join.
  • Drill the reasoning gap in a MockWise interview; the report flags where ties, NULLs, or ordering bite you.
  • Every dedupe or retention question hides a grain decision. Say what one row of your result means before you compute it.
  • Read plans, not opinions: name the one index and the one EXPLAIN output you would check for your own query.

FAQs

Do I need to run these queries to prepare?

Reading helps, but MockWise scores how you explain choices and edge cases out loud, closer to a real SQL round than filling a blank editor.

Is this dialect-specific?

The patterns use standard SQL and window functions available in PostgreSQL and MySQL 8+. Syntax details vary by engine.

How much performance depth for a DS or analyst loop?

Less than engineering loops but nonzero: know why an index helps your predicate, why a window usually beats a correlated subquery, and what a row explosion does to a join. Name one EXPLAIN observation and you are above the bar.

Can I practice SQL on MockWise without a resume?

Yes. Pick the technical type and paste the target JD with a line like “weight SQL reasoning.” The follow-ups still probe grain, ties, and plans, which is the drill.

Ready to practice?

Practice your answers in a mock interview, then review the transcript-based feedback.

Related practice

Last updated: 2026-09-03 · Questions? Contact support · Security