Interview Prep I — SQL & Coding
The technical screen is where most data-engineering candidates get filtered out — and it's also the most drillable part of the whole process. The questions repeat. Once you've seen the dozen-or-so patterns that show up again and again, the live SQL round stops being scary and starts being a checklist. This chapter is a workout: read the patterns, then do the drills with the solutions hidden until you've tried. Type every query.
The technical screen formats
"Technical screen" is an umbrella for a few different formats. For data engineering, the mix skews heavily toward SQL — but you should know what each one looks like so nothing surprises you on the day.
| Format | What it looks like | How common (DE) |
|---|---|---|
| Live SQL | Screen-share a shared editor (HackerRank, CoderPad, or a real DB). You're given a schema and asked to write queries while talking through them. 30–45 min. | Very common — expect it almost every loop |
| Take-home | A dataset (CSVs / a SQLite file) plus a prompt: clean it, model it, answer a few business questions. Timeboxed (2–4 hrs) but done on your own machine. | Common at smaller / data-heavy companies |
| Python / coding round | A shared editor, one or two problems on data manipulation and light algorithms. Easier than a pure-SWE bar (see below). | Common, often paired with SQL |
| Data modeling | "Design the tables for X." Whiteboard or verbal. Sometimes its own round; often folded into system design (Ch.05). | Occasional at this stage |
If you only have a weekend, spend it on live SQL. It is the single most common — and most filtering — round for data engineers, and it's pure pattern recognition once you've drilled it. The Python round is usually the easier one to clear if you can already wrangle a list of dicts.
The SQL patterns that recur
Almost every live-SQL question is a remix of a small set of patterns. You already met the building blocks in Foundations Ch.05 (SQL) — here we map them to the shapes interviewers actually ask. Learn to recognise the pattern from the prompt and the query writes itself.
| Pattern | What the prompt sounds like | The tool |
|---|---|---|
| Multi-table joins | "List each order with the customer's name and the product category." | JOIN on keys; know INNER vs LEFT cold |
| Aggregation + HAVING | "Which customers placed more than 5 orders?" | GROUP BY … HAVING count(*) > 5 |
| Ranking / top-N per group | "Top 3 products by revenue in each category." | ROW_NUMBER() / RANK() OVER (PARTITION BY … ORDER BY …) |
| Running totals / moving windows | "Cumulative revenue by day." / "Month-over-month growth." | SUM(...) OVER (ORDER BY ...), LAG() |
| Dedup | "Keep only the latest row per user." | ROW_NUMBER() then filter = 1 |
| Date / time logic | "Active users in the last 30 days." / "Bucket by month." | date_trunc, date math, BETWEEN |
| Self-join | "Find employees who earn more than their manager." | Join a table to itself with different aliases |
| Gaps & islands (lite) | "Longest streak of consecutive login days." | ROW_NUMBER() trick to group runs |
Joins and GROUP BY are table stakes — everyone can do them. The candidates who pass are the ones fluent in window functions: ROW_NUMBER, RANK, LAG/LEAD, and SUM() OVER (...). If you drill one thing, drill these. The structure is always func() OVER (PARTITION BY ... ORDER BY ...) — "do this calculation, restarting for each group, in this order."
A quick reference for the window-function skeleton you'll reuse in every drill below:
SELECT
some_columns,
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col DESC) AS rn,
SUM(value) OVER (PARTITION BY group_col ORDER BY sort_col) AS running_total,
LAG(value) OVER (PARTITION BY group_col ORDER BY sort_col) AS prev_value
FROM some_table;
-- PARTITION BY = "restart for each group" (omit it to run over the whole table)
-- ORDER BY = the sequence the function walks throughSQL drills
Three problems below. Try each one before opening the solution. Write it out — in your head isn't enough; the fluency lives in your fingers. Each solution comes with a one-line explanation of why it works.
Drill 1 — Second-highest salary, and top-2 per department
Problem. Given employees(id, name, dept, salary): (a) return the second-highest salary overall, and (b) return the top 2 highest-paid employees in each department.
Try it before revealing.
-- (a) second-highest salary overall.
-- DENSE_RANK handles ties (two people tied for #1 still makes the next #2).
SELECT salary
FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 2
LIMIT 1;
-- (b) top 2 earners per department.
SELECT dept, name, salary
FROM (
SELECT dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 2
ORDER BY dept, salary DESC;Why it works: rank rows inside a subquery, then filter on the rank in the outer query — you can't put a window function in WHERE directly, so the subquery (or a CTE) is mandatory. Use DENSE_RANK when ties should share a place; ROW_NUMBER when you want exactly N rows per group.
Drill 2 — Running total and month-over-month growth
Problem. Given orders(id, order_date, amount): produce, per calendar month, (a) that month's total revenue, (b) a cumulative running total across months, and (c) the percent change versus the previous month.
Try it before revealing.
WITH monthly AS (
SELECT date_trunc('month', order_date) AS mth,
SUM(amount) AS revenue
FROM orders
GROUP BY date_trunc('month', order_date)
)
SELECT
mth,
revenue,
SUM(revenue) OVER (ORDER BY mth) AS running_total,
LAG(revenue) OVER (ORDER BY mth) AS prev_month,
ROUND(
100.0 * (revenue - LAG(revenue) OVER (ORDER BY mth))
/ LAG(revenue) OVER (ORDER BY mth), 1
) AS mom_pct
FROM monthly
ORDER BY mth;Why it works: first roll the raw rows up to one row per month in a CTE, then layer window functions on top. SUM() OVER (ORDER BY mth) accumulates because an ORDER BY in a window defaults to "everything from the start up to the current row." LAG grabs the previous month's value for the growth comparison; the first month's mom_pct is NULL, which is correct — there's nothing before it.
Drill 3 — Deduplicate to the latest row per user
Problem. A table events(user_id, status, updated_at) has multiple rows per user from repeated updates. Return exactly one row per user — the most recent one. Then, as a stretch, count how many users have a contiguous streak: their two latest events both have status = 'active'.
Try it before revealing.
-- Dedup: number each user's rows newest-first, keep #1.
WITH ranked AS (
SELECT user_id, status, updated_at,
ROW_NUMBER() OVER (PARTITION BY user_id
ORDER BY updated_at DESC) AS rn
FROM events
)
SELECT user_id, status, updated_at
FROM ranked
WHERE rn = 1;
-- Stretch: users whose two most-recent events are both 'active'.
WITH ranked AS (
SELECT user_id, status,
ROW_NUMBER() OVER (PARTITION BY user_id
ORDER BY updated_at DESC) AS rn
FROM events
)
SELECT COUNT(*) AS streaking_users
FROM (
SELECT user_id
FROM ranked
WHERE rn <= 2
GROUP BY user_id
HAVING COUNT(*) = 2 -- needs at least 2 events
AND SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) = 2
) t;Why it works: "one row per group" is the canonical ROW_NUMBER() … = 1 pattern — partition by the entity, order by recency, keep the top. The stretch reuses the same ranking, takes the two newest rows, and uses a conditional SUM to confirm both are active. That CASE-inside-SUM trick (counting rows that meet a condition) is itself a recurring interview move.
Python & coding rounds
Many DE loops include a Python round. Good news: the bar is usually lower than a pure software-engineering interview. You're rarely asked to invert a binary tree or do dynamic programming. Instead you're tested on the thing you'll actually do on the job: pushing data around.
| You SHOULD be fluent in… | You probably WON'T need… |
|---|---|
| Dicts, lists, sets, and when to reach for each | Advanced dynamic programming |
Grouping / aggregating records (e.g. defaultdict, Counter) | Graph algorithms (BFS/DFS, Dijkstra) |
| Parsing files / strings (CSV-ish lines, splitting, cleaning) | Tricky bit manipulation |
| Dedup and lookups with sets/dicts (O(1) membership) | Balancing trees, custom data structures |
Sorting with a key=, comprehensions, basic complexity sense | Heavy LeetCode-hard puzzle solving |
What level to prepare to: aim for comfortable on LeetCode easy and the more straightforward mediums — especially anything tagged hash-map, array, or string. If you can confidently group a list of dicts, count occurrences, deduplicate, and parse a messy line into clean fields, you're at the level most DE Python rounds expect.
Three imports cover most DE Python interviews: collections.defaultdict and collections.Counter for grouping/counting, and csv / json for parsing. Reaching for Counter instead of hand-rolling a tally signals that you write Python the way a practitioner does.
Python drills
Two problems. Write a working version before opening the solution — ideally in a real REPL so you catch your own bugs.
Drill 4 — Group and aggregate a list of dicts
Problem. Given a list of order records, return total revenue per region, sorted highest-first. Each record looks like {"region": "us", "amount": 120}.
Try it before revealing.
from collections import defaultdict
orders = [
{"region": "us", "amount": 120},
{"region": "eu", "amount": 80},
{"region": "us", "amount": 200},
{"region": "eu", "amount": 95},
]
totals = defaultdict(float)
for o in orders:
totals[o["region"]] += o["amount"]
# sort by revenue, highest first -> list of (region, total) tuples
result = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
print(result) # [('us', 320.0), ('eu', 175.0)]Why it works: defaultdict(float) lets you += into a key that doesn't exist yet (no "is this key here?" check), which is the cleanest group-and-sum in Python. Then sorted with a key= lambda orders by the value. Say the complexity out loud: one pass to group (O(n)), one sort (O(k log k)) over the number of regions.
Drill 5 — Parse records and find duplicates
Problem. You're handed raw lines like "alice,alice@x.com" (some with stray whitespace and mixed case in the email). Parse them, and return the list of email addresses that appear more than once — normalised (trimmed + lowercased) and deduplicated.
Try it before revealing.
from collections import Counter
lines = [
"alice, Alice@X.com ",
"bob,bob@x.com",
"alice2, alice@x.com", # same email as the first row, different name
"carol,carol@x.com",
"bob3, BOB@x.com ", # same email as bob
]
def email_of(line):
_name, email = line.split(",", 1) # split once; names never contain commas here
return email.strip().lower() # normalise: trim + lowercase
counts = Counter(email_of(line) for line in lines)
dupes = [email for email, n in counts.items() if n > 1]
print(dupes) # ['alice@x.com', 'bob@x.com']Why it works: the parsing step is the real trap — interviewers love messy input. Normalising (strip().lower()) before comparing is what makes the dedup correct. Counter tallies in one pass; the comprehension keeps only emails seen more than once. Mention edge cases out loud: malformed lines with no comma, or empty emails — handling them gracefully scores points even if the prompt didn't ask.
How to practice (so it actually sticks)
Grinding random problems for hours is low-yield. A handful of deliberate habits beat raw volume:
- Spaced repetition over cramming. Twenty minutes of SQL drills most days beats one six-hour binge. The patterns need to move into reflex, and reflex is built by frequency, not duration.
- Think out loud — always. In a live round, interviewers score your reasoning, not just the final query. Narrate: "I need top-N per group, so that's a
ROW_NUMBERpartitioned by category…" Practising silently trains the wrong muscle. Talk through every drill in this chapter as if someone's watching. - Do timed mocks. Set a 30-minute timer and solve under pressure, ideally with a friend playing interviewer (or out loud to a rubber duck). The clock and the audience are the parts you can't simulate by reading.
- State assumptions and edge cases. Ask about
NULLs, ties, duplicates, empty inputs. Strong candidates surface these before writing — it shows judgement. - Review your misses. Keep a short log of every problem you fumbled and the pattern it belonged to. Re-drill those specific patterns next week.
| Skill | Where to drill | Aim for |
|---|---|---|
| SQL | StrataScratch / DataLemur-style platforms (DE & analytics question banks) | Comfortable with every pattern in the table above; window functions on reflex |
| Python | LeetCode easy + select mediums (hash-map / array / string tags) | Group, count, dedup, parse — without looking up syntax |
| Realistic mocks | A peer, a mock-interview service, or a timer + rubber duck | Solving and narrating under 30 min |
Memorising solutions to specific problems fails the moment the interviewer tweaks the schema. Internalising the pattern ("this is a top-N-per-group, so window-rank-then-filter") survives any variation. Always ask yourself "which pattern is this?" before "what's the answer?"
✓ Check yourself
Answer from memory before opening each one.
-
employeesholds four salaries: 100, 100, 90, 80. You rank them bysalary DESCand filter to rank= 2to get the second-highest salary. What comes back withROW_NUMBER, withRANK, and withDENSE_RANK— and why must the filter sit in an outer query?Show answer
ROW_NUMBERreturns 100 (the tie gets numbers 1 and 2),RANKreturns no rows (it numbers them 1, 1, 3, 4 — there is no 2), and onlyDENSE_RANKreturns 90, because ties share a place without leaving a gap. A window function cannot appear inWHERE, so you rank in a subquery or CTE and filter outside it. See Drill 1. -
You run the month-over-month query from Drill 2 on an
orderstable with revenue of 200 in January, 300 in February, nothing in March and 150 in April. What does the April row report asmom_pct, and why is that misleading?Show answer
It reports −50.0, comparing April with February.
LAGfetches the previous row in the window's order, not the previous calendar month, and theGROUP BYproduces no row for a month with no orders. Say so out loud and ask whether gaps are possible; the fix is to join onto a complete list of months first. See Drill 2. -
The final drill states that
loginshas at most one row per user per day. Suppose a user logged in on March 1, 2, 3, 5 and 6, and the March 2 row is accidentally loaded twice. What does the streak query return for them, and what one-line change makes it safe?Show answer
It returns 4, though the true longest streak is 3. The duplicate pushes every later row number up by one, so
login_date − rnstops being constant within the first run and, by coincidence, the March 5–6 rows land on the same key as March 1–2 — four rows in one "island". De-duplicate first (SELECT DISTINCT user_id, login_date) before numbering; this is why you ask about duplicates before writing. See the final drill and stating assumptions. -
In Drill 5 a candidate tallies the emails with
Counterbut skips thestrip().lower()step. What does their code return for the sample lines, and what does that tell you about where the difficulty in this kind of problem sits?Show answer
An empty list:
" Alice@X.com "," alice@x.com","bob@x.com"and" BOB@x.com "are four different strings, so nothing is counted twice. The counting is trivial; the parsing is the trap — normalise before comparing, and mention malformed lines and empty emails even if the prompt did not. See Drill 5. -
A friend has solved 150 SQL problems silently, alone, untimed, and can reproduce most of the solutions from memory. In a live round the interviewer renames two columns and adds a tie-breaking rule, and your friend freezes. Which two practice habits would have prevented this?
Show answer
Internalising patterns rather than memorising answers — "this is top-N per group, so rank in a window, then filter" survives any schema change, where a memorised query does not. And practising out loud under a timer: interviewers score the reasoning as well as the query, and the clock and the audience are exactly what silent solo practice never rehearses. See How to practice.
-
Your loop next week has a live SQL round and a Python round, and you have one weekend to prepare. How do you split it, and what would change the split?
Show answer
Mostly live SQL, and within it window functions: it is the most common and most filtering round, and joins and
GROUP BYare table stakes that will not set you apart. The Python bar is lower — group, count, dedup and parse at LeetCode-easy level. The split reverses if window functions are already reflex for you but you cannot yet group a list of dicts without looking up syntax, and if the recruiter says the screen is a take-home, practise a timeboxed clean-model-answer run instead. See The technical screen formats and Python & coding rounds.
Final drill — Longest streak of consecutive active days (gaps & islands lite)
Problem. Given logins(user_id, login_date) with at most one row per user per day, find the length of each user's longest run of consecutive calendar days. Try it before revealing — this is the classic "gaps and islands" pattern dressed down.
WITH numbered AS (
SELECT user_id, login_date,
ROW_NUMBER() OVER (PARTITION BY user_id
ORDER BY login_date) AS rn
FROM logins
),
grouped AS (
-- Subtracting the row number from the date is constant
-- for any run of consecutive days -> that constant IS the group key.
SELECT user_id,
login_date - (rn * INTERVAL '1 day') AS streak_key
FROM numbered
)
SELECT user_id, MAX(streak_len) AS longest_streak
FROM (
SELECT user_id, streak_key, COUNT(*) AS streak_len
FROM grouped
GROUP BY user_id, streak_key
) runs
GROUP BY user_id
ORDER BY user_id;Why it works: the key insight is that date − row_number stays constant across a stretch of consecutive days (both increase in lockstep) but jumps whenever there's a gap. So that difference becomes a group key for each "island" of consecutive days. Group by it, count the rows in each island, take the max per user. If you recognised this as gaps-and-islands and reached for the ROW_NUMBER trick, you're ready for the live round.
Takeaway
- Live SQL is the most common and most filtering round for data engineers, and its questions are a small set of patterns in new clothes.
- Joins and
GROUP BYare table stakes, and window functions (ROW_NUMBER,RANK,LAG,SUM() OVER) are what separate candidates who pass. - A window function cannot sit in
WHERE, so rank in a subquery or CTE and filter in the outer query; useDENSE_RANKwhen ties share a place andROW_NUMBERwhen you want exactly one. - One row per group is
ROW_NUMBER() OVER (PARTITION BY entity ORDER BY recency DESC) = 1. - For month-over-month questions, roll the rows up to one per month in a CTE first, then layer the running total and
LAGon top. - The Python round is usually easier than a software engineering bar: group, count, deduplicate and parse with
defaultdict,Counter,csvandjson, and normalise messy input before you compare. - Learn the pattern, not the answer, and say your assumptions about NULLs, ties and duplicates out loud before you write.
You can clear the SQL and coding screens, and the final rounds test how you reason about systems and how you tell your story. → Interview Prep II — System Design & Behavioral
Further reading
- PostgreSQL tutorial: Window functions — PostgreSQL docs, accessed 2026. The clearest short explanation of
OVER,PARTITION BYand window ordering. - PostgreSQL: Window functions — PostgreSQL docs, accessed 2026. The reference list for
row_number,rank,dense_rank,lagandlead. - PostgreSQL: WITH queries — PostgreSQL docs, accessed 2026. Common table expressions, the structure for layering window functions.
- Python: collections — Python docs, accessed 2026.
Counteranddefaultdict, the two classes behind most grouping answers. - Python: csv — Python docs, accessed 2026. Parsing delimited lines with the standard library.