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.
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.
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.
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.
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.
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".
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.
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;
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).
Try it yourself - this week ◐ 20-30 min total
- Extend Demo 1's batch with a row that changes order 1034's quantity from 2 to 3 - you will need the UPDATE branch to set more than status. (Update quantity and line_revenue together, or the row lies.)
- Write the full-reload version of the fact load as one CTAS over staging (b4's script is your template) and time it against the MERGE. At Daybreak's size, full reload wins on simplicity - say out loud when that flips.
- Classify three pipelines at work (or imagine them): ETL or ELT? Full or incremental? Idempotent - would you dare re-run each one at 2am?
- Design the merge key for a fact table of website events (customer, url, timestamp). Harder than it looks - write down what makes an event row unique, and what happens if two identical events are both real.
- Bring your answer to b7, where the loaded warehouse finally starts serving the business.
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:
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.