Fabric data ingestion: what to use when

Data platforms usually fail in two predictable ways: they drown in shadow copies nobody owns, or they calcify around a single ingestion pattern that does not fit every source. Microsoft Fabric offers a broader palette. You can read data where it already lives, replicate operational systems into a governed lake, and run high‑throughput batch and low‑latency streams without wiring a dozen services together. The work isn’t picking a tool; it’s choosing deliberately so your estate stays fast, testable, and governable as it grows.

This guide treats Zero Unmanaged Copies (ZUC) as a strong—but not exclusive—operating model. ZUC constrains where bytes land and keeps lineage simple: if data persists, it is inside OneLake under policy and catalog; if it does not need to persist, you read it in place. Many teams will also continue to run a traditional lakehouse with raw/bronze landings, curated silver, and published gold. Fabric supports both because everything converges on OneLake (the boundary) and Delta (the table format). We evaluate each option with consistent criteria: performance (bulk throughput and end‑to‑end latency), operational surface (how much you must run and monitor), governance posture (where data persists and how it is secured), team ergonomics (SQL, Spark, or low‑code), and table health (file sizes, partitioning, Delta logs).

For clarity: zero‑copy means reading in place. Managed copy means materializing inside OneLake with lineage. Unmanaged copy is anything persisted outside governance—temporary blobs, stray CSV drops, buckets with unclear ownership. ZUC eliminates that last category; a traditional lakehouse allows governed staging and raw landings as part of the pipeline.

Continue reading “Fabric data ingestion: what to use when”

DirectLake didn’t “take away” your tables—it put them where they belong

I often hear that DirectLake “removes the ability to define tables” and “doesn’t work like traditional Power BI or Tableau.” At a glance, the workflow is different—deliberately so—because Fabric is a data platform, not a visualization tool. In the old days we’d push transformations past gold and into the semantic layer because that was the only practical place left. That was necessary; it was never ideal. By definition, gold is supposed to be ready to consume.

DirectLake mode in MS Fabric’s Power BI gives you (almost) everything Power BI gave you in Desktop. The one big thing you don’t do anymore is DAX calculated columns/tables—and that’s a feature, not a bug. Nuance for accuracy: In DirectLake, calculated columns and calculated tables that reference DirectLake tables aren’t supported; however, some calculated tables that don’t reference DirectLake tables (e.g., documentation helpers) and calculation groups/what‑if parameters are allowed. DirectLake reads Delta in OneLake, the model still uses VertiPaq, and data prep moves into the platform (Dataflows Gen2, Lakehouse/Warehouse SQL, notebooks).

Continue reading “DirectLake didn’t “take away” your tables—it put them where they belong”

Slowly Changing Dimensions (SCDs): A Practical Guide for Your Star Schema

Star schemas shine when your facts (events) are analyzed through dimensions (who/what/where/when). But in real life, dimension attributes change—customers move, products rebrand, sales territories realign. Slowly changing dimensions (SCDs) are the modeling patterns that preserve analytic correctness as those attributes evolve.

Continue reading “Slowly Changing Dimensions (SCDs): A Practical Guide for Your Star Schema”

Materialized Lake Views—A Real‑World Demo with F1 Data

This post demonstrates Materialized Lake Views (MLVs) in a realistic Microsoft Fabric setup. We pair MLVs with Real‑Time Intelligence (RTI)—specifically an Eventhouse (KQL database)—to show how event‑scale data (lap times and race results) can feed a medallion‑style model with no bespoke pipelines and clean governance.

We’ll use the Kaggle Formula 1 World Championship dataset (link shows 1950–2024) and build this end‑to‑end:

  • Eventhouse (RTI) to own lap_times and results, surfaced to OneLake as Delta via OneLake Availability.
  • Lakehouse to declare MLVs (Silver/Gold) over those Delta tables.
  • Warehouse to query Lakehouse tables with three‑part names (no replication).
  • Power BI Semantic Model in Direct Lake over the materialized Delta outputs.

1) Concept & dataset

Concept: Make MLVs the “engine” of your medallion layers. Keep high‑volume facts in Eventhouse, expose them to OneLake as Delta, and declare your Silver/Gold logic as MLVs in the Lakehouse. Query the results from a Warehouse and publish a Direct Lake model—without wiring separate pipelines or scattering logic across tools.

Dataset: Kaggle — Formula 1 World Championship (1950–2024). You’ll use:
drivers.csvconstructors.csvcircuits.csvraces.csvresults.csvlap_times.csv.


2) Reference architecture (MLV‑first)

Kaggle CSVs (drivers, constructors, circuits, races, results, lap_times)
        └──► Eventhouse (KQL DB): ingest CSVs as KQL tables
               └──► OneLake Availability: exposes Eventhouse tables as Delta

Lakehouse
  └──► Table Shortcuts to the Eventhouse-backed Delta tables
  └──► Materialized Lake Views (Silver/Gold) declared in Spark SQL

Warehouse (SQL)
  └──► Cross-database queries (database.schema.table) against Lakehouse Delta

Power BI
  └──► Direct Lake model bound to MLV Delta outputs (avoid SQL views to prevent fallback)

Why this pattern? Eventhouse is purpose‑built for event‑scale ingestion and ad‑hoc KQL. OneLake Availability turns those tables into Delta so every other engine can read them without copy steps. MLVskeep transformation logic declarative, governable, and monitorable.


3) Create Fabric items

  • Eventhouse (KQL DB): f1_event
  • Lakehouse: f1_lake (enable schemas)
  • Warehouse (SQL): f1_wh (optional but useful for SQL demos)

Note that when you create your lakehouse, you won’t see a default semantic model. This model has been sunset, and now you will need to create a semantic model manually, following best practice. This avoids a number of errors.


4) Load the dataset into Eventhouse (KQL)

Use the Eventhouse ingestion wizard or KQL commands. Below is a concise pattern you can adapt.

4.1 Create tables

// Event tables
.create table lap_times (
  raceId:int, driverId:int, lap:int, position:int, [time]:string, milliseconds:long
);

.create table results (
  resultId:int, raceId:int, driverId:int, constructorId:int,
  number:int, grid:int, position:string, positionText:string, positionOrder:int,
  points:real, laps:int, [time]:string, milliseconds:long,
  fastestLap:int, rank:int, fastestLapTime:string, fastestLapSpeed:real,
  statusId:int
);

// Reference tables
.create table drivers (
  driverId:int, driverRef:string, number:int, code:string,
  forename:string, surname:string, dob:string, nationality:string, url:string
);

.create table constructors (
  constructorId:int, constructorRef:string, name:string, nationality:string, url:string
);

.create table circuits (
  circuitId:int, circuitRef:string, name:string, location:string,
  country:string, lat:real, lng:real, alt:int, url:string
);

.create table races (
  raceId:int, year:int, round:int, circuitId:int,
  name:string, [date]:string, [time]:string, url:string
);

4.2 Define CSV mappings (example for one table; repeat as needed or use the wizard)

.create table results ingestion csv mapping "ResultsCsv"
'['
 '{"column":"resultId","datatype":"int","Ordinal":"0"},'
 '{"column":"raceId","datatype":"int","Ordinal":"1"},'
 '{"column":"driverId","datatype":"int","Ordinal":"2"},'
 '{"column":"constructorId","datatype":"int","Ordinal":"3"},'
 '{"column":"number","datatype":"int","Ordinal":"4"},'
 '{"column":"grid","datatype":"int","Ordinal":"5"},'
 '{"column":"position","datatype":"string","Ordinal":"6"},'
 '{"column":"positionText","datatype":"string","Ordinal":"7"},'
 '{"column":"positionOrder","datatype":"int","Ordinal":"8"},'
 '{"column":"points","datatype":"real","Ordinal":"9"},'
 '{"column":"laps","datatype":"int","Ordinal":"10"},'
 '{"column":"time","datatype":"string","Ordinal":"11"},'
 '{"column":"milliseconds","datatype":"long","Ordinal":"12"},'
 '{"column":"fastestLap","datatype":"int","Ordinal":"13"},'
 '{"column":"rank","datatype":"int","Ordinal":"14"},'
 '{"column":"fastestLapTime","datatype":"string","Ordinal":"15"},'
 '{"column":"fastestLapSpeed","datatype":"real","Ordinal":"16"},'
 '{"column":"statusId","datatype":"int","Ordinal":"17"}'
']';

4.3 Ingest from storage

My recommendation here is to use the wizard to ingest the data you need via a shortcut. (I pulled the data for the “changeable” items, like the results and standings from an Azure Blob Storage data source after I had truncated them to remove the 2024 data so that we can demonstrate ingesting new data through the eventhouse.

  1. Click on the ellipsis next to the desired table (…)
  2. Click “Get Data”
  3. Choose your storage method
  4. Fill out your connection details (making sure to properly filter your data to only a single file format)
  5. Validate your schema mappings
  6. Complete!

For completeness, I also created tables for the remaining reference tables: seasons and status, which is supported from this same interface.


5) Enable OneLake Availability on Eventhouse

Turn it on at the database or table level. Eventhouse will publish the KQL tables to OneLake as Delta with a governed latency window. While enabled, some operations (e.g., rename) are restricted. This makes your data instantly consumable by Lakehouse, Warehouse, and Power BI without extra copy steps.


6) In the Lakehouse, create Table Shortcuts to Eventhouse tables

Create new schemas, bronze, silver, and gold in the lakehouse.

From f1_lake → Tables → bronze ▸ New Shortcut → Source: KQL database → select driversconstructorscircuitsracesresultslap_times.
They appear as Delta tables in the Lakehouse—queryable by Spark, the SQL analytics endpoint, and Direct Lake.


7) Declare Materialized Lake Views (MLVs)

Prereqs: In the Lakehouse, schemas must be enabled. Create MLVs from a notebook using Spark SQL. You’ll monitor, schedule, and see lineage from the Managed materialized lake views pane.

7.1 Silver layer (standardize/enrich)

-- silver.dim_driver
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.dim_driver AS
SELECT CAST(driverId AS INT)          AS driver_id,
       CONCAT(forename, ' ', surname) AS driver_name,
       code,
       nationality,
       CAST(dob AS DATE)              AS dob
FROM drivers;

-- silver.dim_constructor
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.dim_constructor AS
SELECT CAST(constructorId AS INT) AS constructor_id,
       name AS constructor_name,
       nationality
FROM constructors;

-- silver.dim_circuit
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.dim_circuit AS
SELECT CAST(circuitId AS INT) AS circuit_id,
       name AS circuit_name,
       location,
       country
FROM circuits;

-- silver.dim_race (join circuits)
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.dim_race AS
SELECT r.raceId             AS race_id,
       r.year               AS season,
       r.round,
       r.name               AS race_name,
       CAST(r.date AS DATE) AS race_date,
       r.circuitId          AS circuit_id,
       c.circuit_name,
       c.location,
       c.country
FROM races r
LEFT JOIN silver.dim_circuit c
  ON r.circuitId = c.circuit_id;

-- silver.results_clean with data quality
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.results_clean
(
  CONSTRAINT pos_order_valid CHECK (positionOrder >= 1) ON MISMATCH DROP,
  CONSTRAINT points_nonneg   CHECK (points >= 0)        ON MISMATCH FAIL
)
AS
SELECT resultId, raceId, driverId, constructorId,
       grid, positionOrder, points, laps,
       fastestLap, fastestLapTime, fastestLapSpeed
FROM results;

-- (Optional) silver.laps_clean
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.laps_clean
(
  CONSTRAINT lap_ms_nonneg CHECK (milliseconds >= 0) ON MISMATCH DROP
)
AS
SELECT raceId, driverId, lap, position, milliseconds
FROM lap_times;

7.2 Gold layer (analytics‑ready facts)

-- gold.fact_results
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS gold.fact_results AS
SELECT d.driver_id,
       co.constructor_id,
       r.season,
       r.race_id,
       rc.grid,
       rc.positionOrder AS finish_pos,
       rc.points,
       rc.laps,
       rc.fastestLap,
       rc.fastestLapTime,
       rc.fastestLapSpeed
FROM silver.results_clean rc
JOIN silver.dim_race       r  ON rc.raceId       = r.race_id
JOIN silver.dim_driver     d  ON rc.driverId     = d.driver_id
JOIN silver.dim_constructor co ON rc.constructorId = co.constructor_id;

-- gold.fact_laps
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS gold.fact_laps AS
SELECT r.season,
       l.raceId       AS race_id,
       l.driverId     AS driver_id,
       l.lap,
       l.position     AS lap_position,
       l.milliseconds AS lap_time_ms
FROM silver.laps_clean l
JOIN silver.dim_race  r ON l.raceId = r.race_id;

Operate the MLVs: From Manage materialized lake views, set a schedule, trigger a manual refresh, review lineage, and inspect data‑quality results (dropped rows vs. failures). Current behavior is full refresh on change; runs are skipped when no inputs changed.


8) Query from the Warehouse (no copies)

From f1_wh, use three‑part names (database.schema.table) to query Lakehouse tables directly:

-- Inspect gold fact
SELECT TOP 10 *
FROM f1_lake.gold.fact_results
ORDER BY season DESC, race_id DESC;

-- Top winners
SELECT d.driver_name, COUNT(*) AS wins
FROM f1_lake.gold.fact_results fr
JOIN f1_lake.silver.dim_driver d
  ON fr.driver_id = d.driver_id
WHERE fr.finish_pos = 1
GROUP BY d.driver_name
ORDER BY wins DESC;

9) Build the Semantic Model (Power BI) in Direct Lake

Bind model tables directly to the Delta outputs (gold.*, selected silver.*). Avoid layering SQL views between Power BI and your Delta tables; views can cause Direct Lake → DirectQuery fallback. Use physical Delta tables (your MLV outputs) for the best performance.

Starter DAX

Wins := COUNTROWS( FILTER( 'fact_results', 'fact_results'[finish_pos] = 1 ) )
Podiums := COUNTROWS( FILTER( 'fact_results', 'fact_results'[finish_pos] <= 3 ) )
Avg Lap (ms) := AVERAGE('fact_laps'[lap_time_ms])
Points := SUM('fact_results'[points])

10) Notes & gotchas

  • MLVs today: created via Spark SQL; refresh is full on change with skipped runs when unchanged.
  • Eventhouse availability: while enabled, certain DDL operations (e.g., rename) are restricted; configure latency to balance freshness vs. cost.
  • Direct Lake: binding to tables (not SQL views) avoids fallback to DirectQuery.
  • Alternate path for reference data: If you prefer not to store dims in Eventhouse, you can import the CSVs into the Lakehouse as Delta (e.g., “Create table from files” or a short Spark notebook). This does create a managed copy, but keeps your MLV story intact—still no separate pipeline tool required.

11) Quick validation

KQL (Eventhouse)

results
| summarize total_points = sum(points) by driverId
| top 10 by total_points desc

Spark SQL (Lakehouse)

SELECT season, COUNT(DISTINCT race_id) AS races
FROM gold.fact_results
GROUP BY season
ORDER BY season DESC;

T‑SQL (Warehouse)

SELECT TOP 20 r.season, r.race_name, d.driver_name, fr.finish_pos, fr.points
FROM f1_lake.gold.fact_results AS fr
JOIN f1_lake.silver.dim_race   AS r ON fr.race_id   = r.race_id
JOIN f1_lake.silver.dim_driver AS d ON fr.driver_id = d.driver_id
ORDER BY r.season DESC, r.race_id DESC, fr.finish_pos ASC;

Why Materialized Lake Views (MLVs) matter

  • Declarative pipelines: one Spark SQL statement defines each transform. Fabric handles orchestration, lineage, and monitoring—no separate ETL/pipeline asset to wire up.
  • Governed & observable: MLVs show source dependencies, run history, and data‑quality outcomes in a single, first‑class UI.
  • Built‑in data quality: add CHECK constraints with ON MISMATCH DROP | FAIL to reject or fail on bad data—without custom code.
  • Predictable refresh: full refresh when sources change, skipped runs when they don’t—simple behavior that’s easy to explain to stakeholders.
  • Open downstream: MLV outputs are Delta tables in OneLake—query them from Warehouse (SQL), notebooks, and Power BI in Direct Lake for fast BI.

Closing

MLVs turn your medallion layers into governed assets: one statement per transform, first‑class lineage, schedulable refresh, and built‑in data quality. Pair them with Eventhouse and OneLake Availability to keep ingestion fast and analytics open—then light up Warehouse and Direct Lake without extra copy steps or pipeline sprawl.

Implementing Stars and Galaxies in Power BI

Power BI rewards clean dimensional models—but it also punishes sloppy ones. This post walks through how to implement star and galaxy schemas in Power BI semantic models, why ambiguous (multiple) filter paths cause headaches, why implicit measures don’t scale beyond the simplest star, and how tightly defined data products keep your BI ecosystem fast, correct, and governable. Because this is such an important topic, I’ve included links to references with each point.

Continue reading “Implementing Stars and Galaxies in Power BI”

“Zero Copy” Doesn’t Mean “No Copies.” It Means “No Unmanaged Copies.”

The rallying cry of modern data platforms—Zero Copy—is revolutionary because it flips the default: don’t move data unless there’s a good reason and the platform manages it for you. In Microsoft Fabric, that starts with in-place access via OneLake Shortcuts and an open storage layer, then selectively uses managed and automated copies (like Mirroring and Materialized Lake Views) when they deliver clear value. The result is less sprawl, more trust, and faster analytics—without hand-built duplication. 

Continue reading ““Zero Copy” Doesn’t Mean “No Copies.” It Means “No Unmanaged Copies.””

A Lightweight Ingestion Framework in Microsoft Fabric

Modern Fabric estates don’t need a forest of bespoke pipelines, but they do need metadata-driven tools to reduce time to insight. You can land data quickly in Bronze, promote it reliably to Silver and Gold with a metadata‑driven Spark Structured Streaming engine, and treat Gold as the foundation for your data products—semantic models, AI endpoints, and any other served formats.

Continue reading “A Lightweight Ingestion Framework in Microsoft Fabric”

How Unity Catalog Views Change The Lakehouse and Enable Data Mesh

Unity Catalog represents a paradigm shift in many ways, but one of the most underappreciated elements is its views. These views represent a massive change to the way we think about the medallion architecture and the lakehouse. In addition to being a critical part of the effort to reduce the number of copies of data that exist around an organization’s data estate, they also can form the building blocks of Data Mesh and Data Fabric architectures and can even be leveraged to create data products, which all become especially powerful as an organization increases its data strategy maturity and its analytics maturity. With two conceptually simple features, Databricks has turned the view from a useful part of the data engineering toolbox into an indispensable tool for every architect and even for citizen developers, especially in the modern day of generative AI.

Before diving into the Unity Catalog, it’s important for us to define our terms, especially those that are a part of the Medallion architecture. This approach organizes data processing in three layers: bronze, silver, and gold.

  • The bronze layer is where raw data is ingested from various sources. This is the first step in the data processing pipeline, where data is collected and stored in its raw form.
  • In the silver layer, the data is cleansed, transformed, and enriched. This is where the data is processed and prepared for analysis, with any errors or inconsistencies being corrected.
  • The gold layer contains business-level aggregates and metrics that are ready for decision-making processes. This is the final stage of the data processing pipeline, where the data is presented in a form that is easily understandable and actionable for business users.

This tiered approach helps in managing data lineage clearly and efficiently, making it easier for organizations to trace how data transforms across its lifecycle. The Medallion architecture is by far the most common architecture for Lakehouses, and we will be referencing it throughout this piece, but Unity Catalog can work with any of the many different reference architectures, notably Data Vault 2.0 or Lambda.

A traditional medallion architecture layout.

Building Blocks for a new Lakehouse

By combining views with UDFs and Unity Catalog’s permission model, we start to build a new, more fluid approach. Users (or in the best practice, group members) are then able to see, using customized permissions, exactly what they are allowed to see, but only one object must exist. Even more powerfully, the view executes as the view’s owner, meaning that the user does not need permissions to either the underlying data, or to the functions that are used to make the view work.

Using this principal, there are three building blocks that can be used to create highly dynamic tables for end users:

  1. Row-Based Access Control – Users can be given access to only the rows that they need to do their jobs, limiting the scope of their access. This allows users to focus on what they need to do and prevents the accidental overexposure of data.
  2. Column-Level Access Control – Specific columns can be shown or hidden based on a user’s permissions. This allows fine-grained access to data while respecting both privacy and security.
  3. Dynamic Data Masking – When most people think of masking, they think of the kind of masking applied to credit card numbers, and that is definitely possible with Unity Catalog views, but there a whole universe of masking available to a developer in Databricks now. This dynamic masking can occur at a row or column level and can be used to restrict access to specific restricted information while still granting it to those with need to know. In this way, aggregations can be maintained without the need for complex rollup tables.

All these capabilities come together to provide a new, data governance integrated experience that allows for duplicate effort and data to be minimized across a data estate. With this end-users and citizen data scientists can be safely given access to data to enable them to use their domain-level expertise to find more possibilities. That brings us to an evolving paradigm in the data warehousing world.

A lakehouse created with views rather than copying data.

How Views Change the Lakehouse

In a traditional lakehouse, data is copied into each layer as it is transformed. This represents an opportunity for inconsistencies and stale – or even bad – data to become a part of our final analysis set. This copying also means that the entire data warehouse must be reloaded from scratch when changes are made, reducing the nimbleness of the data warehouse and making it impossible to represent as highly consistent and repeatable code, rather than changeable and expensive data. The view changes this, but making it possible to reduce the number of copies of the data from three (or more) to just two – the data in the bronze layer that has not yet been transformed, and the clean enriched data in the silver layer. Gold simply becomes a view (or many views) on top of the silver layer.

This change drastically reduces the amount of work needed to create a secure lakehouse. Under previous models, it would often be necessary to create entirely different tables for each persona, and move data into them, creating significant delays and data duplication. This also introduced an opportunity for data to become out of sync, or for the wrong data to be copied into the wrong place. Increasing the risk of a lakehouse compared to a traditional data warehouse.

Enabling Data Products and Data Mesh

This makes Unity Catalog views an ideal way to represent data products and to build into a Data Mesh architecture. By creating a pattern that can be used to represent both complex, IT/Data driven data products as well as local, departmentally produced data products, the paradigm builds the foundation for a true data mesh architecture.

An illustration of a Lakehouse as a data product.

Non-IT organizations can build out and provide their own data products based on data that they have that is integral to a specific use case, and customize it to many different personas – and these personas do not have to be the same personas used by other data products – each data product can be totally a totally independent member of the organizations data estate.

Following on to that, though, is the ability for views (and these view-based data products) to build upon other views. This truly exemplifies the data mesh architecture, and the dynamic way all these data models interact can give a persona truly bespoke data model access. Not simply to the specific rows and records that they need, but customizing data display within those records, ensuring that data availability is maximized while data privacy and data security are not compromised.

An illustration of different data products interacting together and building on top of eachother.

Unity Catalog represents a significant shift in the way we think about data management and the Medallion architecture. By reducing the number of copies of data and enabling the use of views, the Unity Catalog improves data governance and reduces the risk of inconsistencies and stale data. Furthermore, the Unity Catalog enables the creation of data products and the implementation of a data mesh architecture, allowing for more dynamic and flexible data management. Overall, the Unity Catalog has the potential to revolutionize the way organizations manage their data, improving efficiency, security, and flexibility.