The migration question nobody asks early enough
A SQL Server data warehouse that's been running for five or ten years doesn't have simple, isolated workloads. It has stored procedures that call views that call other views that reference tables that have been renamed twice since the original developer left. It has cross-database references to reporting databases on the same instance. It has shared utility functions used by 30 different procedures that nobody remembers writing.
When a migration team asks "which workload goes first?", they're usually thinking about business priority, data volume, or team ownership. All valid criteria. But none of them tell you what will break when that workload moves — which is the thing that causes rollbacks, incidents, and project delays.
The dependency structure of the database tells you that. And SQL Server has been building it for you automatically, since 2008.
What SQL Server already knows about your dependencies
sys.sql_expression_dependencies is a catalog view that records every reference one SQL object makes to another. When a stored procedure calls a view, that reference is stored. When a view references a function, that too. Every dependency edge in your database's object graph, recorded automatically, queryable right now on your SQL Server 2016, 2019, 2022, Azure SQL, or Synapse database.
One query to start every Fabric migration planning session:
SELECT
referenced_entity_name AS object_name,
COUNT(*) AS caller_count
FROM sys.sql_expression_dependencies
WHERE referenced_database_name IS NULL
GROUP BY referenced_entity_name
ORDER BY caller_count DESC;
Run this on your source SQL Server database. The top of the list — the objects with the highest caller_count — are your highest-dependency objects. These are the objects you migrate last, or handle with the most care, because every object that calls them needs to be verified after the migration.
Objects at the bottom (caller_count of 0 or 1) are candidates to move first, in any order, because nothing depends on them or only a single object does.
Three categories every SQL-to-Fabric migration produces
Once you have the dependency map, workloads naturally sort into three categories:
Retire
Objects that appear in the dependency graph but have no callers and no recent execution history. These are dead code — migrate them to Fabric only if you're certain they're still needed.
Rehost
Objects with low caller counts and no cross-database dependencies. These can be moved to Fabric Warehouse or Azure Synapse without significant risk, and serve as the first migration wave.
Migrate carefully
High-dependency objects — views or functions with 20+ callers, cross-database references, objects called from application code. These move last, after their callers are stable in the new environment.
This categorization is impossible without the dependency map. With it, the migration sequence practically writes itself.
The incremental SQL Server → Fabric path
One increasingly common pattern is an incremental modernization rather than a hard cutover: keep SQL Server (or Azure SQL) as the operational layer while gradually introducing Fabric OneLake and Fabric Warehouse for analytics workloads. This approach reduces risk by allowing parallel running, but it introduces its own dependency challenge: which objects belong on which platform?
The dependency map helps here too. Objects that are purely analytical — aggregations, historical views, reporting procedures — are candidates for Fabric Warehouse. Objects that are transactional, that feed application logic, or that are called from operational stored procedures should stay closer to the SQL Server layer.
Finding those boundaries requires tracing the dependency graph to see which analytical objects are truly isolated from transactional ones, and which are more entangled than they appear.
Cross-database references: the Fabric migration blocker
SQL Server databases on-premises frequently use three-part naming to reference objects in sibling databases (ReportingDB.dbo.vwMonthlyRevenue). This pattern works transparently on a SQL Server instance but doesn't translate directly to Microsoft Fabric Warehouse, which uses a different cross-database access model.
Find every cross-database reference before you commit to a migration plan:
SELECT
referencing_entity_name AS calling_object,
referenced_database_name AS target_database,
referenced_entity_name AS target_object
FROM sys.sql_expression_dependencies
WHERE referenced_database_name IS NOT NULL
ORDER BY referenced_database_name, calling_object;
Each row in this result is a dependency that crosses a database boundary. In a Fabric migration, each of these needs an explicit resolution — either consolidating objects into the same Fabric Workspace, using Fabric shortcuts, or rewriting the references.
Finding these on day one of the migration project is far less painful than finding them on cutover day.
Dynamic SQL: the gap in the catalog view
One important caveat: sys.sql_expression_dependencies does not capture references inside dynamic SQL strings. A stored procedure that builds a query with EXEC() or sp_executesql and calls another object inside that string will not show up as a caller in the dependency view.
On heavily dynamic SQL codebases — common in older data warehouses that used dynamic SQL for parameterized reporting — the catalog view will undercount dependencies. Complement the catalog analysis with:
- Searching procedure definitions for
EXEC(andsp_executesqlpatterns - Reviewing SQL Server Query Store for execution patterns not visible in the catalog
- Checking Extended Events or Profiler traces for object access patterns at runtime
What to do with the dependency map
Once you've run the analysis, the dependency map supports several artifacts that make the rest of the migration project easier:
- Migration sequence document — object-by-object migration order based on dependency depth, with the leaf nodes first and the highest-dependency objects last
- Risk register — the top 10–20 objects by caller count, with the list of their callers, as the explicit high-risk section of the migration plan
- Cross-database reference inventory — every three-part name reference, as a resolved action list for the migration team
- Retirement candidates — objects with no callers and no recent query store activity, flagged for confirmation before migration
None of these require anything beyond the catalog view queries above and a spreadsheet. They represent the pre-migration analysis phase that most teams skip because it feels less tangible than running the migration tools — and then discover they needed when something breaks in the new environment.
See the dependency graph visually. Skupa reads sys.sql_expression_dependencies live from Azure SQL, SQL Managed Instance, and Azure Synapse and renders the full call graph as an interactive swimlane diagram. Export to Visio, CSV, or JSON for the migration planning documents.
The dependency map is a migration prerequisite, not a nice-to-have
SQL Server to Fabric migrations fail most often not because the target platform doesn't support the workload — it usually does — but because the migration team didn't know what the workload depended on before they moved it. A view breaks because a function it calls wasn't migrated in the same wave. A procedure fails because it references a database that wasn't included in the migration scope. A report breaks because the object it queries was renamed during the migration as a "quick cleanup."
Every one of those incidents shows up in sys.sql_expression_dependencies before the migration happens. The catalog view is already there, on your running SQL Server 2016 database, populated with the complete dependency graph. It takes one query to read it.
← Back to blog · sys.sql_expression_dependencies deep dive →