Data Engineer Practice Problems

Data engineer practice problems are the tasks a data engineering interview asks you to solve live: SQL over messy tables, Python that reshapes raw records, data models with a stated grain, pipeline designs that survive late data and reruns, and Spark jobs that have to scale. The best ones run against hidden data and come with the edge case that fails most first attempts, so this guide works 1 real problem from each family through to the answer and shows how to practice the rest.

Last updated: Proudly published by: Jeff Wahl17 min read

What data engineer practice problems cover

A data engineering interview loop asks for 5 kinds of work, and practice problems are worth your time only when they train those 5. SQL comes first and appears in nearly every loop: queries over tables that hold duplicates and NULLs, with ties and late rows mixed in, where the interviewer checks that the result is right for every row, not only the sample. Python is the second coding round, and in a data engineering loop it is about records rather than algorithms: parsing and deduplicating them, then grouping and reshaping them with the standard library.

Data modeling problems ask you to design tables for a business process: pick the grain and the keys, and decide how each dimension keeps history. Pipeline design problems ask how data gets from a source to a consumer on time and stays correct when jobs rerun or events arrive late, and when a source changes. Spark problems show up at companies that run Spark, and they test how the engine shuffles and partitions data and how it handles joins and skew.

DataDriven holds {COUNT:total} problems across these rounds: {COUNT:sql} in SQL, {COUNT:python} in Python, {COUNT:data_modeling} in data modeling and {COUNT:pipeline_architecture} in pipeline architecture, all asked in real data engineering interviews. Each section on this page works 1 of them through to an answer and names the edge case that fails most first attempts.

Prepare for the interview
01 / Open invite
02min.

Know data engineer practice problems the way the interviewer who asks it knows it.

a data engineer practice problems query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1SELECT user_id,
2 COUNT(*) AS sessions
3FROM events
4WHERE ts >= NOW() - INTERVAL '7 day'
5
Execute your solution0.4s avg.

SQL practice problem: keep the latest row per key

For each account status in a users table, return the most recently signed-up user with their id, username, email and signup date, keeping the larger user_id when 2 accounts tie on the latest date. This shape is the most common SQL problem in data engineering because CDC feeds and event logs need it before anyone can use the data, and so does every slowly changing source. The pattern is a window function that numbers each status's rows newest first, then a filter that keeps row 1. GROUP BY account_status with MAX(signup_date) looks shorter, but the other columns then come from no particular row, so the username can belong to a different user than the date.

The edge case is the tie. When 2 rows for 1 key share the same timestamp, ROW_NUMBER() still gives them 1 and 2, but which row gets 1 is up to the engine, so the query can return a different row on each run. The PostgreSQL documentation defines the behaviour: rows equal on the ORDER BY columns are peers, rank() gives every peer the same rank, and row_number() simply counts. Swap in RANK() and a tied key comes back twice. The fix is a tiebreaker that is unique within the key, user_id DESC here, so the order is total and the answer never changes.

The same query also handles exact duplicate rows: 2 identical rows for 1 key still number 1 and 2, so only 1 survives. When an interviewer asks why you chose ROW_NUMBER() over RANK() or DENSE_RANK(), the answer is this pair of cases. The RANK vs DENSE_RANK vs ROW_NUMBER practice drills the difference, and the SQL practice problems cover the patterns that build on it.

How a tiebreaker settles the latest row per key

WITH ranked AS (
  SELECT
    customer_id,
    city,
    updated_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY updated_at DESC, update_id DESC
    ) AS rn
  FROM customer_updates
)
SELECT customer_id, city, updated_at
FROM ranked
WHERE rn = 1
ORDER BY customer_id;

The same technique on a small change log: customer_updates holds 7 rows for 3 customers, customer 103 has 2 updates at the same minute and customer 102 has 1 row loaded twice. Remove update_id DESC from the window to see the tie become unstable, or swap ROW_NUMBER for RANK to see customer 103 return twice.

SQL practice problem: find streaks of consecutive days

Given a logins table, return each user's streaks of consecutive login days with the start and length of each. This is gaps and islands, and a data engineer meets it in many forms: consecutive days active, unbroken runs of sensor readings, periods a subscription stayed paid. A self join or a loop gets there slowly. The pattern interviewers look for takes 1 window function.

Number each user's login days in order, then subtract the row number from the date. Within an unbroken run the date rises by 1 per row and so does the row number, so the difference stays the same; after a gap the date jumps while the row number does not, so the difference changes. For a user who logs in on days 1 to 3 and again on days 5 and 6, the difference is 0 for the first 3 days and 1 for the last 2, which splits the 5 logins into a 3 day streak and a 2 day streak at the missing day 4.

Login days 1, 2, 3, 5 and 6 with row numbers 1 to 5 and no login on day 4: day minus row number is 0 for days 1 to 3, a 3 day streak, and 1 for days 5 and 6, a 2 day streakLogin days 1, 2, 3, 5 and 6 with row numbers 1 to 5 and no login on day 4: day minus row number is 0 for days 1 to 3, a 3 day streak, and 1 for days 5 and 6, a 2 day streak

Call that difference the anchor. Group by it and each group is a streak. In PostgreSQL, subtracting an integer from a date gives a date, so the anchor is a real date and MIN(login_date) names the start of each streak:

WITH days AS (
  SELECT DISTINCT user_id, login_at::date AS login_date
  FROM logins
),
islands AS (
  SELECT
    user_id,
    login_date,
    login_date - CAST(ROW_NUMBER() OVER (
      PARTITION BY user_id ORDER BY login_date
    ) AS int) AS anchor
  FROM days
)
SELECT user_id, MIN(login_date) AS streak_start, COUNT(*) AS streak_days
FROM islands
GROUP BY user_id, anchor
ORDER BY user_id, streak_start;

The edge case is the DISTINCT in the first step. A user who logs in twice on 1 day gets 2 row numbers for 1 date, the difference shifts by 1 and the streak breaks in 2. Deduplicate to 1 row per user and day before numbering, and cast timestamps to dates in the time zone the business reports in. The SQL window functions practice has more problems built on this trick.

The same idea finds runs of rows that share a state, not only runs of dates. Number every health check per service, number the healthy ones again, and the difference between the 2 numbers stays fixed through each unbroken healthy run. Try it on the longest healthy streak across a set of services:

Longest Uptime Streak

> We monitor a fleet of services that each log periodic health checks, and we want to crown the single steadiest one: the service with the longest unbroken run of 'healthy' checks, where casing doesn't matter. A run ends the moment a service logs anything other than healthy, and if two services tie for the longest run, the one whose name comes first alphabetically takes the title. Return that service's name and the length of its streak.

Python practice problem: group events into sessions

Click events arrive as (user_id, timestamp) pairs in no particular order, and each event needs a session number, where a session ends after 30 minutes with no activity. It is a typical data engineering Python round, with no clever algorithm but a dozen ways to get the details wrong. The solution sorts by user and time, walks each user's events once, and starts a new session whenever the gap since the previous event passes the limit.

from datetime import timedelta
from itertools import groupby
from operator import itemgetter

GAP = timedelta(minutes=30)

def sessionize(events):
    """Yield (user_id, ts, session) for (user_id, ts) pairs in any order."""
    ordered = sorted(events, key=itemgetter(0, 1))
    for user_id, rows in groupby(ordered, key=itemgetter(0)):
        session, last_ts = 0, None
        for _, ts in rows:
            if last_ts is not None and ts - last_ts > GAP:
                session += 1
            yield user_id, ts, session
            last_ts = ts

Take 1 user with events at 09:00, 09:10, 09:55, 10:05. The gaps run 10 minutes, then 45, then 10, so only the 45 minute gap passes 30, and the 4 events form 2 sessions. Write the example out loud before coding; interviewers weigh the reasoning as much as the code, and the example is where you agree on what the boundary means.

The edge cases are the ones an interviewer will ask about next. A gap of exactly 30 minutes stays in the session because the test is >; say which you chose and why. Input that arrives unsorted is why the function sorts. A file too large for memory changes the answer: let the warehouse or an external sort order the data, and the generator then holds 1 event at a time, so memory stays flat however many events stream through it.

The Python interview questions cover the follow-ups this round brings, from sorting cost to generators, and the Python practice problems drill parsing, grouping and deduplicating records.

Data modeling practice problem: a ride-sharing warehouse

Design the warehouse tables for a ride-sharing app so analysts can report trips and cancellations, plus fares, by city and by the driver's rating band at the time of each trip. Interviewers judge data modeling answers on 3 decisions, in this order: the grain of the fact table, the keys that join it to each dimension, and how each dimension keeps history.

State the grain in 1 sentence before drawing a table: 1 row for every trip request whether it completed or was cancelled. That choice decides everything after it. A grain of completed trips makes the cancellation rate impossible to compute, and a grain of trip events (requested, accepted, started, ended) makes every fare sum a filter away from double counting.

The phrase "at the time of each trip" is the history requirement. A driver's rating band changes, and a report on last quarter must use the band the driver had then. That makes dim_driver a type 2 slowly changing dimension. Each change adds a row with a new surrogate driver_key, and valid_from and valid_to bound that version. The fact table stores the key of the version that was current when the trip was requested. Riders and cities change rarely enough to overwrite in place, and a date dimension serves calendar reporting.

The checks to run aloud before you call the design done: can every question in the prompt be answered with 1 join per dimension, does any fare get counted twice, and what happens to a trip whose driver changes band the same day. The data modeling interview questions rehearse defending those choices, and the data modeling practice problems are answered on the same schema canvas an interview uses.

A ride-sharing star schema with driver history

fct_trip
trip_idPKBIGINT
driver_keyFKBIGINT
rider_idFKBIGINT
city_idFKINT
date_keyFKINT
statusVARCHAR
fare_amountDECIMAL
distance_kmDECIMAL
dim_driver
driver_keyPKBIGINT
driver_idBIGINT
rating_bandVARCHAR
valid_fromTIMESTAMP
valid_toTIMESTAMP
is_currentBOOLEAN
dim_rider
rider_idPKBIGINT
signup_dateDATE
home_cityVARCHAR
dim_city
city_idPKINT
city_nameVARCHAR
countryVARCHAR
dim_date
date_keyPKINT
calendar_dateDATE
week_startDATE
is_weekendBOOLEAN

1 row in fct_trip for every trip request whether completed or cancelled. dim_driver is a type 2 dimension: every rating band change adds a version with its own driver_key, so a trip joins to the band its driver had when the trip was requested.

Pipeline practice problem: an incremental load that loses rows

An hourly job copies new and changed rows from the orders table in the application database to the warehouse by reading every row whose updated_at is later than the watermark the last run stored. Analysts report that a few orders a day never reach the warehouse. Find the cause and fix it without reloading the table every hour.

The cause is the gap between when a row is stamped and when it becomes visible. A transaction stamps updated_at = 09:58 but commits at 10:03. Run 1 at 10:00 cannot see the uncommitted row and stores a watermark of 10:00. Run 2 at 11:00 reads only rows after 10:00, so the row stamped 09:58 is never read by any run. Clock skew between application servers and rows updated by a backfill script that preserves old timestamps lose rows the same way.

Timeline of an hourly orders extract: the watermark is 10:00, a row stamped updated_at 09:58 commits at 10:03, and run 2 at 11:00 misses it when reading updated_at > 10:00 but catches it with a 15 minute lookbackTimeline of an hourly orders extract: the watermark is 10:00, a row stamped updated_at 09:58 commits at 10:03, and run 2 at 11:00 misses it when reading updated_at > 10:00 but catches it with a 15 minute lookback

The fix has 2 parts that only work together. Read with an overlap: start each run at the watermark minus a lookback of 15 minutes, longer than the longest transaction you expect, so run 2 reads from 09:45 and finds the late row. Then write with a MERGE on the primary key, so the rows read twice update in place instead of loading as duplicates. Overlap without the merge double counts; the merge without the overlap still loses rows. When the lookback cannot be bounded, the answer is change data capture from the database log, which records each change in commit order.

Interviewers like this problem because it tests the 2 properties every pipeline answer needs: no row is lost, and a rerun changes nothing. The pipeline architecture practice problems are built around the same failures: late data and duplicate deliveries, plus backfills and reruns.

Keep Most Recent Record

> Our users table records each account's status and signup date, and we only want one representative per status. For each account status, return the single most recently signed-up user with their id, username, email, and signup date, keeping the larger user id if two accounts tie on the latest date.

Pipeline design practice problem: clickstream to a dashboard

Design the pipeline that turns web and app click events into a product analytics dashboard showing sessions and conversion by page. The dashboard can lag live traffic by at most 15 minutes. Some events arrive hours late from phones that were offline, and malformed events must not stop the pipeline.

A strong answer starts from the requirements. 15 minutes of freshness rules out a nightly batch but doesn't require per-event processing, so a stream into durable storage plus a micro-batch every few minutes meets it at lower cost. Late events mean sessions must be recomputed for the hours a late event touches, which is a reason to partition the session table by hour and rebuild partitions instead of appending. Malformed events go to a dead letter queue with the reason attached, so 1 bad client release cannot block every other event.

Name the guarantees out loud: the queue retains events long enough to replay a day, the raw layer keeps every event as received so any table can be rebuilt, each load is idempotent so a retry rewrites the same partition, and a quality gate stops a broken batch before it reaches the dashboard. A design that states its freshness target and how it handles replay and failures beats a longer list of tools every time.

The data pipeline interview questions ask for these guarantees out loud, and the data pipeline practice problems are designed on the pipeline canvas, where each answer is checked for freshness, replay and failure handling.

A clickstream pipeline with a 15 minute freshness target

Ingest
Store and model
Serve
API
client events
Kafka
events topic
Flink
parse events
ERRORdead letter
dead letter queue
bad events
Iceberg
raw events
Spark
build sessions
IDEMPOTENCYoverwrite partition by hour
Great Expectations
session checks
ERRORalert
Snowflake
fct sessions
SLA< 15min
Looker
product dashboard

Events land in Kafka and are parsed into a raw Iceberg table, with malformed events routed to a dead letter queue. A Spark micro-batch rebuilds the hourly session partitions a batch touches, so late events update the right hour and a retry rewrites the same partition; a quality gate holds a broken batch back from the dashboard.

Spark practice problem: a join stuck on 1 task

A PySpark job joins a 100 GB events table to customers on customer_id. 199 of the 200 tasks in the join stage finish in a minute and 1 runs for an hour. Explain why and fix it. The cause is skew: 1 enterprise customer owns 30 GB of the events, and a sort-merge join sends every row for that customer to the same partition.

Put numbers on it. With the default of 200 shuffle partitions, the other 70 GB spread to a median of about 360 MB per partition, while the hot partition holds 30 GB, about 85 times the median. The Spark performance tuning guide describes the first fix, which costs nothing: adaptive query execution, on by default since Spark 3.2.0, treats a partition as skewed when it is larger than 5 times the median and larger than 256 MB, and splits it across several tasks. This partition passes both tests, so on a current Spark version check the plan for a skew join before changing any code.

If customers is small, the second fix removes the shuffle: a broadcast join copies the small table to every executor, and Spark does it by itself for tables under the 10 MB default of spark.sql.autoBroadcastJoinThreshold. When both sides are large and AQE is off or not enough, salt the key by hand. Add a random salt from 0 to 15 to each event, copy each customer row once per salt, and join on both columns:

from pyspark.sql import functions as F

SALTS = 16
events = events.withColumn("salt", (F.rand() * SALTS).cast("int"))
salts = spark.range(SALTS).withColumnRenamed("id", "salt")
customers = customers.crossJoin(salts)
joined = events.join(customers, ["customer_id", "salt"])

The hot key now spreads over 16 partitions of about 1.88 GB each, and the cost is a customers table 16 times larger. Interviewers follow up with exactly that trade: salt only the keys you know are hot, or the copy of the small side outgrows the problem it solved.

A common version of this problem has a dimension just over the broadcast threshold, where the fix is 1 explicit broadcast() hint rather than salting. Diagnose and fix one here:

Fix Skewed Viewing Events Pipeline

> You are the on-call data engineer at a streaming company and the nightly `viewing_engagement` Spark job just paged you: it normally finishes in 45 minutes but has been running for over two hours and is still stuck. The job joins a large `event_data` table (800M rows/day of viewing, playback, and interaction events) against a small `users` dimension (2M subscribers) on `user_id`, then produces daily engagement counts by event type and account status, and its SLA is 60 minutes. Read the Spark UI evidence to find the root cause and fix the job so it meets SLA.

The PySpark interview questions ask about shuffles, joins and skew the way interviewers follow up on a slow job, and the PySpark practice problems run your code on a real Spark engine.

How to practice data engineering problems so it carries into the interview

Solve under the conditions of the round. Set a timer of about 20 minutes for a SQL or Python problem and 40 for a design, talk through your plan before you write, and do not run the code until you believe it is right. An interview rarely gives you 10 runs to converge on an answer, and the habit of checking your own logic first is what the round measures.

Before you submit, run the edge cases in your head: an empty input, a NULL in the join key, 2 rows tied on the sort column, a duplicate row, a key with 1 row. Most wrong answers in data engineering rounds return plausible numbers on the sample and wrong ones on real data, and these 5 cases find nearly all of them.

Read a reference solution only after you have a working answer or have spent the full time, and then compare approaches rather than syntax. Come back to the problems you missed 3 or 4 days later and solve them again from a blank editor. A pattern you can rebuild from nothing is one you will recognise in a new problem. The SQL interview questions pair well with this: they ask you to explain the same patterns in words, which is the other half of a SQL round.

More data engineer practice problems with answers

SQL

Return the top 3 products by revenue in each category, keeping ties.

Aggregate revenue per product first, then apply DENSE_RANK() OVER (PARTITION BY category ORDER BY revenue DESC) and keep rows with a rank of 3 or less. DENSE_RANK keeps every product tied at a position without skipping the next rank; ROW_NUMBER would cut ties arbitrarily, and RANK would skip ranks after a tie, so a category could return fewer than 3 distinct revenue levels.

SQL

Compute each user's 7 day rolling count of orders when some days have no orders.

Join the users to a date spine first, so every user has a row for every day with a count of 0 where nothing happened, then apply SUM(order_count) OVER (PARTITION BY user_id ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW). Without the spine, the 6 preceding rows are the last 6 days that had orders, which can stretch over weeks.

SQL

Find customers who placed an order in January but none in February.

Use an anti join: select January customers, then drop anyone with a February order, using NOT EXISTS or a LEFT JOIN on customer_id that keeps rows where the February side is NULL. Avoid NOT IN against a subquery that can return a NULL customer_id, because 1 NULL makes NOT IN return an empty result.

Python

Flatten nested JSON records into rows with dotted column names.

Write a recursive function that walks each dict and joins the keys with a dot, so {"user": {"id": 7}} becomes {"user.id": 7}. Decide up front what a list does: explode it into 1 row per element, or keep it as a JSON string. Collect the union of keys across records, because records missing a key must still produce the column, filled with None.

Python

Deduplicate a stream of events that can arrive more than once, without holding the whole stream in memory.

Keep a set of event ids seen within a bounded window, such as the last hour of event time, and evict ids older than the window as time advances. Memory is bounded by the events per window, not the stream. Say the trade aloud: a duplicate that arrives after the window passes through, so the window length comes from how late duplicates arrive in practice.

Data modeling

Model orders where 1 order holds many products and each product can appear in many orders.

Set the fact table's grain at 1 row per order line keyed by order_id and line_number. Its foreign keys go to the product and customer dimensions and to a date dimension. Order level values such as shipping cost either go in a separate order fact or are allocated across the lines by a stated rule, because repeating them on every line double counts them in any sum.

Data modeling

When would you choose a type 1 dimension over a type 2?

Type 1 overwrites the attribute, so history is lost and every report shows today's value; it suits corrections and attributes nobody reports on historically, like a fixed typo in a name. Type 2 adds a version row per change, so facts keep the value they had at the time; it suits attributes that reports slice by, like a customer's region or plan.

Pipelines

A daily job failed for 3 days. How do you backfill it safely?

Make each run process exactly 1 date partition chosen by a parameter, not by the current clock, and make it overwrite that partition rather than append. Then rerun the 3 dates oldest first and check row counts per partition against the source. A job written this way can be rerun any number of times with the same result.

Spark

Why does a Spark job write thousands of tiny files, and how do you stop it?

Each task writes at least 1 file per output partition it holds, so 200 shuffle partitions writing into 50 date partitions can produce up to 10,000 files. Repartition by the output partition column before the write so each date is written by few tasks, or coalesce to a sensible count, and compact tables that receive small appends over time.

How to tell you are ready for the interview

Count patterns, not problems. A data engineering SQL round draws on about 8 patterns. Deduplication and top N per group belong on that list, as do running totals with moving windows. So do gaps and islands and sessionization, and the rest are pivots and anti joins, plus period over period change. You are ready for the round when you can read a problem you have never seen, name its pattern within 2 minutes, and finish in about 20 with the edge cases handled.

The same test works for the other rounds. For Python, it's writing a parser and a deduplicator, then a group-by, from a blank file without looking anything up. For data modeling, it is stating the grain and the history rule of any business process in 2 sentences. For pipeline design, it's explaining how your design behaves on a rerun or a late event, and during a backfill, before the interviewer asks.

When the patterns hold, move from single problems to full rounds, because pacing across 4 or 5 rounds in 1 day is its own skill. The data engineer interview prep guide lays out each round of the loop and what it checks, so you can see which of your patterns still need work.

Data engineer practice problems FAQ

Where can I find data engineer practice problems?+
Look for problems that match the rounds you will face: SQL over realistic tables, Python that transforms records, data models with a stated grain, and pipeline designs. A good source runs your code against hidden data full of duplicates and NULLs where ties are common, so a query that only fits the sample fails the way it would in front of an interviewer. DataDriven runs SQL and Python problems in the browser, along with PySpark, and has a canvas for data modeling and pipeline design.
Are LeetCode problems enough for a data engineer interview?+
No. Algorithm puzzles are a small part of most data engineering loops. The rounds that decide the offer test SQL over messy data and Python that cleans and reshapes records. They also test data modeling and pipeline design, and an algorithm list trains none of those.
How many practice problems should a data engineer solve before interviewing?+
Count patterns, not problems. You're ready for a SQL round when you can name the pattern behind a new problem within 2 minutes and finish it in about 20. That bar applies to deduplication and top N per group, to running totals and gaps and islands, to sessionization and pivots, and to anti joins and period over period change. Most people need several problems per pattern to get there.
What SQL topics come up most in data engineer interviews?+
Joins and their row counts come up most, along with GROUP BY with HAVING and window functions (ROW_NUMBER, RANK, LAG, running sums). Deduplication and gaps and islands are common too, and so is date handling. Interviewers also ask why a query is correct, which means explaining what NULLs and ties do to it and what happens when rows are duplicated.
Do data engineer interviews include Spark problems?+
Usually yes at companies that run Spark. The questions lean on how Spark executes rather than syntax: shuffles and partitioning, plus broadcast joins and skew. At warehouse-first companies that build with SQL and dbt, Spark questions are rarer, so check the stack of the company you're interviewing with.
How do I practice data modeling and pipeline design problems?+
Write the grain of the fact table in 1 sentence before drawing anything, then the keys, then how each dimension handles change. For pipelines, state the freshness target, how the load stays correct on a rerun and where bad records go. Draw it, then check the design against a hard case: a late event, a duplicate delivery, a backfill of last month.
02 / Why practice

The candidate who gets the offer

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    5 problem shapes cover 80% of data engineer loops

    Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition

Related guides