WHERE Modified >= '$(vLast)' ... Concatenate ... WHERE NOT Exists(ID); STORE ... INTO x.qvd

QVD incremental load in Power BI

The QVD incremental pattern (load the changes, append the old QVD rows not in the change set, store) has no file-level equivalent in Power BI. Use the incremental refresh policy on the semantic model, or in Fabric land the data in a Delta table with MERGE.

Partial / different approach Power BI service Fabric

Short answer: Create RangeStart and RangeEnd datetime parameters, filter the fact with >= RangeStart and < RangeEnd, and define an incremental refresh policy (for example store 5 years, refresh the last 10 days). For true upserts and deletes, do the merge upstream: in a Fabric lakehouse or warehouse, or in your ETL tool.

What it does in Qlik

The classic Qlik pattern: - read rows changed since the last run from the database - concatenate the historic QVD rows whose key is not in the new set (WHERE NOT Exists(ID)) - optionally inner join to the current key list to remove deleted rows - STORE the result back to the QVD

The QVD is a persistent, app-independent cache.

Qlik example

Qlik script
LET vLast = Peek('MaxModified', 0, 'LastRun');   // from a previous step

Orders:
SQL SELECT * FROM dbo.Orders WHERE ModifiedDate >= '$(vLast)';

Concatenate (Orders)
LOAD * FROM [lib://Data/Orders.qvd] (qvd) WHERE NOT Exists(OrderID);

Inner Join (Orders) SQL SELECT OrderID FROM dbo.Orders;   // handle deletes

STORE Orders INTO [lib://Data/Orders.qvd] (qvd);

Power BI equivalent

Incremental refresh filter (Power Query)

Power Query M
// RangeStart and RangeEnd: parameters of type Date/Time (names are mandatory)
= Table.SelectRows(Orders, each [OrderDate] >= RangeStart and [OrderDate] < RangeEnd)

Then, in Desktop: right-click the table, choose Incremental refresh, and set the archive and refresh windows. Optionally turn on Detect data changes on a ModifiedDate column. Publish, and the service creates and manages the partitions.

Fabric lakehouse upsert (Spark SQL)

SQL
MERGE INTO lakehouse.orders AS t
USING staging_orders AS s
  ON t.OrderID = s.OrderID
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

Gotchas and behaviour differences

  • Use >= on one boundary and < on the other. Otherwise rows on partition edges are loaded twice or not at all.
  • Partition on a date that does not change (such as OrderDate). Partitioning on ModifiedDate moves rows between partitions and creates duplicates. Use ModifiedDate for Detect data changes instead.
  • The filter must fold to the source (check View native query). Otherwise every partition refresh reads the whole table.
  • Incremental refresh does not remove rows deleted in the source from partitions it doesn't refresh. Run a periodic full or partition refresh, or handle deletes upstream.
  • Licensing: incremental refresh works on Pro, Premium Per User, Premium/Fabric capacity and Embedded. Real-time data (a DirectQuery partition for the newest rows) needs Premium, PPU or Embedded, not Pro. XMLA-endpoint partition management, such as refreshing single partitions from SSMS or Tabular Editor, also needs Premium/PPU/Fabric capacity. Each refresh still has the usual time limit: 2 hours on Pro, 5 hours on Premium/Fabric capacity.

Official documentation

Related pages

Spotted an error or a better pattern? QlikToDax is maintained by a working BI consultant; corrections are welcome via the About page. Last reviewed 2026-10-25.