learn-data-warehouse-with-phoebe / Builder session 6 of 10
Learn Data Warehouse with Phoebe · Builder track · Session 6 of 10

Loading patterns

Daybreak's warehouse has shape - staging, a star, history-keeping dimensions. Tonight it gets a heartbeat. New orders land every day, and someone (you) has to move them in: ETL or ELT, full reload or incremental, and above all safely re-runnable - because your pipeline WILL crash mid-run one night, and the difference between a shrug and a all-hands incident is how you loaded.

🟠 Builder track Practitioners: analysts · DE · DS · PMs Runs in your browser · DuckDB 45 min
0-3 · Recap 3-20 · ETL/ELT & incremental 20-42 · Build-along: MERGE loads, twice 42-45 · Q&A
Part 0

Where the build stands

The Daybreak warehouse so far: a typed staging layer (b2), a star schema with fct_order_line and three dimensions (b3-b4), and a Type 2 customer dimension that keeps history honest (b5). Everything you built used CREATE TABLE AS over a frozen snapshot. Real warehouses do not get frozen snapshots - they get tonight's batch, every night, forever. Loading patterns are the difference between a warehouse and a one-off analysis.

Live - presented in session Self-study - read after class ▶ Live warehouse - editable & runnable Official sources covered
★ What you walk out with today The ETL vs ELT distinction (and why the industry flipped), the full-reload vs incremental trade, and the one property that separates professional pipelines from hopeful ones: idempotency - proven live by running the same MERGE twice and watching nothing double-count.
Part 1 · covers IBM DW Fundamentals M2 populate labs, 365DS S5 ETL, DLAI C4 dbt demos

ETL vs ELT: where does transform run? 7 min live

Every load answers three verbs: Extract data out of the source, Transform it into warehouse shape, Load it in. The only real question is the order of the last two - where the transform happens. That one choice defines two eras of data engineering.

ETL · the old default (warehouse compute was scarce) Source (OLTP) Transform server outside · custom code Warehouse receives clean data only Raw data never lands. Debugging means re-extracting. Transform logic lives in a separate system. ELT · the cloud default (warehouse compute got cheap) Source (OLTP) Warehouse - everything after extract happens in here, in SQL raw / landed staging (b2) star (b3-b4) Raw lands first, so every step is inspectable and re-runnable with a query. Transforms are just SQL. Your b2-b4 CTAS scripts were ELT all along. Cheap columnar compute flipped the whole industry this way.
🔍 Click to zoom - same three verbs, opposite homes for the T
LiveYou have been doing ELT since b24 min

Surprise: this course never taught you ETL, because you never needed it. The raw Daybreak tables land in the engine untouched, and every layer - staging, dimensions, facts - is a SQL transform running inside the warehouse. That is the whole ELT idea, and here it is in one breath:

CREATE OR REPLACE TABLE monthly_revenue AS
SELECT strftime(o.order_date, '%Y-%m') AS month,
       ROUND(SUM(oi.line_amount), 2) AS revenue
FROM stg_orders o
JOIN stg_order_items oi USING (order_id)
WHERE o.status = 'completed'
GROUP BY month;

SELECT * FROM monthly_revenue ORDER BY month;
  • ETL wins when data must be cleaned or masked before it may land anywhere (strict PII regimes), or when the target engine is weak.
  • ELT wins almost everywhere else: raw is preserved for audit and re-processing, transforms are versionable SQL, and the warehouse's own horsepower does the lifting.
Self-studydbt in one honest paragraph3 min read

Once a team runs dozens of in-warehouse SQL transforms, managing them by hand collapses. dbt is the tool that grew to fill that gap: each of your CTAS scripts becomes a "model" file, dbt figures out the dependency order, runs them, tests them (unique keys, no NULLs, accepted values), and generates docs and lineage automatically. The DeepLearning.AI course demos it; the mental model you need tonight is simply "dbt industrializes what I just did by hand." Depth belongs to a data-engineering course - here it is one card, on purpose.

Part 2 · full reload vs incremental

Full reload or incremental? 7 min live

Tonight's batch arrives. Do you rebuild the whole fact table from scratch, or move only what changed? Both are legitimate. The trade is simplicity versus cost, and the honest answer changes as the data grows.

Full reload · drop and rebuild, every night All source CREATE OR REPLACE TABLE fct ... Fresh fact table Simple and self-healing: any past mistake is erased by tonight's rebuild. Cost grows with ALL history - fine at 53 rows, brutal at 5 billion. Batch window: the whole table is rebuilt whether 1 row changed or none. Incremental · move only what is new or changed Source watermark: rows past max(date_key), or MERGE on keys Fact table, patched Cheap and fast: cost grows with the CHANGE, not the history. Needs care: a watermark or a merge key, plus a plan for late-arriving rows (yesterday's order landing tonight - MERGE by key absorbs them). Rule of thumb: start full, stay full while you can. Go incremental when the bill or the batch window says so.
🔍 Click to zoom - rebuild everything vs patch the difference
LiveWatermarks, late arrivals, and the batch window4 min
  • Watermark: the highest value already loaded - usually max(date_key) or a max updated-at timestamp. Tonight's extract asks the source only for rows past it.
  • Late-arriving data: an order from Tuesday that reaches you Thursday. A naive date watermark skips it forever; a MERGE keyed on business keys absorbs it whenever it shows up. This is the main reason MERGE beats plain INSERT for incremental loads.
  • Batch window: the hours (often overnight) when the load must finish before dashboards wake up. Full reloads eat the window as history grows; incremental loads keep it flat.
Real world

The 6am cliff. A retailer's full nightly rebuild took 40 minutes in year one, 5 hours in year three, and one Black Friday it was still running when the 6am dashboards opened - empty. The migration to incremental MERGE loads was scheduled that same week. Growth converts "simple and correct" into "simple and late".

Part 3 · the re-run property

Idempotency: design for the re-run 5 min live

A load is idempotent when running it twice leaves the warehouse in exactly the same state as running it once. It sounds academic until 2am, when the pipeline dies halfway through and the on-call question is: "can I just run it again?" If the answer is yes, the incident is a shrug. If the answer is "maybe, but revenue might double", the incident has a postmortem.

LiveWhy plain INSERT fails the 2am test3 min

Picture the crash: tonight's batch INSERTs 500 of 1,000 rows, then the connection drops. Re-run the whole batch and the first 500 rows land twice - revenue double-counts, and worse, it double-counts silently. The pattern that survives the crash:

  • Key every target row by its business identity - for fct_order_line, the pair (order_id, product_key) names one line exactly once.
  • MERGE, not INSERT: rows already present get updated (or left alone), rows missing get inserted. Replaying a half-finished batch simply completes it.
  • Same idea, coarser grain: delete-then-insert the affected date partition, or full reload. All three are idempotent; MERGE is the finest-grained.

Demo 2 does not ask you to trust this - you will run the same load twice and count the rows yourself.

Demo 1 of 2

Tonight's batch, loaded incrementally ★ 12 min · everyone builds

Tonight's batch from the Daybreak app carries three rows: two brand-new order lines (orders 1034 and 1035) and one correction - order 1032's line was refunded this afternoon. One MERGE, keyed on (order_id, product_key), handles all three.

Read the batch in the USING clause: it is just VALUES - in production this would be tonight's extract or a staging table, same shape.

Read the ON clause: (order_id, product_key) is the business identity of a fact row. MERGE checks each batch row against it.

Trace each row's fate: 1034 and 1035 find no match - INSERT. 1032's line finds its match - UPDATE flips its status to refunded. No row is ever written twice.

Run it. The result shows the three touched rows plus the counts: 53 rows before, 55 after - two inserts, one in-place update.

CREATE TABLE cnt_before AS
SELECT count(*) AS n FROM fct_order_line;

MERGE INTO fct_order_line t
USING (
  SELECT * FROM (VALUES
    (9, 2, DATE '2026-06-28', 1034, 2, 18.00, 36.00, 'completed', 'app'),
    (4, 5, DATE '2026-06-29', 1035, 1, 28.00, 28.00, 'completed', 'web'),
    (1, 1, DATE '2026-06-16', 1032, 3, 16.00, 48.00, 'refunded',  'web')
  ) v(customer_key, product_key, date_key, order_id,
      quantity, unit_price, line_revenue, status, channel)
) src
ON t.order_id = src.order_id AND t.product_key = src.product_key
WHEN MATCHED THEN
  UPDATE SET status = src.status
WHEN NOT MATCHED THEN
  INSERT VALUES (src.customer_key, src.product_key, src.date_key,
                 src.order_id, src.quantity, src.unit_price,
                 src.line_revenue, src.status, src.channel);

SELECT f.order_id, f.product_key, f.date_key, f.line_revenue, f.status,
       b.n AS rows_before,
       (SELECT count(*) FROM fct_order_line) AS rows_after
FROM fct_order_line f, cnt_before b
WHERE f.order_id IN (1032, 1034, 1035)
ORDER BY f.order_id;
Late-arrival bonus. Notice the batch is allowed to contain order 1032 from June 16 - twelve days "late" - and nothing breaks. Key-based MERGE does not care when a row arrives, only what it is. A pure date watermark would have missed it.
Demo 2 of 2

The idempotency proof - then break it ★ 10 min · everyone builds

Claims are cheap; counts are not. This script runs the exact same MERGE twice in a row - simulating the 2am "just run it again" - and lets the row counts testify.

LiveRun the same load twice, count the damage: zero5 min
MERGE INTO fct_order_line t
USING (SELECT * FROM (VALUES
    (9, 2, DATE '2026-06-28', 1034, 2, 18.00, 36.00, 'completed', 'app'),
    (4, 5, DATE '2026-06-29', 1035, 1, 28.00, 28.00, 'completed', 'web')
  ) v(customer_key, product_key, date_key, order_id,
      quantity, unit_price, line_revenue, status, channel)) src
ON t.order_id = src.order_id AND t.product_key = src.product_key
WHEN MATCHED THEN UPDATE SET status = src.status
WHEN NOT MATCHED THEN
  INSERT VALUES (src.customer_key, src.product_key, src.date_key,
                 src.order_id, src.quantity, src.unit_price,
                 src.line_revenue, src.status, src.channel);

CREATE TABLE after_first AS
SELECT count(*) AS n FROM fct_order_line;

MERGE INTO fct_order_line t
USING (SELECT * FROM (VALUES
    (9, 2, DATE '2026-06-28', 1034, 2, 18.00, 36.00, 'completed', 'app'),
    (4, 5, DATE '2026-06-29', 1035, 1, 28.00, 28.00, 'completed', 'web')
  ) v(customer_key, product_key, date_key, order_id,
      quantity, unit_price, line_revenue, status, channel)) src
ON t.order_id = src.order_id AND t.product_key = src.product_key
WHEN MATCHED THEN UPDATE SET status = src.status
WHEN NOT MATCHED THEN
  INSERT VALUES (src.customer_key, src.product_key, src.date_key,
                 src.order_id, src.quantity, src.unit_price,
                 src.line_revenue, src.status, src.channel);

SELECT (SELECT n FROM after_first) AS rows_after_first_run,
       count(*) AS rows_after_second_run,
       (SELECT count(*) FROM fct_order_line
        WHERE order_id = 1034) AS copies_of_1034
FROM fct_order_line;
LiveYour turn: break it on purpose3 min

Now be the naive pipeline: swap MERGE for plain INSERT and replay the batch. Two copies of order 1034, and its 36.00 counted as 72.00. This is the exact bug that idempotency exists to make impossible.

INSERT INTO fct_order_line VALUES
  (9, 2, DATE '2026-06-28', 1034, 2, 18.00, 36.00, 'completed', 'app');

INSERT INTO fct_order_line VALUES
  (9, 2, DATE '2026-06-28', 1034, 2, 18.00, 36.00, 'completed', 'app');

SELECT order_id,
       count(*) AS copies,
       SUM(line_revenue) AS revenue_counted
FROM fct_order_line
WHERE order_id = 1034
GROUP BY order_id;
Self-studyThe watermark pattern in one query3 min

Incremental extraction starts by asking the warehouse what it already has. max(date_key) is the high-water mark; the source is then asked only for rows past it. Tonight the answer is zero new orders - the warehouse is caught up. Tomorrow it will not be.

SELECT (SELECT max(date_key) FROM fct_order_line) AS high_water_mark,
       count(*) AS source_rows_past_watermark
FROM stg_orders
WHERE order_date > (SELECT max(date_key) FROM fct_order_line);

Caveat from Part 2: a date watermark alone misses late arrivals. Production incremental loads typically pair a generous watermark (for cheap extraction) with a keyed MERGE (for correct landing).

Homework

Try it yourself - this week ◐ 20-30 min total

Source material

Official sources covered

This track teaches the working core of the major data-warehousing curricula, run on a live engine instead of slides. Certificates, graded labs, and videos stay on the official platforms. This page covers:

IBM Data Warehouse Fundamentals (Coursera) - M2: populating the warehouseDemos 1-2 · the live twin of IBM's populate labs, on MERGE
365DS Intro to Data Warehousing - S5: ETL, loading & automationParts 1-3 · ETL/ELT, full vs incremental, re-runnable loads
DeepLearning.AI Data Engineering C4 (Joe Reis) - M1: dbt & transformation toolingPart 1 self-study card · one honest paragraph; depth belongs to a DE course
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why did the industry largely flip from ETL to ELT?

Columnar cloud engines turned the warehouse into the strongest computer in the room. Land raw, keep it for audit and replay, transform with versionable SQL inside - exactly what your CTAS layers have done since b2.

2 · A load is idempotent when...

The property is about the END STATE under replay. Crashes guarantee re-runs will happen; idempotent design (MERGE on business keys, or partition replace) makes the re-run boring instead of a double-counting incident.

3 · In tonight's MERGE, what happens to a batch row on match vs no match?

MERGE checks each source row against the ON keys - (order_id, product_key) here. WHEN MATCHED updates the existing fact row (the 1032 correction); WHEN NOT MATCHED inserts it (1034, 1035). One statement, both fates.

Builder session 6 cheat sheet · pin this

ETL vs ELTSame three verbs; the question is where transform runs. ELT = load raw, transform in-warehouse with SQL. The cloud default.
dbtIndustrializes ELT: your CTAS scripts as tested, versioned, documented models with automatic dependency order.
Full reloadDrop and rebuild nightly. Simple, correct, self-healing - and cost grows with all of history. Start here.
IncrementalMove only new/changed rows. Cost grows with the change. Needs a watermark or merge key, and a late-arrival plan.
Watermarkmax(date_key) or max updated-at already loaded; extract only past it. Pair with MERGE - dates alone miss late rows.
IdempotencyRun twice = same state as once. The 2am test: "can I just run it again?" Design so the answer is always yes.
MERGEON business keys; WHEN MATCHED update, WHEN NOT MATCHED insert. The idempotent workhorse of incremental loading.
Plain INSERT replayedDouble-counts silently - you proved it live (copies: 2, revenue doubled). Never ship it as a load.