Course 7 · Career

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.

FormatWhat it looks likeHow common (DE)
Live SQLScreen-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-homeA 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 roundA 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
The 80/20 of this chapter

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.

PatternWhat the prompt sounds likeThe 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 BYHAVING 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
Window functions are the differentiator

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:

window function shape
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 through

SQL 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.

solution — SQL
-- (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.

solution — SQL
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.

solution — SQL
-- 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 eachAdvanced 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 senseHeavy 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.

Know your standard library

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.

solution — Python
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.

solution — Python
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_NUMBER partitioned 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.
SkillWhere to drillAim for
SQLStrataScratch / DataLemur-style platforms (DE & analytics question banks)Comfortable with every pattern in the table above; window functions on reflex
PythonLeetCode easy + select mediums (hash-map / array / string tags)Group, count, dedup, parse — without looking up syntax
Realistic mocksA peer, a mock-interview service, or a timer + rubber duckSolving and narrating under 30 min
Don't memorise — internalise

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 defaultdict or Counter from 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.

solution — SQL
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