SQL Interview Questions for Data Engineers - Real Scenarios, Real Answers

Master SQL the way interviewers actually ask it. This course covers the most commonly tested SQL concepts for data engineer and data analyst roles, from window functions and CTEs to joins, aggregations, query optimisation, and real-world data reconciliation scenarios. Every question is written the way a real interviewer frames it, with a structured thinking approach before any code, clean solutions across SQL Server, PostgreSQL, and MySQL, and sharp answers to every follow-up question. No textbook theory. No syntax dumps. Just the patterns that actually come up in interviews at product companies, startups, and data-first teams.

data engineeringsqlpysparkscenario basedproblem solving

Finding Repeat Customers

The Interviewer Says

We have an orders table from our e-commerce platform. The business team wants to identify loyal customers, specifically, customers who have placed more than one order. For each of those customers, I want to see their total spend and a collected list of all products they purchased. Walk me through how you would solve this.

Input Table: orders

order_idcustomer_idproductamountorder_date
1001C01Laptop750002026-01-05
1002C02Phone250002026-01-05
1003C01Headphones30002026-01-06
1004C03Laptop750002026-01-07
1005C02Tablet300002026-01-08
1006C03Charger15002026-01-08

How to Handle This in the Interview

Before writing anything, think like this: Let me break this into steps before I write the query.

Step 1: Understand the filter condition

  • More than one order means we need to group by customer and count rows. Only customers with COUNT > 1 qualify.

Step 2: Identify the aggregations needed

  • Total spend = SUM(amount). Product list = STRING_AGG(product, ', ') in SQL Server or ARRAY_AGG(product) in PostgreSQL. Both require GROUP BY on customer_id.

Step 3: Know where to filter

  • You cannot use WHERE with aggregate functions, WHERE runs before grouping happens. You need HAVING COUNT(*) > 1 which runs after grouping.

Step 4: Ask about edge cases

  • Say: Should the product list include duplicates if the same product was ordered twice, or only distinct values? This shows you think beyond the happy path.

Solution

SQL Server

SELECT customer_id, SUM(amount) AS total_spend, STRING_AGG(product, ', ') AS products_bought FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 ORDER BY customer_id;

PostgreSQL

SELECT customer_id, SUM(amount) AS total_spend, ARRAY_AGG(product) AS products_bought FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 ORDER BY customer_id;

MySQL 8+

SELECT customer_id, SUM(amount) AS total_spend, JSON_ARRAYAGG(product) AS products_bought FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 ORDER BY customer_id;

What to say: I am using HAVING instead of WHERE because HAVING filters after aggregation. WHERE cannot reference COUNT values, it runs before any grouping happens. Also I would clarify with the interviewer which database we are using so I can pick the right function for collecting the product list.

Expected Output

customer_idtotal_spendproducts_bought
C0178000Laptop, Headphones
C0255000Phone, Tablet
C0376500Laptop, Charger

Follow-up Questions & Answers

Q1. What is the difference between WHERE and HAVING?

WHERE filters rows before any grouping or aggregation happens, it works on individual row values. HAVING filters after GROUP BY so it can reference aggregated values like COUNT, SUM, or AVG. A common mistake is writing WHERE COUNT(*) > 1 which throws an error because COUNT does not exist yet at the WHERE stage. The mental model is: WHERE runs first, then GROUP BY, then HAVING, in that exact order.

Q2. What is the difference between STRING_AGG and GROUP_CONCAT? When would you use each?

STRING_AGG is available in MSSQL (2017+) and PostgreSQL, it concatenates values into a single string with a separator you define. GROUP_CONCAT is the MySQL equivalent. Both do the same job, the only difference is syntax and which platform supports them. In MSSQL you write STRING_AGG(product, ', '). If downstream processing needs to work with individual items as an array, PostgreSQL's ARRAY_AGG is a better choice. But if you just need a readable comma-separated list for a report or output, STRING_AGG is the cleanest option in MSSQL.

Q3. What happens if amount has NULL values, how does SUM handle it?

SQL ignores NULLs in SUM by default, a NULL amount is simply skipped rather than making the total NULL. However it is better practice to handle it explicitly to make intent clear and guard against data quality issues.

SELECT customer_id, SUM(COALESCE(amount, 0)) AS total_spend, STRING_AGG(product, ', ') AS products_bought FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 ORDER BY customer_id;

What to say: COALESCE replaces any NULL amount with 0 before SUM runs. I would also ask the interviewer whether orders with a NULL amount should be included in the product list at all or filtered out entirely, that changes the approach.

Q4. Can you rewrite this using a subquery instead of HAVING?

Yes, (this is a common alternative interviewers ask for to test knowledge of subqueries.)

SELECT customer_id, SUM(amount) AS total_spend, STRING_AGG(product, ', ') AS products_bought FROM orders WHERE customer_id IN ( SELECT customer_id FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 ) GROUP BY customer_id ORDER BY customer_id;

What to say: Both give the same result but the subquery version scans the table twice, once to find qualifying customers and once to aggregate. The HAVING version does it in a single pass which is more efficient. I prefer HAVING here but it is good to know both.

Q5. If this table had hundreds of millions of rows, how would you optimise this query?

What to say: At that scale I would think about indexing, partitioning, and avoiding full table scans.

Index on customer_id to speed up GROUP BY and lookups

CREATE INDEX idx_orders_customer ON orders(customer_id); -- In MSSQL, partition the table by order_date -- so queries can skip irrelevant date ranges CREATE PARTITION FUNCTION pf_order_date (DATE) AS RANGE RIGHT FOR VALUES ( '2024-01-01', '2025-01-01', '2026-01-01' ); CREATE PARTITION SCHEME ps_order_date AS PARTITION pf_order_date ALL TO ([PRIMARY]);

What to say: Beyond indexing and partitioning, I would push any date filters as early as possible so the engine scans fewer rows before grouping. If this aggregation feeds a dashboard that runs frequently, I would consider a materialised summary table, in MSSQL that means an indexed view or a scheduled stored procedure that writes results to a summary table, rather than running the full aggregation every single time.