Cleaning, deduplication, joins, enrichment, and type casting; row vs set-based logic and pushing work to the engine.
⏱ 22 min readTopics chapter readerLevel · Building & Orchestration
01 · Orientation
What You'll Master Here
clean (make it valid), shape (combine it), aggregate (summarise it). Do them in that sequence and the result is trustworthy.
⏱ 5 min · Topic 1 of 9
Transformation is the stage where raw, untrustworthy data becomes data people can build decisions on. It is the "T" in ETL/ELT and the part most people picture when they hear "data pipeline". This chapter teaches the fundamentals that apply no matter which tool (SQL, dbt, Spark) you use.
The core idea is that almost every transformation is one of three jobs, cleaning, shaping, or aggregating, applied in that order. Get the jobs and the order right and your transformations are correct and easy to reason about. Get them wrong, usually by aggregating before cleaning, and you produce confident, wrong numbers.
By the end you will be able to take a messy raw table and turn it into a trustworthy metric step by step, and explain why each step happens where it does. You will also understand why set-based thinking (one operation over all rows) beats row-by-row loops at the scales data engineers actually work at.
Core mental model
Transformation is three jobs in order: clean (make it valid), shape (combine it), aggregate (summarise it). Do them in that sequence and the result is trustworthy.
Why it matters
Transformation is where correctness is won or lost. Unlike a crash, a transformation bug produces no error, just wrong numbers that look fine and get acted on. Mastering the fundamentals is how you avoid being the source of "the dashboard is wrong".
transformation
Turning raw data into trustworthy data by cleaning, shaping, and aggregating it.
cleaning
Making data valid: dropping bad rows, fixing types, standardising, de-duplicating.
shaping
Combining data: filtering, joining, and enriching into one coherent structure.
aggregating
Summarising rows into metrics: counts, sums, and rollups.
One day of orders · the transform ran greenFour plausible revenue figures came out of the same data. Only one is right.pick one
41,206 records landed. Two orders arrived twice because the loader retried, three rows are refunds, and one order was placed in dollars. Every task in the DAG succeeded. Which of these is the day’s revenue?
Common mistake
Treating transformation as "just writing some SQL". You skip the disciplined order (clean → shape → aggregate) and bake bad rows into metrics nobody can untangle.
Better habit
Classify every transform as cleaning, shaping, or aggregating.
Always clean before you aggregate.
Prefer set-based operations over row-by-row loops.
The big idea
Clean, then shape, then aggregate. This order is not a style preference; doing it out of order is how wrong numbers are born. Hold the order and most transformation bugs disappear.
How to study this chapter
Read the three-jobs topic, then each job with its worked example, then the row-vs-set topic. Notice how each example cleans before it aggregates.
Remember this
Transformation turns raw data into trustworthy data through three ordered jobs, clean, shape, aggregate; the order is the discipline that keeps metrics correct.
Practice2 prompts
Name the three transformation jobs and the order to apply them.
Explain why aggregating before cleaning produces untrustworthy numbers.
02 · The framework
Clean, Shape, Aggregate
Clean (valid) → shape (combined) → aggregate (summarised). Each job assumes the previous one finished; that dependency is why the order is fixed.
⏱ 6 min · Topic 2 of 9
Every transformation, however complex, decomposes into three kinds of work. Cleaning makes data valid: dropping malformed rows, fixing types, standardising formats, removing duplicates. Shaping combines data: filtering to what you need, joining tables together, enriching rows with context from elsewhere. Aggregating summarises: grouping rows into counts, sums, and the rollups a business reads.
The order matters because each job assumes the previous one is done. Shaping assumes the data is valid (you do not want to join on a malformed key). Aggregating assumes the data is both valid and correctly combined (you do not want to sum amounts that include test rows). Run them out of order and errors compound silently.
This decomposition is also how you read and debug someone else’s transformation. Faced with a 200-line SQL model, you ask: where does it clean, where does it shape, where does it aggregate? Naming the three jobs turns an intimidating wall of code into three understandable phases.
Core mental model
Clean (valid) → shape (combined) → aggregate (summarised). Each job assumes the previous one finished; that dependency is why the order is fixed.
Why it matters
The three-jobs frame is the mental model that makes transformation teachable and debuggable. It tells you what each piece of a transformation is for and reveals the single most common bug: cleaning that should have happened earlier.
validation
Checking and enforcing that data meets expectations (types, ranges, non-null).
join
Combining rows from two tables on a matching key, a core shaping operation.
enrichment
Adding context to rows by joining in reference data (e.g. country for a customer).
rollup
An aggregation that summarises detailed rows into higher-level totals.
Eight requests from the businessClean, shape or aggregate. The answer decides which layer the work belongs in.0 of 8 answered
Trim whitespace from customer names and fold them to title case.
Attach the customer’s region to every order.
Produce revenue per region per day.
Convert every amount to GBP using that day’s rate.
Drop test orders placed by internal staff accounts.
Count distinct buyers per week.
Keep only the latest row per order_id.
Explode one order into one row per line item.
Common mistake
Mixing cleaning, shaping, and aggregating into one tangled step. The transformation becomes impossible to reason about, test, or debug when a number looks wrong.
Better habit
Separate cleaning, shaping, and aggregating into distinct steps.
Read unfamiliar transforms by labelling which job each part does.
Keep each step doing one kind of work.
Interview note
Asked to design a transformation, narrate it as "first I clean…, then I join/shape…, then I aggregate…". That structure signals disciplined thinking and is far stronger than diving straight into SQL.
Remember this
Every transformation is cleaning, shaping, and aggregating in order; naming the three jobs makes transforms understandable, testable, and correct.
Practice2 prompts
Label these as clean/shape/aggregate: drop nulls, join to customers, sum revenue, cast to int.
Explain why shaping assumes cleaning is already done.
03 · Job 1
Cleaning: Making Data Valid
every row is checked against the rules, and only valid rows (or fixed ones) pass through. Rejects are quarantined, not silently dumped.
⏱ 6 min · Topic 3 of 9
Cleaning is the first and most underrated job: making raw data valid before anything else touches it. Raw data is dirty by default, duplicate rows from retries, wrong types (a number stored as text), inconsistent formats ("USA" vs "United States"), nulls where values are required, and test or bot rows that should never count.
The example shows the most common cleaning operations on raw events: drop rows with a null user, remove duplicates by a key, exclude bots, and standardise the country code. Each is a rule, and the power of encoding cleaning as rules is that they run identically every time, unlike a human cleaning a spreadsheet by eye.
A subtle but important decision is what to do with bad rows: drop them, fix them, or quarantine them. Silently dropping data hides problems; a good pipeline often routes invalid rows to a separate "quarantine" table so they can be inspected, rather than vanishing. Cleaning should be visible, not a silent deletion.
Core mental model
Cleaning is quality control at the factory door: every row is checked against the rules, and only valid rows (or fixed ones) pass through. Rejects are quarantined, not silently dumped.
Why it matters
Everything downstream trusts that the data is valid. Skip cleaning and bad rows propagate into every join and every metric, corrupting results in ways that are extremely hard to trace back to their source.
de-duplication
Removing repeated rows, usually by a key, often caused by retries upstream.
type casting
Converting a value to its correct type (text "42" to integer 42).
standardisation
Making formats consistent (casing, units, date formats) across rows.
quarantine
Routing invalid rows to a separate table for inspection instead of dropping them silently.
Ten rows from one day · five cleaning rulesRows fixed, rows dropped, and money moved. The third column is the one nobody counts.1 of 5 rules on
3 rows fixed0 rows dropped£0 moved
Nothing so far has changed a single amount — these rules make values consistent rather than correct. Turn on the currency conversion to see the other kind.
Every row still presentClean once, in one layer, for every consumer. The reason this matters is not tidiness — it is that a rule applied in two places will eventually be applied two different ways, and both results will look plausible.
Cleaning raw events into a valid setworked example
SQL
Input data
raw_events5 rows
event_id
user_id
country
is_bot
e1
u1
us
false
e1
u1
us
false
e2
NULL
gb
false
e3
u2
GB
true
e4
u3
gb
false
e1 is duplicated, e2 has a null user, e3 is a bot, and country casing is inconsistent. All are cleaning problems.
-- Cleaning rules: valid user, de-duplicated, no bots, standard country.selectdistincton(event_id)-- de-duplicate by keyevent_id,user_id,upper(country)ascountry,-- standardise formatevent_timefromraw_eventswhereuser_idisnotnull-- drop invalid rowsandis_bot=false-- exclude botsorderbyevent_id,event_time;
cleaned_events
event_id
user_id
country
event_time
e1
u1
US
…
e4
u3
GB
…
Duplicate e1 collapsed, null-user e2 dropped, bot e3 removed, country standardised to upper case. Two valid rows remain.
Four cleaning rules turn five dirty rows into two valid ones. Encoding them as rules means they apply identically on every run, forever.
Common mistake
Silently dropping rows that fail validation. You lose visibility into data-quality problems and may discard rows that were actually fixable.
Skipping de-duplication on data that can arrive more than once. Duplicate rows inflate every count and sum built on top of them.
Better habit
Encode cleaning as explicit, repeatable rules, not manual fixes.
Quarantine invalid rows rather than silently deleting them.
De-duplicate on a stable key whenever retries are possible.
Production reality
Mature pipelines treat cleaning as a measured stage: they count how many rows were dropped or quarantined each run and alert when that number spikes, because a sudden jump in bad rows usually means an upstream change.
Remember this
Cleaning makes raw data valid through repeatable rules, de-duplicate, fix types, standardise, drop or quarantine bad rows, so everything downstream can trust it.
Practice2 prompts
List four cleaning operations and the problem each solves.
Explain why quarantining bad rows beats silently dropping them.
04 · Job 2
Shaping: Filter, Join, Enrich
filter to what matters, join the pieces, enrich with context. Watch join cardinality, the wrong join silently multiplies rows.
⏱ 6 min · Topic 4 of 9
Once data is valid, shaping combines it into the structure you actually need. The three core operations are filtering (keeping only relevant rows), joining (combining tables on a key), and enriching (adding context from reference data). Shaping is where isolated tables become a coherent, analysable picture.
The example enriches cleaned events with the user’s country and plan from a users table, a join. The result is one table that carries everything a later aggregation needs, so you do not have to re-join repeatedly downstream. Good shaping front-loads the joins so the aggregation step is simple.
The danger in shaping is the join itself. Join on a non-unique key and rows multiply (the "fan-out" trap): one event matching three user rows becomes three events, silently tripling your counts. Always know the cardinality of a join, one-to-one or one-to-many, before you write it, and verify the row count after.
Core mental model
Shaping assembles the picture: filter to what matters, join the pieces, enrich with context. Watch join cardinality, the wrong join silently multiplies rows.
Why it matters
Shaping is where data becomes useful, and where joins can silently multiply or drop rows. A wrong join is one of the most common causes of inflated metrics, and one of the hardest to notice because the numbers still look plausible.
filter
Keeping only the rows relevant to the task (a WHERE clause).
join cardinality
How many rows on each side match: one-to-one, one-to-many, or many-to-many.
fan-out
A join on a non-unique key multiplying rows and silently inflating downstream counts.
reference data
Lookup data (countries, plans, product catalog) joined in to enrich rows.
39,881 orders · £40,076.50Join something to it. Watch the row count and the revenue, and check the grain before you trust either.one row per order
Join tocustomers (one per customer, 3 missing)
Join type
after the join
value
was
rows
39,878
39,881
SUM(total_gbp)
£39,862.00
£40,076.50
one row means
one row per order
one row per order
3 orders silently deleted, £214.50 of real revenue goneThree orders were guest checkouts with no customer record. An inner join treats “no match” as “delete the row”, so three real, paid orders vanish and the revenue drops by £214.50. Nothing errored, and the total is still a plausible number.
The habitCount the rows before and after every join. A transform that changes the row count without meaning to is the most common silent bug in data engineering, and this two-second check catches every instance of it.
Enriching events with user context (a join)worked example
SQL
Input data
cleaned_events2 rows
event_id
user_id
e1
u1
e4
u3
users2 rows
user_id
plan
country
u1
pro
US
u3
free
GB
user_id is unique here, so each event matches exactly one user, no fan-out.
-- Shape: attach each user's plan and country to their events.-- users.user_id is unique, so this one-to-many join does not fan out.selecte.event_id,e.user_id,u.plan,u.countryfromcleaned_eventsejoinusersuonu.user_id=e.user_id;
enriched_events
event_id
user_id
plan
country
e1
u1
pro
US
e4
u3
free
GB
Each event now carries its user’s plan and country, ready for aggregation. Row count is unchanged because the join key was unique.
Enrichment via a join on a unique key keeps the row count stable. If users.user_id were not unique, each event could multiply, a fan-out bug.
Common mistake
Joining without knowing the key’s cardinality. A non-unique key fans out rows, silently multiplying counts and inflating every downstream metric.
Re-joining the same reference data repeatedly downstream. Logic is duplicated and can drift; shape once into an enriched table and reuse it.
Better habit
Know each join’s cardinality before writing it; verify row counts after.
Front-load joins so downstream aggregations stay simple.
Filter early to reduce the data you carry through later steps.
The fan-out trap
Joining on a key that is not unique on the other side multiplies your rows. A count that suddenly doubles after a join is the classic symptom. Always confirm the join is one-to-one or one-to-many, never accidental many-to-many.
Remember this
Shaping combines valid data by filtering, joining, and enriching; the key risk is join fan-out, so always know cardinality and check row counts.
Practice2 prompts
Explain how a join on a non-unique key inflates a count.
Describe how you would verify a join did not fan out.
05 · Job 3
Aggregating: From Rows to Metrics
Aggregation collapses rows into metrics at a chosen grain. State the grain ("one row per country per day") explicitly; it defines what the numbers mean.
⏱ 6 min · Topic 5 of 9
Aggregation is the final job: collapsing many detailed rows into the summary numbers a business reads. Counts, sums, averages, and distinct counts turn thousands of cleaned, shaped events into "daily active users" or "revenue by country". This is where data finally becomes a decision-ready metric.
The example groups enriched events by country and counts distinct users, producing active users per country. Notice it only works because the data was already cleaned (no bots or duplicates to inflate it) and shaped (country was joined in). Aggregation is the payoff of the two jobs before it.
The defining decision in aggregation is the grain: what does one output row represent? "One row per country per day" is a grain. Mixing grains, or aggregating at the wrong one, produces numbers that cannot be compared or summed correctly. State the grain of your output explicitly, it is the single most important thing about an aggregated table.
Core mental model
Aggregation collapses rows into metrics at a chosen grain. State the grain ("one row per country per day") explicitly; it defines what the numbers mean.
Why it matters
Aggregated tables are what executives and dashboards actually consume, so errors here are the most visible and the most consequential. And because aggregation hides the underlying rows, a mistake is very hard to spot from the result alone.
aggregate function
A function that summarises rows: COUNT, SUM, AVG, COUNT(DISTINCT).
grain
What one output row represents (e.g. one row per country per day).
group by
The clause that defines the aggregation grain by grouping rows.
double counting
Inflating an aggregate by counting the same underlying entity more than once.
One day, one dataset, four definitions of revenueAll four are used by real finance teams. They differ by £12,000.£40,076.50
Definition
definition
the number
Gross — everything ordered
£40,076.50
Net — minus refunds
£38,412.00
Recognised — delivered only
£31,880.25
Booked — including pending
£43,905.75
£40,076.50 — and who asks for itThe marketing dashboard, because it measures demand and a refund is not a failure of demand.The trap. Report this as “revenue” beside a finance figure and the two will differ by the refund total, in every meeting, for ever.
Why this belongs in the chapter on transformationThe SQL for all four is three lines and none of it is difficult. What is difficult is that the column is called revenue in all four cases, so the definition lives nowhere except in the head of whoever wrote the model. Aggregation is the stage where the pipeline stops moving data and starts making a claim, and a claim needs a written definition and one implementation.
Aggregating enriched events into a metricworked example
SQL
Input data
enriched_events4 rows
event_id
user_id
country
e1
u1
US
e4
u3
GB
e5
u4
GB
e6
u3
GB
-- Grain: one row per country. Count distinct active users.-- Works only because the data is already clean and shaped.selectcountry,count(distinctuser_id)asactive_usersfromenriched_eventsgroupbycountryorderbyactive_usersdesc;
active_users_by_country
country
active_users
GB
2
US
1
GB has 2 distinct users (u3, u4) despite 3 events, COUNT(DISTINCT) handles u3’s two events. Grain: one row per country.
Count distinct, not count, gives active users, not events. The right aggregate function is part of getting the metric correct.
Common mistake
Using COUNT(*) when you meant COUNT(DISTINCT user). You count events instead of users, overstating the metric, often by a lot.
Aggregating without stating the output grain. Numbers at different grains get mixed or summed incorrectly, and nobody can reconcile them.
Better habit
State the grain of every aggregated table in one sentence.
Choose the aggregate function deliberately (COUNT vs COUNT DISTINCT).
Aggregate only data that is already cleaned and shaped.
Interview note
Stating the grain ("one row per country per day") before writing an aggregation is a strong signal. Interviewers know that grain confusion is the root of most metric disputes.
Remember this
Aggregation collapses clean, shaped rows into metrics at a stated grain; choose the right aggregate function and declare the grain, because the result hides the rows beneath it.
Practice2 prompts
Explain the difference between COUNT(*) and COUNT(DISTINCT user_id) here.
State the grain of a "revenue per product per month" table.
06 · How it runs
Row-by-Row vs Set-Based Thinking
Do not tell the engine how to visit each row; tell it what you want done to all rows, and let it parallelise. Describe the result, not the loop.
⏱ 6 min · Topic 6 of 9
There are two ways to express a transformation, and at data-engineering scale only one of them works. Row-by-row (procedural) processing loops over each row and handles it individually, the way a beginner naturally thinks. Set-based processing describes the change once and lets the engine apply it to all rows at once.
On a thousand rows, the difference is invisible. On a billion rows, it is the difference between seconds and hours, or never finishing. Set-based operations (a single SQL UPDATE, a Spark transformation) let the engine parallelise across all the data; a row-by-row loop forces it through one item at a time, defeating the very engines built to go fast.
The shift to set-based thinking is one of the biggest mindset changes for engineers coming from general programming. The instinct "loop over the rows and do X to each" must become "describe X as a single operation over the whole set". SQL is set-based by design, which is a large part of why it dominates data transformation.
Core mental model
Do not tell the engine how to visit each row; tell it what you want done to all rows, and let it parallelise. Describe the result, not the loop.
Why it matters
Choosing row-by-row at scale is the difference between a transformation that finishes in its window and one that times out. Set-based thinking is not an optimisation; at real volumes it is the only thing that works.
set-based processing
Expressing a transformation as one operation over an entire dataset.
row-by-row / procedural
Looping over rows and processing each individually; slow at scale.
vectorised / parallel execution
The engine applying an operation across many rows at once.
declarative
Describing the desired result (SQL) rather than the step-by-step procedure.
Convert every amount to GBP · one job, three waysAll three are correct. Move the row count and watch two of them stop being usable.1 min
Rows
How it runs
approach
runtime
peak memory
A Python for-loop
1 min
40 MB
pandas, in memory
2 s
540 MB
SQL in the warehouse
1 s
the warehouse’s problem
Fits, and finishesAt this size all three work, which is exactly why the decision gets made by habit. The shape of the curve is what matters: the loop is linear in time and flat in memory, pandas is fast until it is not, and SQL barely notices because the work happens beside the data instead of travelling to it.
Common mistake
Writing a row-by-row loop for a transformation that could be set-based. The job runs orders of magnitude slower and may never finish within its window at scale.
Better habit
Express transformations as set-based operations by default.
Resist the "loop over each row" instinct from general programming.
Reach for procedural row logic only when a transform genuinely cannot be set-based.
Production reality
A transformation rewritten from a row-by-row loop into a single set-based SQL statement routinely goes from hours to minutes. This is one of the highest-leverage skills in transformation work.
Interview note
If you propose iterating over rows to transform a large table, expect pushback. The expected answer is a set-based operation that the engine can parallelise.
Remember this
Express transformations as set-based operations, not row-by-row loops; at real data volumes, describing the result and letting the engine parallelise is the only approach that finishes.
Practice2 prompts
Explain why a row-by-row loop is so much slower than a set-based UPDATE at scale.
Rephrase "for each row, if status=paid add amount to total" as a set-based aggregate.
07 · Made real
Where Each Transformation Runs, and What Runs It
Bronze may not change what arrived, silver may not throw away rows, gold may not be the only copy — each layer is defined by what it is forbidden to do.
⏱ 8 min · Topic 7 of 9
Clean, shape and aggregate are not just an order — they are three different places in a pipeline. Chapter 2, The Data Pipeline Lifecycle, named those places bronze, silver and gold; this topic walks one day of orders through them with the code that runs at each.
Then two tables: every transformation you will meet and the layer it belongs to, and every tool that can run one, with what it costs.
Core mental model
Bronze may not change what arrived, silver may not throw away rows, gold may not be the only copy — each layer is defined by what it is forbidden to do.
Why it matters
Most transformation bugs are not wrong logic. They are correct logic at the wrong layer, where it either destroys evidence or gets rewritten by every team downstream.
the layer contract
What a layer is forbidden to do: bronze may not change what arrived, silver may not throw away rows, gold may not be the only copy of anything.
materialisation
Whether a transformation is stored as a table, rebuilt each run, or left as a view. It decides who pays the compute — the pipeline once, or every reader.
One day of Marlow's orders41,206 records in. Walk the three layers and watch what each one is allowed to change.41,206 typed rows
marlow-bronzeAS RECEIVEDTyped. Same row count.
PySparkgrain — one row per record as received
Land it exactly as it arrived, then make it readable without changing what it says.
in
out
41,206 raw JSON records
41,206 typed rows
· Cast text to real types
· Parse timestamps into UTC
· Add ingested_at and the source file name
· Quarantine records that will not parse
What runs here · PySpark
# bronze: readable, not corrected. Row count must not change.
raw = (spark.read
.schema(orders_raw_schema)
.option("badRecordsPath", "s3://marlow-quarantine/orders/")
.json("s3://marlow-drop/orders/dt=2026-08-07/"))
bronze = (raw
.withColumn("total", col("total").cast("decimal(12,2)"))
.withColumn("ordered_at", to_utc_timestamp(col("ordered_at"), "Europe/London"))
.withColumn("ingested_at", current_timestamp())
.withColumn("source_file", input_file_name()))
bronze.write.mode("overwrite").partitionBy("dt").parquet("s3://marlow-bronze/orders/")
Do this work at the wrong layer and…Deduplicate here and you have destroyed the evidence. Bronze is the copy you re-run from when a silver rule turns out to be wrong — the moment it stops matching what arrived, it cannot do that job.
Window functions are the set-based way to keep one row per key. The row-by-row alternative is the loop this chapter warns about, and it is thousands of times slower at this size.
Identical logic, no cluster. dbt turns a SELECT into a managed, tested, dependency-ordered table — which is why most silver and gold work lives here rather than in Spark.
When the data is small: Python, and that is fineworked example
A few hundred thousand rows on one machine does not need a cluster. Reaching for Spark here costs money and adds a thing to operate; the honest answer is often twenty lines of pandas.
Scala, for the Spark documentation you will end up readingworked example
The same window-function dedupe. Worth seeing once so a Scala answer on a forum is readable.
The transformations you will actually write, and where each belongs
Transformation
What it does
Layer
Typically written in
Cast / parse types
Text becomes numbers, dates and booleans
Bronze
Spark, or the loader
Normalise timestamps
Everything to UTC, one format
Bronze
Spark, SQL
Quarantine bad records
Unparseable rows set aside, not dropped
Bronze
Spark (badRecordsPath)
Deduplicate
One row per business key
Silver
SQL window function
Filter invalid rows
Negative totals, test accounts, nulls in keys
Silver
SQL, Spark
Join reference data
Attach customer, product, region, FX rate
Silver
SQL, Spark
Standardise values
One spelling of a status, one currency
Silver
SQL
Derive columns
margin = revenue − cost, is_first_order
Silver
SQL
Slowly changing dimensions
Keep history when an attribute changes
Silver
dbt snapshot, MERGE
Aggregate to a grain
Daily revenue per region
Gold
SQL, dbt
Apply business definitions
What counts as active, as revenue
Gold
SQL, dbt
Pivot / reshape for a tool
Wide table a dashboard can read fast
Gold
SQL
What can run a transformation — pros, cons and what it costs
Tool
Best for
Pros
Cons
Cost model
Warehouse SQL / dbt
Anything already in the warehouse
No cluster; everyone reads SQL; tests, lineage and docs come with it
Only SQL; only data already loaded
Warehouse compute per query — you already pay for it
Spark (Databricks, EMR, Glue)
Data too big for one machine, or files before loading
Scales horizontally; handles semi-structured files; Python, SQL and Scala
A cluster to size, tune and operate; slow to start for small jobs
Per cluster-minute — idle time is still billed unless serverless
Python (pandas, Polars, DuckDB)
Up to a few million rows on one machine
Fastest to write; no infrastructure; trivial to test locally
Bounded by one machine’s memory; easy to write accidentally row-by-row
The machine it runs on — often cents
Flink / Spark Structured Streaming
Transformations that cannot wait for a batch
Continuous; windowing and state built in
Always on; state, checkpoints and restarts to own
Always-on compute, billed whether events arrive or not
Stored procedures
Logic that must live beside an operational database
No extra system; runs where the data already is
Hard to version, test and review; invisible to lineage tools
Included in the database licence — and in its blast radius
Managed transform (Fivetran, Coalesce, Matillion)
Standard modelling with a small team
Fast to stand up; GUI-driven; vendor maintains connectors
Vendor lock-in; awkward for logic that is not standard
Per row, per connector or per seat — grows with volume
Common mistake
Deduplicating in bronze because the duplicates are annoying. The one copy that matched the source is gone, so a wrong silver rule can no longer be re-run against what actually arrived.
Aggregating in silver to make gold cheaper. The rows behind the number no longer exist, and the first question about any total is which rows produced it.
Choosing Spark for a few hundred thousand rows. You pay cluster-minutes and gain an operational surface for a job pandas would finish in seconds.
Better habit
Ask which layer a transformation belongs to before deciding what to write it in.
Write it once in silver so no consumer has to write it again.
Match the tool to the data size, not to what looks impressive on the diagram.
Keep bronze immutable — it is the only thing that makes a mistake recoverable.
The tool is the last decision, not the first
Layer, then grain, then tool. Teams that pick the tool first end up writing SQL-shaped logic in Spark, or shipping a cluster to do a job the warehouse was already paid for.
Interview note
Asked how you would transform something, answer with a layer before a technology — "dedupe belongs in silver, so it runs once for every consumer; at this volume that is warehouse SQL, not a cluster."
Remember this
Each layer is defined by what it may not do, and the tool follows from the layer and the data size — clean once in silver, aggregate in gold, and never edit bronze.
Practice2 prompts
Take a transformation your team runs and name the layer it lives in. If it runs in more than one place, say which one should own it.
Price the same job two ways — the warehouse you already pay for, and a cluster you would have to start.
08 · Practice
Practice Lab
Clean, shape, aggregate. Say which of the three a piece of logic is, and where it should run follows.
⏱ 3 min · Topic 8 of 9
Five scenarios where the transform is where the bug lives — and in four of them nothing anywhere raises an error.
Build them on the pipeline canvas with the chapter closed. Each is graded against the scenario’s own requirement rules rather than against a model answer, so there is more than one design that passes — and a design that does not pass is told exactly which requirement it missed.
Core mental model
Clean, shape, aggregate. Say which of the three a piece of logic is, and where it should run follows.
Why it matters
Transformation is the only stage whose failures are invisible: a broken extract fails loudly and a broken transform publishes a plausible number. Building these is how you learn to distrust a clean run.
Common mistake
Cleaning the same field in three different models, so the definition of a customer name depends on which table you read. The review will find it, but only after you have submitted a design you believed in — which is the point. That is the memory that survives interview pressure.
Revealing the reference design before your own review comes back. You see what correct looks like without finding out what your version got wrong, and your version is the one you will draw again under pressure.
Better habit
After any join, count the rows on both sides. A transform that changes the row count without meaning to is the most common silent bug there is.
Run the review, fix what it finds, and run it again. The second score is the one that means something.
Narrate the finished design out loud in ninety seconds. If you cannot, the design has a hole you have not found yet.
Joins are where transformation deletes data by accident. Both join scenarios below are valid rows, valid keys, and rows missing at the far end.
These are the round, not a warm-up
Five graded design scenarios, each with staged requirement checks and interview probes of its own. Working them out loud, against a clock, is the closest rehearsal to the real thing this module offers.
Remember this
A chapter read is a chapter you can recognise; a scenario built and reviewed is one you can use. Close this and go build.
Practice2 prompts
Before opening any scenario, write down the freshness requirement and who consumes the output.
After each review, write one sentence naming the requirement you missed and why you missed it.
09 · Next Chapter
Next Chapter
You can now turn raw data into trustworthy metrics through clean, shape, and aggregate. The final step is writing those results into the destination, and how you write matters as much as what you write.
⏱ 3 min · Topic 9 of 9
Next chapter
Loading & Write Patterns
You can now turn raw data into trustworthy metrics through clean, shape, and aggregate. The final step is writing those results into the destination, and how you write matters as much as what you write.
Chapter 9 covers loading and write patterns: append, upsert/merge, and overwrite; idempotent loads that survive retries; and bulk versus row-by-row writes.