← All posts

sql-server · data-migration

Finding order where there seem to be None

Finding the logical order for a data migration with foreign keys, system views, and a recursive SQL query.

Cover illustration for Finding order where there seem to be None

That is pretty much the name of the game when you want to pipeline data into a highly interrelated data model. In other words, you need to know in what order you should import the data so it would satisfy your foreign key constraints. Even simpler put, if table B references table A, you need to migrate data from table A before B, so when you want to import the record ‘b, record ‘a’ already exists, otherwise migration of table B would raise the infamous Foreign Key error.

A foreign key relationship from table B to table A
Given the relationship, you can only insert 'b' if 'a' is present.

Start with the dependencies

One way to do it would be to drop / deactivate all dependencies altogether and reinstate them after the migration process, another would be to brute force the whole process, by which I mean to try each step until it finally succeeds. but these are prone to errors, scale poorly and altogether bad practices.

Alternatively, if you could break the whole process in several logical steps from A to Z, where for each step, all the prerequisites have already been satisfied in the preceding steps, you would never have to worry about any relational constraint holding you down.

But how can we figure out this logical order in a tangled model such as this?

AdventureWorks2017 data model and its table relationships
data model from AdventureWorks2017

In this near-real-world example, even figuring out where to start is not an easy task, let alone the logical order one should follow, in order to migrate data without having to deal with the headache that is “The dependencies”.

Find the migration order with SQL

It is obvious that the key lies in those ever so intersecting lines drawn between each table, that signifies the relationship between them. Fortunately, with the use of system views (that store information pertaining to those relationships) and some intuition, we could find the answer to our dilemma the below code:

WITH fks AS (
	SELECT  
		obj.name AS fk_name,
		sch.name AS source_schema,
		tab1.name AS source_table,
		tab2.name AS target_table
	FROM sys.foreign_key_columns fkc
	INNER JOIN sys.objects obj
		ON obj.object_id = fkc.constraint_object_id
	INNER JOIN sys.tables tab1
		ON tab1.object_id = fkc.parent_object_id
	INNER JOIN sys.schemas sch
		ON tab1.schema_id = sch.schema_id
	INNER JOIN sys.tables tab2
		ON tab2.object_id = fkc.referenced_object_id
),


cte AS (
	SELECT 
		t.table_schema AS source_schema,
		t.table_name AS source_table,
		CONVERT(NVARCHAR(50), '') AS target_schema,
		CONVERT(NVARCHAR(50), '') AS target_table,
		1 as lvl
	FROM INFORMATION_SCHEMA.TABLES AS t
	LEFT OUTER JOIN fks AS f
		ON f.source_table = t.table_name
	WHERE f.source_table IS NULL

UNION ALL

	SELECT
	    f.source_schema AS source_schema,
	    f.source_table AS source_table,
	    CONVERT(NVARCHAR(50), c.source_schema) AS target_schema,
	    CONVERT(NVARCHAR(50), c.source_table) AS target_table,
	    c.lvl + 1 as lvl
	FROM cte AS c
	INNER JOIN fks AS f
		ON f.target_table = c.source_table
	WHERE f.source_table != c.source_table
)

SELECT 
	c.source_schema, 
	c.source_table, 
	MAX(c.lvl) AS orderNo
FROM cte AS c
GROUP BY c.source_schema, c.source_table
ORDER BY 3 ASC

You can also use these links for a more up to date version if there were any changes in the future for both SQL Server and Postgres.

Read the result

The result of this query yields the following:

Tables grouped by their migration order

which tells us that in order to successfully sync migrate data from tables from orderNo = 2, we must first migrate tables from the group with orderNo = 1.

The query starts with tables with no dependency with any other (as the anchor member of the CTE), assigns the value 1 to their Level, then recursively looks ahead to see what tables rely on tables from the previous level and so on. In the end, we assign the biggest dependence level of each table as its orderNo. Everything else is what you might expect from a standard Recursive CTE, the only thing that might seem unique is this predicate in the recursive member of the CTE:

WHERE f.source_table != c.source_table

which makes sure that in case of a self relation in the model, the query doesn’t exhaust the recursion limit.

There is a scenario where a table might be in an indirect relation loop to itself, which I will cover in a later post.


This is a pretty nifty way of sorting you model logically that can help you in different scenarios. Let me know if you have any questions or pointers. 👨💻