The incremental refresh that never increments
Contoso's on-premises SQL Server holds a Sales table with 45 million rows covering five years. The gateway is healthy and a full load takes about twelve minutes. You configure incremental refresh on the dataset with RangeStart and RangeEnd parameters, store three years, and refresh the last ten days. The first refresh succeeds. Every later scheduled refresh takes over five hours, and the DBA reports that the gateway sends a single query returning all 45 million rows with no date predicate. Source columns include ModifiedDate, OrderDate, Revenue and Cost. Your query steps are: filter ModifiedDate between RangeStart and RangeEnd; add Profit = Revenue - Cost; add OrderMonth = Date.StartOfMonth([OrderDate]); change OrderMonth to type Date. Refresh history shows no errors.
1. The policy is configured correctly, yet the gateway reads the whole table on every refresh. Explain what breaks the partitioning and how you would prove it.
Read the worked answer
Folding is evaluated from the source downwards. Power Query pushes a step to SQL Server only while every step before it is also translatable. The first step with no SQL translation becomes the boundary: that step and everything after it runs in the Power Query engine, in memory, over the rows the source returned.
- Step 1 (the ModifiedDate filter with RangeStart/RangeEnd) folds.
- Step 2 (
Revenue - Cost) folds - simple arithmetic is translated. - Step 3 (
Date.StartOfMonth) has no SQL Server translation. That is the boundary.
Because the RangeStart/RangeEnd predicate sits upstream of a step the source cannot evaluate, the engine must materialise all 45 million rows and then filter them client-side. With a three-year policy split into many partitions, every partition query repeats that full read - which is why the run takes hours rather than minutes.
Proof: in Power Query, right-click the last step and choose View native query. It is available at step 2 and greyed out at step 3 and after. Compare the SQL in that window with what the DBA captured. Note that folding never resumes once broken, so a later type change (step 4) does not restore it.
Also confirm the parameters are typed as date/time and that the filter really is the first step.
2. You still need a month-level column for slicing. What are the options, and which do you choose?
Read the worked answer
Best first:
- Delete the column and relate the fact to a Date dimension on OrderDate, keeping Year/Month in that dimension. Nothing is computed per row, folding stays intact, and users slice on
'Date'[Month]. This is the default answer unless the column is needed for something a relationship cannot do. - Create the column in the source - a persisted computed column or indexed view in SQL Server - so the step becomes a plain column reference that folds. Cost: a DBA change and a schema dependency. Benefit: it also works for DirectQuery.
- Use foldable extracts instead of a date truncation.
Date.YearandDate.Monthreturn integers and are commonly translated; you lose the Date data type and need a sort column, so this only suits labels. Verify with View native query before relying on it. - A DAX calculated column on the imported table. It is computed after load, so it cannot break folding - but it adds memory to a 45-million-row table and does not help a DirectQuery table.
Choose option 1. The column duplicates what the Date table already provides, and every extra column adds cardinality to a very large fact. If the column must stay (for example to relate to a planning table), use option 2.
3. After the fix, the first refresh of the new policy still reads the full history and takes 40 minutes. Is that a regression?
Read the worked answer
No. Creating or changing an incremental refresh policy rebuilds the partition scheme, so the first refresh must populate every historical partition - three years of data - before boundary partitions exist. Expect roughly the original full-load time plus partition overhead.
Later refreshes should touch only partitions intersecting the last ten days of ModifiedDate. At day granularity that is a handful of small, folded queries, so the run should fall to minutes or seconds.
Verify three things so the improvement is real:
- the refresh history shows an incremental refresh, not a full one;
- the DBA's captured query now carries the RangeStart/RangeEnd predicate;
- nobody triggers a whole-dataset Refresh now from the workspace, which bypasses the policy.
One design note: day granularity over three years creates about 1,100 partitions. Only a limited number process in parallel, so if the archive period grows, switch the historical partitions to month or year granularity and keep days only for the boundary.