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
- Can you name the eight recurring SQL patterns and the window function each one leans on?
- Can you write a top-N-per-group query from a blank editor without peeking?
- Can you group-and-aggregate a list of dicts in Python with
defaultdictorCounterfrom memory? - Are you practising out loud and on a timer — not just reading?
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.
Next
You can clear the SQL and coding screens. The final rounds test something different — how you reason about systems and how you tell your story. → Interview Prep II — System Design & Behavioral