ETL vs ELT and pipeline design
Learn the difference between ETL and ELT, batch and streaming, and the habits that make pipelines reliable: incremental loads, idempotency, backfills, late data and failing loudly. Core knowledge for interviews and real jobs.
Every pipeline does three things: Extract data from a source, Transform it, and Load it into a destination. The order of those letters, and where the transformation happens, is the difference between ETL and ELT. This lesson explains both, then the design habits that separate a pipeline that works once from one you can trust for years: incremental loads, idempotency, backfills, late data and failure handling.
ETL and ELT
ETL (extract, transform, load): data is pulled from the source, cleaned and reshaped by a separate processing step (a Python script, Spark job or an ETL tool), and only the final result is loaded into the warehouse. This was the norm when warehouses were expensive and slow, so you wanted to load only what was needed.
ELT (extract, load, transform): data is loaded into the warehouse or lake almost as it is, and the transformation runs inside the warehouse using SQL. Cloud warehouses are powerful and storage is cheap, so keeping the raw data and transforming it later is now the common approach. Tools like dbt exist for exactly this (see *Transformations with dbt*).
ETL vs ELT
| Question | ETL | ELT |
|---|---|---|
| Where does transformation run? | In a separate engine before loading | Inside the warehouse or lakehouse after loading |
| Is raw data kept? | Often not | Yes, in a raw layer |
| Re-run with new logic? | Re-extract from the source, which may be impossible | Re-run the SQL on the stored raw data |
| Main language | Python, Spark, Java, ETL tools | SQL (plus Python where needed) |
| Good for | Heavy non-SQL processing, masking data before it lands, small destinations | Analytics on cloud warehouses, fast iteration by analysts |
| Typical tools | Python scripts, Spark, Informatica, Talend | Fivetran or Airbyte to load, dbt to transform |
They are not rivals. Many real pipelines are a mix: a small Python step that parses files or removes personal data (ETL-style) before loading, then SQL models inside the warehouse (ELT-style). Interviewers want to hear that the choice depends on where the compute is cheapest and what must happen before data is stored.
ELT is not always allowed
Sometimes data must be transformed before it is stored, for example to remove or mask personal data such as phone numbers when the warehouse should not hold them. Loading raw data first is not always allowed. See *Governance, privacy and the DPDP Act*.
Batch and streaming
Batch vs streaming
| Style | How it runs | Latency | Example |
|---|---|---|---|
| Batch | Processes a chunk of data on a schedule (hourly, daily) | Minutes to hours | Nightly sales table for the owner's morning report |
| Micro-batch | Very small batches, every few seconds or minutes | Seconds to minutes | Updating a dashboard every minute |
| Streaming | Processes each event as it arrives | Sub-second to seconds | Fraud alert on a UPI payment |
Start with batch. It is simpler, cheaper, easier to test and easier to fix by re-running. Move to streaming only when the business really needs results faster than batch can give. Streaming is covered in *Streaming with Apache Kafka* and *Stream processing*.
Full loads and incremental loads
A full load copies the whole source table every run. It is simple and always correct but gets slow and expensive as data grows. An incremental load copies only what is new or changed since last time. To do that you need a way to know what changed. There are three common ways.
How to load incrementally
| Method | How it works | Catch |
|---|---|---|
| Watermark column | Keep the highest updated_at (or id) you loaded; next run asks for rows greater than that | Needs a reliable updated_at; misses hard deletes |
| Change data capture (CDC) | Read the database's change log (inserts, updates, deletes) | More setup; tools such as Debezium do this |
| Partition by date | Load one day's partition at a time, e.g. all orders for 2026-10-01 | Works well for event data that is not edited later |
-- Run for each batch; :last_watermark is saved from the previous successful run
SELECT order_id, customer_id, status, total, updated_at
FROM orders
WHERE updated_at > :last_watermark
ORDER BY updated_at;After the load succeeds, store the max(updated_at) you saw as the new watermark. Store it only after success, otherwise a failed run would skip rows.
Late data
If rows can arrive with an older timestamp than your watermark (a slow phone upload, a retried job), a strict watermark will miss them. Common fixes: re-read a small overlap window (for example the last 2 days) and make the load idempotent, as described next.
Idempotency: safe to run twice
Pipelines fail. Networks drop, servers restart, someone re-runs a job by mistake. A pipeline is idempotent if running it twice with the same input leaves the same result as running it once. Without it, a re-run after a crash duplicates rows and quietly doubles your revenue.
The simplest idempotent patterns are below.
Idempotent load patterns
| Pattern | How it works | Good for |
|---|---|---|
| Replace the partition | In one transaction, delete the day's rows then insert the new ones | Daily or hourly batch loads |
| Upsert / MERGE | Insert new rows, update existing ones by key | Tables where rows change, keyed by id |
| Write to a temp table, then swap | Build the full result separately, then replace the live table in one step | Small and medium tables |
-- Pattern 1: replace the partition
BEGIN;
DELETE FROM order_lines WHERE order_date = '2026-10-01';
INSERT INTO order_lines SELECT * FROM staging_order_lines;
COMMIT;
-- Pattern 2: upsert by key (PostgreSQL and SQLite 3.24+)
INSERT INTO orders (order_id, status, total)
SELECT order_id, status, total FROM staging_orders WHERE true
ON CONFLICT (order_id) DO UPDATE
SET status = excluded.status, total = excluded.total;Run either one twice and the table is the same. A plain INSERT run twice would duplicate rows. The WHERE true is needed in SQLite to avoid a parsing ambiguity and is harmless elsewhere. Warehouses such as BigQuery and Snowflake use MERGE for the upsert.
Add a unique key and let the database enforce it. A primary key on (order_id, item_id) turns an accidental duplicate into an error you can see, rather than a wrong number you cannot.
Backfills and re-runs
A backfill re-runs a pipeline for past dates, for example after fixing a bug or adding a new column. If each run takes a date as a parameter (--date 2026-10-01) and is idempotent, a backfill is just a loop over dates. Hard-coding "yesterday" inside the script makes backfills painful. Always pass the date in.
Design checklist for any pipeline
A pipeline you can trust
- Keep a raw copy of what you received before changing it.
- Make the run take its date or window as a parameter.
- Make the load idempotent (replace partition or upsert).
- Validate data on the way in: types, nulls, allowed values, row counts. Send bad rows to a rejects table instead of dropping them silently.
- Retry temporary failures (network errors) with a wait between attempts, but do not retry forever.
- Fail loudly: a failed run must alert someone. Never swallow exceptions.
- Log counts at each step: rows read, rows rejected, rows loaded.
- Handle schema changes: decide what happens when the source adds, renames or removes a column.
- Be able to answer: how do I know today's data is complete and correct?
Orchestration
A real pipeline has many steps that depend on each other: extract, then validate, then transform, then publish. An orchestrator runs the steps in the right order on a schedule, retries failures, shows what failed and lets you re-run one date. Apache Airflow is the most widely used; Dagster, Prefect and cloud services such as Cloud Composer, Azure Data Factory and AWS Step Functions do similar jobs. Cron works for one simple script. You build an Airflow pipeline in *Orchestration with Apache Airflow*.
How this comes up in interviews
Common questions
- "ETL vs ELT: what is the difference and why has the industry moved to ELT?"
- "How do you load only new data every day?" (Watermark, CDC, partitions, and their catches.)
- "What does idempotent mean, and how do you make a load idempotent?"
- "The job failed halfway. What state is the data in and how do you recover?" (Transaction or partition replace, then re-run.)
- "How would you handle late-arriving data?"
- "Design a pipeline that loads daily orders from PostgreSQL into a warehouse."
You are a senior data engineer reviewing a pipeline design. Check it against these questions: is it idempotent, how does it handle late data and schema changes, what happens if it fails halfway, how are bad rows handled, and how would I backfill 30 days? Give concrete fixes.
Source: [system and table]
Destination: [warehouse or lake]
Volume and schedule: [rows per day, how often it runs]
Current design: [describe each step, and how you load: full, watermark, CDC]What to expect: Look for specific fixes such as a replace-partition load, an overlap window on the watermark and a rejects table, not generic advice.
Try it: Make this pipeline safe
A nightly job runs INSERT INTO daily_sales SELECT date, SUM(total) FROM orders WHERE date = CURRENT_DATE - 1 GROUP BY date. Name three things that go wrong in production, and rewrite the design to fix them.
Try it: Choose ETL or ELT
For each, say ETL, ELT or a mix, and why: (a) loading Shopify-style order exports into BigQuery for analysts, (b) loading customer data that includes Aadhaar-like or phone identifiers into a shared warehouse where most staff should not see them, (c) parsing 50,000 PDF invoices into rows.
Key takeaways
- ETL transforms before loading; ELT loads first and transforms inside the warehouse. Modern cloud stacks mostly use ELT, often with a small ETL-style step for parsing or privacy.
- Start with batch; use streaming only when the business needs the speed.
- Incremental loads use a watermark, CDC or date partitions, each with a catch (late data, deletes).
- Idempotent loads (replace the partition, or upsert) make re-runs and backfills safe.
- Pass dates into runs, keep raw data, validate, log counts, retry temporary errors and fail loudly.
- Use an orchestrator once there is more than one dependent step.
Check yourself
Finishing the quiz marks the lesson as done.
1. What is the key difference between ETL and ELT?
2. A daily job inserts yesterday's sales rows. It crashes at the end, is re-run, and revenue now looks doubled. What property was missing?
3. You load incrementally with WHERE updated_at > last_watermark. Some rows arrive a day late with an old updated_at. What happens?
4. When should the watermark be saved?
5. Why should a pipeline script take the run date as a parameter instead of using 'yesterday'?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.