SQL interview questions and answers for data analysts, data engineers and data scientists

SQL is the one round almost every data role shares, analysts, scientists, engineers all get tested on it. These are real SQL interview questions asked for data roles, with worked answers: window functions, joins, aggregation, and the gotchas interviewers actually probe.

How the SQL round is run, and what it scores

What the round actually tests

Whether you can express a business question as a query without hesitating, and whether you know what your own query does to the row count. Almost nobody fails a SQL round on syntax. They fail on a join that silently fans out, a filter in the WHERE clause that turns a LEFT JOIN into an INNER one, or an aggregate that quietly drops the rows with NULLs.

The bar rises with seniority in one specific way: a junior candidate is asked to produce the number, a senior candidate is asked what would make the number wrong.

The questions that repeat

Second-highest salary. Running totals and moving averages. Month-over-month retention or cohort tables. Consecutive-day streaks. Top N within each group. Deduplication where you must say which row you kept and why. Median without a median function. These recur across companies with the column names changed.

Most of them are one window function away from trivial, which is why window functions are the single highest-return thing to drill: ROW_NUMBER, RANK and DENSE_RANK and the difference between them, LAG and LEAD, and a frame clause you can write from memory rather than recognize.

What interviewers score

State the grain before you write anything: one row per what. Interviewers watch for this because it is the habit that prevents fan-out. Then the query, then the edge cases you raise unprompted, ties, NULLs, empty groups, duplicate keys.

Say the complexity or the scan pattern if the table is large, and say which index would help. A candidate who writes a correct query and then explains why it would be slow on a billion rows is scored above one who writes the same query and stops.

How to prepare, and in what order

Window functions first, because they carry the most questions. Then joins with an honest understanding of what each one does to cardinality, then aggregation with GROUP BY and HAVING, then date arithmetic, which is where dialect differences bite. CTEs last, since they are readability rather than capability.

Practice out loud and against a clock. The round is usually thirty to forty minutes for two or three problems, and the failure mode is silence: thinking correctly for four minutes while the interviewer cannot tell whether you are stuck. Narrate the grain, the join, then the filter.

Where candidates lose it

Putting a condition on the right table in WHERE instead of ON, which converts a LEFT JOIN into an INNER JOIN and drops exactly the rows the question was about. Using COUNT where COUNT DISTINCT was meant, then reporting line items as orders. Assuming a join key is unique without saying so. And reaching for a subquery where a window function is both shorter and faster, which reads as unfamiliarity rather than preference.

15 SQL questions

11. fact_orders has an order_ts. dim_customer is SCD Type 2 with valid_from/valid_to. Join each order to the customer attributes that were current when the order was placed.

A range join, not an equality join: ON o.customer_id = d.customer_id AND o.order_ts >= d.valid_from AND o.order_ts < d.valid_to. The half-open interval is what stops an order matching two versions on a boundary. Joining on the customer key alone, or filtering d.is_current = true, silently reports today's attributes for historical orders, the classic SCD mistake, and the reason last year's revenue-by-segment number quietly changes.

12. A daily batch job loads into a target table. Make re-running it for the same day safe, with no duplicates.

MERGE on a stable business key, or delete-then-insert scoped to that day's partition inside one transaction. The job has to be idempotent rather than append-only: keyed on something like (entity_id, load_date) so a re-run replaces its own previous output instead of stacking on top of it. INSERT-only pipelines look correct until the first retry, and then every downstream metric double-counts.

Practice this for real