| Type | Lifetime | Behaviour and use |
|---|---|---|
| TEMP VIEW | Session | VERIFIED. Scoped to the SparkSession, invisible to other sessions, vanishes on session end. The workhorse for staging steps inside one notebook. No catalog write, so no permissions to manage. |
| GLOBAL TEMP VIEW | Application |
VERIFIED. Shared across all sessions in the same Spark application — which in
Fabric means across notebooks sharing a
high-concurrency session.
Must be read via the global_temp database:
SELECT * FROM global_temp.v_name. Forgetting that prefix is the usual
"table not found".
|
| VIEW (persistent) | Permanent | VERIFIED. Stored in the catalog, survives sessions, visible to anyone with access to the Lakehouse. This is a logical view — the SQL re-executes on every read. No storage, no refresh, no cost until queried. |
| MATERIALIZED LAKE VIEW | Permanent + stored |
Fabric-only. FALSIFIED for OSS: CREATE MATERIALIZED VIEW raises
ParseException on open-source Spark 3.5 — the syntax simply does not
exist there. In Fabric, CREATE MATERIALIZED LAKE VIEW produces a real
Delta table with managed refresh (internals Sec 20).
|
-- FAILS: [CREATE_PERMANENT_VIEW_WITHOUT_ALIAS]
CREATE OR REPLACE VIEW v_bad AS SELECT cat, count(*) FROM orders GROUP BY cat;
-- WORKS
CREATE OR REPLACE VIEW v_good AS SELECT cat, count(*) AS n FROM orders GROUP BY cat;
-- WORKS (temp views are exempt - which is why this bites only on promotion to permanent)
CREATE OR REPLACE TEMP VIEW v_tmp AS SELECT cat, count(*) FROM orders GROUP BY cat;
| Type | Runtime | Verdict |
|---|---|---|
| Built-in functions | both | Always first choice. Catalyst optimises through them, whole-stage codegen fuses them, and NEE offloads them natively. |
SQL UDFCREATE FUNCTION … RETURN |
4.x only |
VERIFIED FALSIFIED on 3.5: raises ParseException on Spark 3.5 —
it is genuinely Runtime 2.0 only, not merely "newer". Where available it is the best
home for shared business logic: catalog-resident, inlined by the optimizer, zero
serialization, NEE-friendly.
|
pandas UDF@pandas_udf |
both |
Vectorized via Arrow — the right fallback when logic cannot be expressed in
built-ins. Appears as ArrowEvalPython in the plan.
|
Python row UDFF.udf(…) |
both |
VERIFIED it works — and it is still the wrong tool. Row-at-a-time
serialization, opaque to Catalyst, breaks codegen, and drops the enclosing operator
out of NEE. Appears as BatchEvalPython.
|
Registered Python UDF for SQLspark.udf.register
|
both |
VERIFIED. Makes a Python function callable from %%sql. Same
performance caveats as a row UDF — convenience, not optimisation.
|
Python UDTF@udtf |
4.x |
Table-valued Python functions. The udtf symbol exists in the 3.5 API
surface, but treat UDTFs as a 4.x capability for production use.
|
SHOW USER FUNCTIONS lists what is registered in the current session —
useful for confirming a SQL UDF actually landed in the catalog rather than the session.
UnsupportedOperationException: sun.misc.Unsafe or java.nio.DirectByteBuffer… not
available
— Arrow needs --add-opens java.base/java.nio=ALL-UNNAMED on modern JDKs.
Fabric configures this for you; the reason to know it is that the same error in a
custom JVM setting or a local dev environment on Runtime 2.0 (JDK 21) is a JVM flag
problem, not a code problem.
Mechanics and fallback tiers are in internals Sec 25. This is the practice sheet.
*Transformer /
*NativeFileScan in the plan, and watch the Diagnostics pane's fallback
panel.
nb_nee_fallback_analyzer rather than assuming
acceleration.
df.explain() shows no
*Transformer operators while spark.native.enabled=true, NEE is
costing you and giving nothing. Either remove the trigger, or turn it off for that job.
Three access paths, and they are not interchangeable. Picking the wrong one is the source of most "why is my metadata layer slow" complaints.
| Path | Can it call… | Use for |
|---|---|---|
| pyodbc + Entra token | tables, views, stored procedures, DML, DDL | The control plane. Anything transactional, anything with output parameters, anything that must run exactly once. The only path that can execute a stored procedure. |
| Spark JDBC | tables, views, query pushdown |
Bulk reads only. Returns a DataFrame for joining against lakehouse data. Cannot call stored procedures. |
| OneLake mirror | mirrored tables (read-only) | Analytical reads over SQL DB data at lakehouse speed, no connection at all. Near-real-time, not synchronous — never use it for control-plane decisions. |
# Obtain Entra ID bearer token for Azure SQL / Fabric SQL Database
token = notebookutils.credentials.getToken("https://database.windows.net/")
jdbc_url = f"jdbc:sqlserver://{SQL_SERVER}:1433;database={SQL_DB};encrypt=true;"
# Read a VIEW: Push filter predicates into SQL engine directly
meta = (
spark.read.format("jdbc")
.option("url", jdbc_url)
.option("accessToken", token)
.option("query", "SELECT entity_id, target_table, load_type FROM dbo.vw_active_entities WHERE layer = 'silver'")
.load()
)
# Parallel read of a LARGE table with partition pushdown
big = (
spark.read.format("jdbc")
.option("url", jdbc_url)
.option("accessToken", token)
.option("dbtable", "dbo.fact_positions")
.option("partitionColumn", "position_id")
.option("lowerBound", 1)
.option("upperBound", 50_000_000)
.option("numPartitions", 16) # 16 concurrent JDBC connections
.load()
)
# Obtain Entra ID bearer token for Azure SQL / Fabric SQL Database
token = notebookutils.credentials.getToken("https://database.windows.net/")
jdbc_url = f"jdbc:sqlserver://{SQL_SERVER}:1433;database={SQL_DB};encrypt=true;"
# Read a VIEW: Push filter predicates into SQL engine directly
meta = (
spark.read.format("jdbc")
.option("url", jdbc_url)
.option("accessToken", token)
.option("query", "SELECT entity_id, target_table, load_type FROM dbo.vw_active_entities WHERE layer = 'silver'")
.load()
)
# Parallel read of a LARGE table with partition pushdown
big = (
spark.read.format("jdbc")
.option("url", jdbc_url)
.option("accessToken", token)
.option("dbtable", "dbo.fact_positions")
.option("partitionColumn", "position_id")
.option("lowerBound", 1)
.option("upperBound", 50_000_000)
.option("numPartitions", 16) # 16 concurrent JDBC connections
.load()
)
partitionColumn is one connection, one task.
Fine for metadata, disastrous for a 50M-row table — and it looks like "Spark is slow"
rather than "you asked for one thread".
numPartitions is concurrent connections to an operational
database. 64 partitions against a busy SQL DB is a denial-of-service you inflicted on
yourself.
execute() per row against SQL DB is enormous overhead; use
cur.executemany(...) or accumulate and write once (internals Sec 27).
runMultiple activities.
Open per activity, close in finally.
A User Data Functions item is serverless Python hosted by Fabric, callable from notebooks, pipelines, Activator rules, Power BI translytical task flows, and external clients via a per-function REST endpoint. Crucially it is not a Spark UDF: it does not run inside your Spark plan, so it never appears in a query plan and never causes NEE fallback.
| Dimension | Spark UDF | Fabric User Data Function |
|---|---|---|
| Executes | Inside the Spark plan, per row/batch on executors | Outside Spark, serverless, once per call |
| Scale unit | Billions of rows | One call (which may carry a DataFrame) |
| Plan impact | BatchEvalPython; forces NEE fallback |
None — invisible to Catalyst |
| Reuse | Per session, or a library | Item-level, cross-workspace, versioned, REST-addressable |
| Right for | Row transformations that must be distributed | Control-plane logic: validation, lookups, logging, API calls, orchestration decisions |
# --- Inside the User Data Functions item ---
import fabric.functions as fn
import logging
udf = fn.UserDataFunctions()
@udf.function() # only decorated functions are externally callable
def validate_entity(entity_name: str, row_count: int, expected_min: int = 1000) -> dict:
ok = row_count >= expected_min
logging.info(f"validate {entity_name}: {row_count} rows, ok={ok}")
return {"entity": entity_name, "ok": ok, "row_count": row_count}
def _helper(): # no decorator = private, not callable from outside
...
# --- Calling it from a notebook ---
fns = notebookutils.udf.getFunctions("etl-shared-functions") # same workspace
fns = notebookutils.udf.getFunctions("etl-shared-functions", WS_ID) # cross-workspace
result = fns.validate_entity(entity_name="silver_orders", row_count=41230) # named params
display(fns.validate_entity.functionDetails) # inspect the signature
| Use | Why a UDF item beats the alternatives |
|---|---|
| Centralised run logging |
One log_run() callable identically from a notebook and a
pipeline. Without it, pipelines cannot write your run log without a notebook
activity purely to hold the code.
|
| Validation gates | "Is this entity allowed to run now?" as one function consulted by every orchestrator. Business rules change in one place. |
| Config resolution | Wrap the SQL Database lookup so notebooks never carry connection logic or secrets handling at all. |
| Rate-limited API calls | Async support suits I/O-bound work, and the serverless model means no Spark session idles while you wait (internals Sec 32). |
| Cross-workspace standards | Centralise in a platform workspace; every domain workspace calls the same tested function. |
withColumn over millions of
rows is a per-row network round trip. Community guidance is consistent: keep them for
light tasks. Pandas DataFrame/Series input and output (Arrow-backed) lets you pass
a batch in one call, which is the correct pattern when data must go through a UDF
item at all — but distributed row transformation belongs in Spark built-ins.
Microsoft ships two separate adapters, and the split matters more than most teams expect: they target different engines, different SQL dialects, and different materializations.
| Adapter | Target | Mechanics |
|---|---|---|
| dbt-fabricspark | Lakehouse (Spark SQL) | Connects to Fabric Spark through the Livy API, session-jobs mode only (so the Spark session is reused across models). Auto-detects schema-enabled lakehouses via the Fabric REST API and switches between two-part and three/four-part naming accordingly. |
| dbt-fabric | Warehouse (T-SQL) | Connects over ODBC — requires Microsoft ODBC Driver 18 (17 also works). T-SQL dialect, warehouse semantics. |
dbt Jobs in Fabric entered public preview in December 2025 as a native
orchestration item, and Microsoft has stated dbt Fusion support is expected later in Q2
2026. A community adapter (dbt-fabric-samdebruyn) supports both engines in
one package and newer dbt-core versions — worth knowing about, but evaluate support
model before production use.
dbt-fabricspark supports
materialized='materialized_lake_view'
— dbt can create and schedule Materialized Lake Views directly, including cron and
daily schedule configuration. This resolves the usual tension: you keep dbt's lineage,
testing and CI/CD while the refresh itself is managed by Fabric.
{{ config(
materialized='materialized_lake_view',
mlv_schedule={
"enabled": true,
"configuration": {
"startDateTime": "2026-04-10T00:00:00",
"endDateTime": "2026-12-31T23:59:59",
"localTimeZoneId": "Central Standard Time",
"type": "Daily",
"times": ["06:00", "18:00"]
}
}
) }}
SELECT order_date, SUM(amount) AS revenue
FROM {{ ref('silver_orders') }}
WHERE status = 'complete'
GROUP BY order_date
incremental
dbt model doing a MERGE is the better materialization — you get incremental behaviour dbt
controls, instead of an MLV that quietly rebuilds every cycle.
| Gotcha | Detail |
|---|---|
| Schema-enabled vs not (Spark) |
Four-part naming works only against schema-enabled lakehouses; setting
workspace_name on a non-schema-enabled target raises a parse-time
error. Set it once in profiles.yml as a target default rather than per
model.
|
| Incremental models use staging tables |
The Spark adapter uses persisted staging tables instead of temp views to work
around Spark's REQUIRES_SINGLE_PART_NAMESPACE limitation. Expect extra
physical tables, and include them in your maintenance scope (internals Sec 30).
|
| Livy session lifetime | Session-jobs mode reuses one Spark session across models — good for CU, but a long dbt run holds that session. Livy sessions also time out on inactivity, which surfaces as a mid-run failure on very long DAGs. |
| ODBC driver on the runner (Warehouse) |
dbt-fabric needs ODBC Driver 18 installed on whatever runs dbt,
including CI agents and containers. On Debian/Ubuntu you also need the ODBC header
files before pip install.
|
| Collation (Warehouse) |
Fabric Warehouse is tested on Latin1_General_100_BIN2_UTF8 — a
case-sensitive, binary collation. Models that rely on case-insensitive joins
or comparisons behave differently than on a typical SQL Server.
|
| Authentication |
Azure CLI (az login) is fine for self-hosted development; use an
Entra service principal for CI/CD and scheduled runs.
|
| Threads |
Fabric Lakehouse profiles commonly use threads: 1. Raising it
multiplies concurrent Livy work — test against capacity before increasing (internals Sec 18).
|
| Signal | Lakehouse (dbt-fabricspark) | Warehouse (dbt-fabric) |
|---|---|---|
| Team dialect | Spark SQL; Python models available | T-SQL; familiar to SQL Server teams |
| Consumption | Direct Lake, Spark, notebooks | T-SQL clients, multi-table transactions, tools expecting a relational warehouse |
| Transactions | Per-table Delta commits | Multi-table transactional guarantees |
| MLV materialization | Supported | Not applicable |
| Write path | Spark, so V-Order and clustering are yours to control | Warehouse manages its own storage and maintenance |
materialized_lake_view for genuinely append-only SQL-expressible
aggregates, incremental with MERGE for everything else. Serve Direct Lake
from the Lakehouse gold; push into Warehouse only where you need T-SQL semantics or
multi-table transactions. Orchestrate with dbt Jobs or a pipeline calling dbt, and keep
one schedule per lakehouse lineage. Fabric Data Agents provide conversational data access over Lakehouses, Warehouses, and KQL Databases under strict security isolation. Unlike raw LLM text-to-SQL prompts, Fabric Data Agents enforce a four-layer governance hierarchy and restrict unbounded outputs.
Fabric IQ introduces a shared semantic ontology layer mapped directly to existing Power BI semantic models and OneLake Delta tables. This replaces ad-hoc entity definitions across departments with an authoritative instance graph.