Fabric Loading Patterns: Full, Incremental, and Dimensional Model Loads
A loading pattern defines how new and changed data moves from a source into a Fabric destination: a full load replaces the target every run, an incremental load applies only rows that changed since the last run, and a streaming load appends events continuously as they arrive. DP-700 expects you to pick the right pattern for a scenario and implement it — tracking change with watermark columns, Copy job, Dataflow Gen2 incremental refresh or Change Data Capture, applying changes idempotently with <code>MERGE</code>, and shaping the result for a dimensional model with surrogate keys and slowly changing dimensions. This lesson works through each pattern in a Fabric Lakehouse and Warehouse context: when a full truncate-and-reload is genuinely the better choice, how a high-water-mark incremental load is built so it never skips rows, how upserts and deletes land in silver and gold tables, and how a streaming pattern differs by being append-only through the medallion layers.
On this page12 sections
- Full loads vs incremental loads: how to choose
- Implementing a watermark-based incremental load
- Incremental loads with Copy job and Dataflow Gen2 incremental refresh
- Change Data Capture and upserts with Delta MERGE
- Propagating deletes: full snapshots, soft-delete flags and key reconciliation
- Idempotent loads: making reruns and re-deliveries safe
- Preparing data for a dimensional model: the star schema
- Surrogate keys and slowly changing dimensions
- Implementing SCD Type 2 in Fabric
- Load order and late-arriving dimension members
- Loading patterns for streaming data
- Scenario: designing the loads for a retail Lakehouse
- Choose between full and incremental loads for a given source size, change rate and change-detection capability
- Implement a watermark-based incremental load that brackets each run with an old and a new bound, survives failures and handles late-committing rows
- Choose between a pipeline, Copy job incremental copy and Dataflow Gen2 incremental refresh for an incremental load
- Apply change sets with MERGE in a Lakehouse or Warehouse, deduplicating to the latest change per key and propagating deletes
- Design loads that are idempotent when a run is retried or a batch is re-delivered
- Prepare source data for a star schema: grain, flattened dimensions, conformed dimensions and surrogate keys
- Choose SCD Type 0, 1, 2 or 3 from the requirement and implement Type 2 correctly, including fact-key lookups by date
- Design an append-only streaming loading pattern, choose its landing store and deduplicate events downstream
Full loads vs incremental loads: how to choose
Choose a full load when the dataset is small or the source cannot tell you what changed; choose an incremental load when the source is large and changes can be identified reliably. A full load truncates (or overwrites) the target and rewrites every row on every run — simple, self-healing, and idempotent, but the cost grows linearly with source size and it burns capacity re-processing data that never changed. An incremental load extracts only rows created or modified since the previous run, so runtime stays proportional to the change volume rather than the table size.
The trade-off is complexity. Incremental loads need a way to detect change (a watermark column or Change Data Capture), somewhere to store load state between runs, and a strategy for applying changes to the target (append or merge). They also need a recovery story: if state is lost or logic drifts, you typically fall back to a one-off full reload to re-baseline.
| Factor | Full load | Incremental load |
|---|---|---|
| Source size | Small to moderate | Large, growing |
| Change detection in source | Not required | Required (watermark column or CDC) |
| Run cost | Proportional to table size | Proportional to changed rows |
| Handles deletes | Automatically (target rebuilt) | Only with CDC or soft-delete flags |
| Complexity | Low — idempotent by design | Higher — state, merge logic, recovery |
| Typical use | Reference data, small dimensions, re-baselining | Fact tables, large transactional sources |
A full load only removes deleted rows if it actually replaces the target. Loading a full snapshot with an insert-and-update MERGE keeps every row the source has dropped, because the merge never visits them — see the section on propagating deletes.
In practice a solution mixes both: full-load the small dimension sources nightly, incrementally load the large fact sources, and keep a documented full-reload path for recovery.
Implementing a watermark-based incremental load
A watermark (high-water-mark) load filters the source on a column whose value only increases when a row is inserted or changed — typically a last-modified timestamp, a ROWVERSION or an ever-increasing ID — and pulls only rows above the value recorded by the previous run. It has two prerequisites: the column must be maintained reliably on every insert and update, and the last watermark must be persisted somewhere (a control table) between runs.
The safe pattern brackets each run with two bounds:
- Read the old watermark from the control table.
- Capture the new upper bound from the source —
SELECT MAX(modified_at)— before copying. - Extract the rows between the two:
modified_at > old AND modified_at <= new. - Save the new bound only after the load succeeded.
In a Fabric data pipeline that is two Lookup activities (old watermark from the control table, new upper bound from the source), a Copy activity whose source query uses both values, and a Stored procedure activity (against a Warehouse) or Notebook activity that writes the new value. The last activity must hang off the Copy activity's On success dependency. An Upon completion dependency also fires when the copy fails, so the watermark would advance past rows that were never loaded and the next run would skip them for good.
Why the clock and the "last row written" are unsafe watermarks
A copy query reads the rows that exist when it reads them. Rows updated while a long copy is still running get a modified_at later than the moment they were (or were not) read, but earlier than the clock time when the copy finishes. If you save the clock as the new watermark, those rows sit below it and are never extracted. The value of the last row the copy happened to write is no better: copy order, and the order of parallel writes, is not the order of modified_at, so the last row written is usually not the newest row extracted, and a bound taken from it is effectively arbitrary. Take the bound from the source before the copy (or the maximum of the extracted set itself), and filter up to it.
Late-committing transactions
Some applications stamp modified_at when a transaction starts, then commit minutes later. Such a row becomes visible with a value already below the watermark the previous run saved, so a strict > old filter never sees it. The fix is a lookback window: re-read from the old watermark minus a safety interval at least as long as the longest open transaction. The overlap re-delivers rows already loaded, so the apply step must be a keyed MERGE (an upsert), never an append, or every overlapping row is duplicated.
In a notebook the same pattern is a few lines of PySpark:
old_wm = spark.sql(
"SELECT max_value FROM control.load_watermarks WHERE table_name = 'orders'"
).first()[0]
new_wm = spark.read.table("bronze.orders").agg(max("modified_at")).first()[0]
changed = (spark.read.table("bronze.orders")
.filter((col("modified_at") > lit(old_wm)) & (col("modified_at") <= lit(new_wm))))
# apply with MERGE, then persist new_wmA plain watermark also cannot see hard deletes: a row removed at the source simply stops appearing. If deletes matter, you need Change Data Capture, soft-delete flags, or reconciliation against the source's keys.
Incremental loads with Copy job and Dataflow Gen2 incremental refresh
Fabric gives you two low-code alternatives to hand-building the watermark pipeline: Copy job, which tracks incremental state for you across many tables, and Dataflow Gen2 incremental refresh, which refreshes a single query bucket by bucket.
Copy job incremental copy
In incremental copy mode, a Copy job's first run copies everything; every later run moves only new or changed rows. Change is detected either from a watermark column (such as ROWVERSION, datetime, date, integer, or a string interpreted as a datetime) or from CDC when it is enabled on the source — and only CDC carries deletes. The job stores the state of the last successful run itself, so there is no control table to build; a failed run does not move that state, and the next run resumes from the end of the last successful one. You choose how rows are written: append (the default), merge on a key column (insert or update; with CDC, delete too), overwrite, or SCD Type 2 (a preview option that works with CDC replication and turns source deletes into soft deletes). One Copy job can cover many tables, which makes it the least-effort choice when dozens of tables each have a reliable change column.
Dataflow Gen2 incremental refresh
Incremental refresh is configured per query in a Dataflow Gen2. You pick a Date, DateTime or DateTimeZone column to filter on, how far back to extract, and a bucket size (day, month and so on). On each refresh the dataflow checks the maximum value of a change-detection column per bucket; a bucket whose maximum changed is extracted in full and replaced in the destination (its old rows deleted, then the fresh rows inserted), while unchanged buckets are skipped. Replace is the only update method — there is no row-level merge or append. Destinations that support it directly are Lakehouse, Warehouse and Azure SQL Database, and a query can have at most 50 buckets, the whole dataflow 150.
| Pipeline (Lookup + Copy + control table) | Copy job incremental copy | Dataflow Gen2 incremental refresh | |
|---|---|---|---|
| Change detection | Whatever your query implements | Watermark column or source CDC | Max of a column per date bucket |
| State | Control table you maintain | Tracked automatically, resumes from last success | Tracked per bucket by the dataflow |
| Write behaviour | Your choice (append, MERGE in a notebook or procedure) | Append, merge, overwrite, SCD Type 2 (preview, with CDC) | Replace the whole bucket only |
| Deletes | Only if you build them | With CDC | Only within a re-extracted bucket |
| Fits best | Custom logic, orchestration with other steps | Many tables, least development effort | A Power Query transformation over date-partitioned data |
Change Data Capture and upserts with Delta MERGE
Change Data Capture (CDC) is change detection done by the source system itself: instead of you inferring changes from a timestamp column, the source's transaction log yields a record of every insert, update and delete, each tagged with an operation type and a sequence number (for SQL Server, a log sequence number, LSN). That closes the gaps a watermark leaves open — deletes are visible, and every intermediate change is captured. In Fabric you meet CDC as mirroring (which keeps a OneLake replica current from the source's change feed), as Copy job CDC-based incremental copy, and as Delta change data feed (CDF).
Be precise about CDF: it records the changes made to that Delta table — the inserts, updates and deletes your own writes performed — so that downstream layers (silver to gold) can process them incrementally. It knows nothing about the upstream source. If a row is hard-deleted in the source database and your load never deletes it from the Delta table, CDF has nothing to report.
Whatever produced the change set, applying it to a table is a MERGE — the upsert statement that matches incoming rows to target rows on a business key and applies the right action per row in one atomic operation. MERGE exists both in Spark SQL for Lakehouse Delta tables and in T-SQL in a Fabric Warehouse. The example below is Spark SQL: the UPDATE SET * and INSERT * shorthands are Delta Lake syntax, and the Warehouse T-SQL version lists the columns explicitly.
MERGE INTO silver.orders AS t USING latest_changes AS s ON t.order_id = s.order_id WHEN MATCHED AND s.op = 'D' THEN DELETE WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED AND s.op <> 'D' THEN INSERT *
Deduplicate to the latest change per key first
A change log holds one row per change, not one row per key. An order updated twice since the last run appears twice, both rows match the same target row, and MERGE fails ("multiple source rows matched"). Reduce the change set to the last change per key, ordered by the source's sequence number (LSN, or commit timestamp if that is all you have) — and keep that last row even when it is a delete, so the delete is applied. dropDuplicates(["order_id"]) is not a fix: it keeps an arbitrary row, which may be the older version.
w = Window.partitionBy("order_id").orderBy(col("lsn").desc())
latest = changes.withColumn("rn", row_number().over(w)).filter("rn = 1")Propagating deletes: full snapshots, soft-delete flags and key reconciliation
A load reflects source deletes only if something tells it which rows disappeared. Which mechanism you use depends on what the source gives you.
- The source is a full snapshot. A
MERGEthat only hasWHEN MATCHED THEN UPDATEandWHEN NOT MATCHED THEN INSERTnever visits rows missing from the snapshot, so rows the source no longer contains stay in the target forever. AddWHEN NOT MATCHED BY SOURCE THEN DELETE, which acts on target rows with no source match. It is supported in Fabric Warehouse T-SQL and in Delta Lake. Use it only when the batch is the complete dataset: on an incremental batch it would delete every row that simply did not change. - The source soft-deletes (sets
IsDeleted = 1and bumpsmodified_at). The watermark picks the flagged rows up, so handle them in the merge:WHEN MATCHED AND s.IsDeleted = 1 THEN DELETEremoves rows loaded earlier, and the insert clause must exclude flagged rows (WHEN NOT MATCHED AND s.IsDeleted = 0 THEN INSERT) because a row the target never received has nothing to delete and must not be inserted either. - The source has CDC. Apply the delete operations from the deduplicated change set, as in the previous section.
- No CDC, no flag, too big to reload. Reconcile keys: copy just the source's key column (narrow and cheap even for large tables), anti-join the target against it to find keys that no longer exist, and delete those rows. Run it on whatever schedule the business tolerates for stale deletes.
Idempotent loads: making reruns and re-deliveries safe
A load is idempotent when running it twice with the same input leaves the target exactly as running it once — so a retried pipeline, a re-delivered file or an overlapping lookback window cannot corrupt the data. Design every load for it, because retries happen.
| Write pattern | Idempotent? | Why |
|---|---|---|
| Append | No | A rerun adds the same rows again and totals double. |
| Overwrite the whole table | Only if the batch is the full dataset | Overwriting with an incremental batch wipes everything the batch does not contain. |
Keyed MERGE | Yes, for incremental batches | A second run matches the same keys and writes the same values. |
| Replace the partition the run owns | Yes, even without a unique key | The rerun replaces exactly the slice it produced, leaving other slices alone. |
The last row matters when rows have no unique key — for example web-server log lines, where two identical requests in the same second are genuinely two hits. A key-based MERGE would collapse genuine rows, and in-batch deduplication cannot stop cross-run duplicates. If each run loads exactly one slice (say one hour of logs, tagged LogHour), make the run replace that slice: in a Lakehouse, write with Delta's replaceWhere option (.mode("overwrite").option("replaceWhere", "LogHour = '2026-10-01T09'")); in a Warehouse, delete that slice and insert the batch inside one transaction.
Preparing data for a dimensional model: the star schema
Preparing data for a dimensional model means reshaping source tables into a star schema: a central fact table of numeric, additive measurements surrounded by dimension tables of descriptive attributes. Facts answer "how much, how many" (order amount, quantity, duration); dimensions answer "who, what, where, when" (customer, product, store, date). Each fact row carries foreign keys to its dimensions, and analytical queries join the fact to a handful of dimensions and aggregate.
The preparation work happens in the silver-to-gold transformation, whether you run it in a notebook or with T-SQL in a Warehouse:
- Split entities. A denormalized source extract with customer and product columns repeated on every order line becomes one fact table plus distinct customer and product dimensions.
- Conform dimensions. If sales and returns both reference products, they must share a single product dimension so results reconcile across facts.
- Flatten hierarchies into the dimension. Product → subcategory → category becomes columns of one product dimension. The alternatives are a snowflake (separate subcategory and category tables chained by keys), which adds joins and relationships for no analytical gain, and one big table (attributes copied onto every fact row), which bloats the fact, repeats attribute changes across millions of rows and cannot be shared across facts. A flattened star dimension is the target for Power BI semantic models.
- Declare grain. Fix exactly what one fact row represents — one order line, one order, one daily snapshot — before writing any load logic; every measure and key must be consistent with that grain.
- Build a date dimension. A generated calendar table with one row per day supports time intelligence and consistent fiscal attributes.
Grain decides what happens to data recorded at another level. A header-level measure such as a single discount applied to a whole invoice does not fit an invoice-line fact as is: repeating it on every line overstates every total, and putting it on only the first line makes it impossible to analyse by product. Allocate it to the lines by a stated rule (proportional to line amount, quantity or weight) so it becomes a line-grain measure that sums correctly by any dimension — or, if nobody needs it below invoice level, keep it in an invoice-grain fact. Data at a genuinely different grain, such as quarterly sales targets by region, gets its own fact table joined to the shared (conformed) date and geography dimensions at the coarser level; it is never squeezed into the sales fact.
In Fabric the gold star schema typically lives as Delta tables in a Lakehouse or tables in a Warehouse, and a semantic model in Direct Lake mode sits on top for Power BI. The cleaner the star schema, the simpler and faster that semantic model is — dimensional preparation is as much a modeling task as a loading task.
Surrogate keys and slowly changing dimensions
A surrogate key is a warehouse-generated integer that identifies a dimension row and replaces the source's business (natural) key as the join key from facts. You need one for three reasons: business keys collide when you integrate several sources, business keys get reused or change format, and — decisively — tracking history requires several rows for the same business key, which only a per-row key can distinguish. Surrogate keys are a modeling choice, not a platform requirement: Fabric Warehouse lets you declare a primary key on whichever column identifies the row, including a business key.
Keys in a Fabric Warehouse
- Constraints are declared
NOT ENFORCED. Primary keys (which must beNONCLUSTERED), unique constraints and foreign keys are metadata only, e.g.ALTER TABLE dbo.DimCustomer ADD CONSTRAINT PK_DimCustomer PRIMARY KEY NONCLUSTERED (CustomerKey) NOT ENFORCED;. The engine does not reject duplicates or orphans, so the load must guarantee them. IDENTITYisBIGINTonly, with no seed or increment:CustomerKey BIGINT IDENTITY. The values are unique but not sequential and can have gaps, because the warehouse allocates them in parallel. SpecifyingINTorIDENTITY(1,1)fails. Never rely on key order to mean insert order.- In a notebook, assign keys yourself — for example
row_number()over the new rows, added to the dimension's current maximum key.
Slowly changing dimension types
The slowly changing dimension (SCD) type answers one business question: when an attribute changes at the source, does the old value still matter?
| Type | What happens on a change | Use it when |
|---|---|---|
| Type 0 — retain original | Nothing; the first value is kept forever | The attribute must never change (original signup date); wrong for corrections, because the fix is never applied |
| Type 1 — overwrite | Update in place; history is lost and all facts, past and future, show the new value | Fixing errors; attributes whose history has no analytical value |
| Type 2 — add a row | Expire the current row and insert a new version with its own surrogate key and effective dates | Reports must show the value as it was at the time of each fact, however many times it changes |
| Type 3 — previous-value column | Shift the old value into a column such as PreviousRegion, overwrite the current one | Only "current vs immediately previous" comparisons; a second change overwrites the first, so it fails for repeated changes |
A single dimension can mix types per attribute. The rule for a Type 1 attribute in a Type 2 dimension: update it on every version row of that business key, not just the current one — otherwise reports by that attribute split the same member across old and new values.
The trigger phrases are explicit: "region at the time of the sale" means Type 2; "only the latest value matters" or "the old value was wrong" means Type 1.
Implementing SCD Type 2 in Fabric
A Type 2 dimension needs two things per row: a surrogate key per version and effective dates (ValidFrom, ValidTo, with an open or far-future ValidTo on the current version). An IsCurrent flag is an optional convenience for filtering — the current version is already the one whose end date is open. The business key stays a plain attribute; it cannot be the primary key any more, since several rows share it.
The load compares incoming attributes with each business key's current row. For changed keys it expires the current row (sets ValidTo, clears IsCurrent) and inserts a new version with a new surrogate key; new keys are simply inserted.
The single-MERGE trap
A MERGE with WHEN MATCHED AND attributes differ THEN UPDATE (expire) and WHEN NOT MATCHED THEN INSERT expires changed rows but never inserts their new versions: each source row takes exactly one branch, and a changed key always matches. Two fixes work:
- Stage changed rows twice. Union the changes with a second copy of the changed rows whose merge key is
NULL. The real-key copy matches and expires the old row; theNULL-key copy cannot match anything, so it takes the insert branch. This is the documented Delta Lake pattern for SCD Type 2 in one statement. - Run a separate insert after the
MERGE(or anUPDATEthen anINSERTin one Warehouse transaction).
Dimensional history vs time travel
Fabric Warehouse time travel (OPTION (FOR TIMESTAMP AS OF '…') on a query) and Delta time travel read a whole table as it was at a past point, within a limited retention period. That is a recovery and audit tool, not dimensional history: it cannot tell a fact which customer version applied on its own date, and it expires. Use Type 2 rows when reports need attributes as of each fact's date.
Load order and late-arriving dimension members
Dimensions load before facts — that keeps a star schema consistent. The fact load looks up each incoming business key in the dimension and writes the resulting surrogate key into the fact row; if facts loaded first there would be nothing to look up against.
Against a Type 2 dimension the lookup must use the business key and the fact's own date: pick the version where ValidFrom <= fact date < ValidTo. Looking up only the current row (IsCurrent = 1) works for today's facts but attaches a late-arriving fact — a sale from three weeks ago loaded today — to the customer's current region rather than the region at the time of the sale.
SELECT f.*, d.CustomerKey FROM staged_sales f JOIN dim.Customer d ON d.CustomerID = f.CustomerID AND f.OrderDate >= d.ValidFrom AND f.OrderDate < d.ValidTo
The rule breaks when a fact references a dimension member that does not exist yet — a sale for a product created minutes ago that the nightly product load has not seen. This is the late-arriving dimension member (early-arriving fact) problem, with three standard responses:
- Infer the member. Insert a placeholder row containing just the business key (attributes defaulted to "Unknown", an
IsInferredflag set), take its new surrogate key, and load the fact now. When the real attributes arrive, update that placeholder row in place and clear the flag. Do not create a new Type 2 version: the facts already loaded point at the placeholder's key, and the arrival of real data is a completion, not a change over time. This is the usual production answer because no data is delayed or dropped. - Park the fact. Divert unmatched fact rows to a quarantine table and retry them on later runs. Nothing incorrect enters the model, but measures are understated until the retry succeeds.
- Map to a permanent Unknown member. Point the fact at a reserved "Unknown" row. Totals are right immediately, but the fact is never reattributed unless you add a repair step.
Fabric Warehouse foreign keys are declared NOT ENFORCED, and Lakehouse Delta tables have no foreign keys at all — so referential integrity in a Fabric star schema is entirely the loading pattern's job. The lookup-and-infer logic is the constraint.
Loading patterns for streaming data
A streaming loading pattern differs from batch in one fundamental way: it is append-only. Events arrive continuously, each is an immutable statement of something that happened, and the load appends them as they come — there is no truncate-and-reload and no per-run watermark bookkeeping, because the stream's own offset tracking plays that role. A correction is itself an event: the source sends a new event for the same key, both are kept, and readers resolve the latest one downstream (for example by event time). Nothing is updated in place at the landing edge.
Choosing the landing store
An Eventstream ingests from a source such as Azure Event Hubs and lands raw events into a bronze layer. Pick the destination by what consumes it:
- Eventhouse (KQL database) when people query recent telemetry ad hoc and need data within seconds of arrival — it is built for high-rate ingestion and fast queries over time-series events.
- Lakehouse Delta table when the events feed analytical lake workloads (Spark, SQL, Direct Lake). The Eventstream Lakehouse destination buffers events and writes them in batches, so freshness is minutes, and it avoids writing a flood of tiny files.
Deduplicating at-least-once events
Most event sources deliver at least once, so duplicates are normal and are removed downstream, keeping the raw table intact. In an Eventhouse the right tool is a materialized view that keeps one row per event key:
.create materialized-view EventsDedup on table RawEvents
{
RawEvents | summarize take_any(*) by EventId
}The view is maintained in the background across everything ingested, so queries read an already-deduplicated result. A KQL update policy cannot do this job: it runs on each newly ingested batch and sees only that batch, so a duplicate that arrives in a later batch slips through. Update policies are for per-batch transformation (parsing, reshaping) into a silver table. On the Lakehouse side, Spark structured streaming does the equivalent silver step, still appending micro-batches.
Only at gold does the pattern converge with batch again: aggregating events into dimensional summaries can involve MERGE-style updates to serving tables, typically on a short schedule rather than per event. If a scenario says data "arrives continuously" and asks for a landing design, the answer appends to bronze without transformation; any option that updates rows in place at ingestion, or reloads the landing table, is wrong. Windowed aggregation and engine choice belong to the streaming-transformation topic.
Scenario: designing the loads for a retail Lakehouse
Put the patterns together. A retailer runs Fabric with a medallion Lakehouse: a transactional database holds 400 million order lines with a reliable modified_at column, a product catalog of a few thousand rows changes rarely, customer records change often and the business wants history of region moves, and point-of-sale telemetry streams through Azure Event Hubs.
The design that DP-700 expects:
- Order lines — incremental. Far too large to reload nightly, and
modified_atgives a usable watermark. A pipeline looks up the stored watermark and the source's current maximummodified_at, copies the rows between them to bronze, and a notebookMERGEs them into silver onorder_line_idafter keeping the latest row per key. The new watermark is saved on the Copy activity's success path, so a failed run is simply repeated — the merge makes that safe. - Product catalog — full load. Thousands of rows: a truncate-and-reload every night is cheaper than maintaining incremental machinery, and it self-heals from any drift.
- Customer dimension — incremental with SCD Type 2. The history requirement forces Type 2: changed customers get their current row expired and a new surrogate-keyed row inserted; the fact load resolves each sale's surrogate key by customer ID and sale date, so old sales keep their old region.
- Point-of-sale stream — append-only. An Eventstream lands raw events in bronze; structured streaming conforms them into silver; a scheduled job aggregates into gold. No merges at the landing edge.
- Load order. Dimensions before facts, with inferred members covering products that sell before the catalog load has seen them.
Every choice traces back to three questions the exam keeps asking: how big is it, can you detect change, and does history matter?
Tip. DP-700 tests loading patterns through scenarios: given a source size, a change-detection capability and a freshness or history requirement, pick the pattern or spot its flaw. Parse the trigger words: 'only rows changed since the last run' (watermark or Copy job incremental copy), 'many tables, least effort' (Copy job), 'deleted rows must be reflected' (CDC, soft-delete handling, NOT MATCHED BY SOURCE for full snapshots — a plain watermark is wrong), 'reruns must not duplicate' (keyed MERGE or replacing the owned partition), 'as it was at the time of the sale' (SCD Type 2, fact lookup by date), 'the old value was wrong' (Type 1) and 'arrives continuously' (append-only landing). Flaw-spotting items hide the defect in a detail: a watermark taken from the clock or saved on completion, a MERGE fed several changes for one key, a single MERGE that never inserts new Type 2 versions, or an update policy expected to deduplicate across batches.
- Full loads replace the target every run — simple and idempotent, right for small or change-opaque sources; incremental loads process only changed rows and are mandatory at scale.
- A watermark load filters between the old watermark and a new upper bound read from the source before the copy, and saves that bound only on the copy's success path — never the clock, never the last row written.
- Late-committing transactions need a lookback window, and the overlap needs a keyed MERGE so re-read rows are not duplicated.
- Copy job incremental copy tracks state itself and resumes from the last successful run (append, merge, overwrite); Dataflow Gen2 incremental refresh replaces whole date buckets per query.
- Deduplicate a change set to the last change per key by source sequence number before MERGE; Delta change data feed reports only changes made to the Delta table itself.
- Deletes need CDC, soft-delete handling in the MERGE, WHEN NOT MATCHED BY SOURCE for full snapshots, or key reconciliation.
- SCD Type 1 overwrites, Type 2 adds a surrogate-keyed version with effective dates, Type 3 keeps one previous value, Type 0 never changes; facts find their Type 2 version by business key and fact date.
- Streaming loads are append-only; corrections arrive as new events, and an Eventhouse materialized view (take_any by key) deduplicates across batches where an update policy cannot.
Frequently asked questions
When should I still use a full load instead of an incremental load?
Use a full load when the source is small enough that reloading it costs less than maintaining incremental machinery, when the source has no reliable change indicator (no trustworthy modified timestamp and no CDC), or when you need to re-baseline a target after incremental state was lost or logic changed. The full load must replace the target, by overwrite or truncate-and-reload; merging a full snapshot without a delete step leaves rows the source removed.
What is a watermark column and how do I avoid skipping rows with it?
A watermark column is a value that increases whenever a row is inserted or changed, such as a last-modified timestamp or a ROWVERSION. Each run reads the stored old watermark, captures the new upper bound from the source before copying, extracts rows between the two, and saves the new bound only after a successful load. Saving the clock time instead skips rows changed during the copy, and transactions that commit late need a lookback window plus a MERGE. A watermark sees only each row's latest state, never intermediate changes or hard deletes, and if the column is not updated on every change the missed rows are skipped silently.
Should I use a pipeline, Copy job or Dataflow Gen2 incremental refresh for incremental loads?
Copy job is the least effort for many tables: its incremental copy uses a watermark column or source CDC, tracks the last successful run itself and writes by append, merge or overwrite. Dataflow Gen2 incremental refresh suits a Power Query transformation over date-based data; it is set per query and replaces whole date buckets whose change column's maximum moved, with bucket limits per query and per dataflow. A pipeline with Lookup, Copy and a control table is for custom logic and orchestration.
How does MERGE handle inserts, updates and deletes in one operation?
MERGE matches source rows to target rows on a key and applies clauses per row: WHEN MATCHED can UPDATE or DELETE, WHEN NOT MATCHED inserts, and WHEN NOT MATCHED BY SOURCE acts on target rows absent from the source, which is safe only when the source is a complete snapshot. It works on Lakehouse Delta tables and in Fabric Warehouse T-SQL. If several source rows match one target row the MERGE fails, so reduce a change log to the latest change per key by sequence number first.
How do I make an incremental load safe to rerun?
Make it idempotent, so running it twice leaves the same result as once. A keyed MERGE is idempotent for incremental batches. Overwriting the whole table is idempotent only when the batch is the full dataset. Appending is never idempotent. When rows have no unique key, have each run own a slice, such as one load date, and replace exactly that slice: Delta replaceWhere in a Lakehouse, or delete-then-insert in one transaction in a Warehouse.
What is the difference between SCD Types 0, 1, 2 and 3?
Type 0 keeps the original value and ignores changes. Type 1 overwrites in place, so history is lost and all facts show the newest value, which is right for corrections. Type 2 expires the current row and inserts a new version with its own surrogate key and effective dates, so facts keep the value that was true at the time, however often it changes. Type 3 stores only the immediately previous value in an extra column, so a second change loses the first.
Why do dimension tables need surrogate keys, and does Fabric require them?
Surrogate keys stay unique when several sources' business keys collide, insulate the model from reused or reformatted business keys, and make SCD Type 2 possible because one business key maps to several version rows. Fabric does not require them: a Warehouse primary key, declared NONCLUSTERED and NOT ENFORCED, can sit on a business key. When you do generate them in a Warehouse, IDENTITY is BIGINT only, with no seed or increment, and the values are unique but not sequential.
How is a streaming loading pattern different from an incremental batch load?
An incremental batch load runs on a schedule, pulls a change set with a stored watermark and often merges it. A streaming pattern runs continuously and is append-only: an Eventstream lands events in an Eventhouse (seconds-fresh, ad hoc queries) or a Lakehouse (buffered batches for lake analytics), corrections arrive as new events, and duplicates are removed downstream, for example with an Eventhouse materialized view using take_any(*) by event key, because an update policy only sees one ingested batch.
Source
This lesson covers the "Ingest and transform data" domain of the official DP-700 exam guide. Vendors revise their guides — check the source for the current version.
- Microsoft DP-700 study guide — Microsoft Learn
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.