我有一个脚本,可以执行外键遍历的基本版本。我很快对其进行了修改(请参见下文),您也许可以将其用作起点。
在给定目标表的情况下,脚本会尝试为所有可能的源表打印最短路径的连接字符串(或在联系的情况下为其中之一),以便可以遍历单列外键到达目标表。该脚本在具有数千个表和我尝试过的许多FK连接的数据库上似乎运行良好。
正如其他人在评论中提到的那样,如果需要处理多列外键,则需要使其更加复杂。另外,请注意,这绝不是可用于生产且经过全面测试的代码。如果您决定扩展此功能,希望它对您有所帮助!
-- Drop temp tables that will be used below
IF OBJECT_ID('tempdb..#paths') IS NOT NULL
DROP TABLE #paths
GO
IF OBJECT_ID('tempdb..#shortestPaths') IS NOT NULL
DROP TABLE #shortestPaths
GO
-- The table (e.g. "TargetTable") to start from (or end at, depending on your point of view)
DECLARE @targetObjectName SYSNAME = 'TargetTable'
-- Identify all paths from TargetTable to any other table on the database,
-- counting all single-column foreign keys as a valid connection from one table to the next
;WITH singleColumnFkColumns AS (
-- We limit the scope of this exercise to single column foreign keys
-- We explicitly filter out any multi-column foreign keys to ensure that they aren't misinterpreted below
SELECT fk1.*
FROM sys.foreign_key_columns fk1
LEFT JOIN sys.foreign_key_columns fk2 ON fk2.constraint_object_id = fk1.constraint_object_id AND fk2.constraint_column_id = 2
WHERE fk1.constraint_column_id = 1
AND fk2.constraint_object_id IS NULL
)
, parentCTE AS (
-- Base case: Find all outgoing (pointing into another table) foreign keys for the specified table
SELECT
p.object_id AS ParentId
,OBJECT_SCHEMA_NAME(p.object_id) + '.' + p.name AS ParentTable
,pc.column_id AS ParentColumnId
,pc.name AS ParentColumn
,r.object_id AS ChildId
,OBJECT_SCHEMA_NAME(r.object_id) + '.' + r.name AS ChildTable
,rc.column_id AS ChildColumnId
,rc.name AS ChildColumn
,1 AS depth
-- Maintain the full traversal path that has been taken thus far
-- We use "," to delimit each table, and each entry then has a
-- "<object_id>_<parent_column_id>_<child_column_id>" format
, ',' + CONVERT(VARCHAR(MAX), p.object_id) + '_NULL_' + CONVERT(VARCHAR(MAX), pc.column_id) +
',' + CONVERT(VARCHAR(MAX), r.object_id) + '_' + CONVERT(VARCHAR(MAX), pc.column_id) + '_' + CONVERT(VARCHAR(MAX), rc.column_id) AS TraversalPath
FROM sys.foreign_key_columns fk
JOIN sys.columns pc ON pc.object_id = fk.parent_object_id AND pc.column_id = fk.parent_column_id
JOIN sys.columns rc ON rc.object_id = fk.referenced_object_id AND rc.column_id = fk.referenced_column_id
JOIN sys.tables p ON p.object_id = fk.parent_object_id
JOIN sys.tables r ON r.object_id = fk.referenced_object_id
WHERE fk.parent_object_id = OBJECT_ID(@targetObjectName)
AND p.object_id <> r.object_id -- Ignore FKs from one column in the table to another
UNION ALL
-- Recursive case: Find all outgoing foreign keys for all tables
-- on the current fringe of the recursion
SELECT
p.object_id AS ParentId
,OBJECT_SCHEMA_NAME(p.object_id) + '.' + p.name AS ParentTable
,pc.column_id AS ParentColumnId
,pc.name AS ParentColumn
,r.object_id AS ChildId
,OBJECT_SCHEMA_NAME(r.object_id) + '.' + r.name AS ChildTable
,rc.column_id AS ChildColumnId
,rc.name AS ChildColumn
,cte.depth + 1 AS depth
,cte.TraversalPath + ',' + CONVERT(VARCHAR(MAX), r.object_id) + '_' + CONVERT(VARCHAR(MAX), pc.column_id) + '_' + CONVERT(VARCHAR(MAX), rc.column_id) AS TraversalPath
FROM parentCTE cte
JOIN singleColumnFkColumns fk
ON fk.parent_object_id = cte.ChildId
-- Optionally consider only a traversal of the same foreign key
-- With this commented out, we can reach table A via column A1
-- and leave table A via column A2. If uncommented, we can only
-- enter and leave a table via the same column
--AND fk.parent_column_id = cte.ChildColumnId
JOIN sys.columns pc ON pc.object_id = fk.parent_object_id AND pc.column_id = fk.parent_column_id
JOIN sys.columns rc ON rc.object_id = fk.referenced_object_id AND rc.column_id = fk.referenced_column_id
JOIN sys.tables p ON p.object_id = fk.parent_object_id
JOIN sys.tables r ON r.object_id = fk.referenced_object_id
WHERE p.object_id <> r.object_id -- Ignore FKs from one column in the table to another
-- If our path has already taken us to this table, avoid the cycle that would be created by returning to the same table
AND cte.TraversalPath NOT LIKE ('%_' + CONVERT(VARCHAR(MAX), r.object_id) + '%')
)
SELECT *
INTO #paths
FROM parentCTE
ORDER BY depth, ParentTable, ChildTable
GO
-- For each distinct table that can be reached by traversing foreign keys,
-- record the shortest path to that table (or one of the shortest paths in
-- case there are multiple paths of the same length)
SELECT *
INTO #shortestPaths
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY ChildTable ORDER BY depth ASC) AS rankToThisChild
FROM #paths
) x
WHERE rankToThisChild = 1
ORDER BY ChildTable
GO
-- Traverse the shortest path, starting from the source the full path and working backwards,
-- building up the desired join string as we go
WITH joinCTE AS (
-- Base case: Start with the from clause to the child table at the end of the traversal
-- Note that the first step of the recursion will re-process this same row, but adding
-- the ParentTable => ChildTable join
SELECT p.ChildTable
, p.TraversalPath AS ParentTraversalPath
, NULL AS depth
, CONVERT(VARCHAR(MAX), 'FROM ' + p.ChildTable + ' t' + CONVERT(VARCHAR(MAX), p.depth+1)) AS JoinString
FROM #shortestPaths p
UNION ALL
-- Recursive case: Process the ParentTable => ChildTable join, then recurse to the
-- previous table in the full traversal. We'll end once we reach the root and the
-- "ParentTraversalPath" is the empty string
SELECT cte.ChildTable
, REPLACE(p.TraversalPath, ',' + CONVERT(VARCHAR, p.ChildId) + '_' + CONVERT(VARCHAR, p.ParentColumnId)+ '_' + CONVERT(VARCHAR, p.ChildColumnId), '') AS TraversalPath
, p.depth
, cte.JoinString + '
' + CONVERT(VARCHAR(MAX), 'JOIN ' + p.ParentTable + ' t' + CONVERT(VARCHAR(MAX), p.depth) + ' ON t' + CONVERT(VARCHAR(MAX), p.depth) + '.' + p.ParentColumn + ' = t' + CONVERT(VARCHAR(MAX), p.depth+1) + '.' + p.ChildColumn) AS JoinString
FROM joinCTE cte
JOIN #paths p
ON p.TraversalPath = cte.ParentTraversalPath
)
-- Select only the fully built strings that end at the root of the traversal
-- (which should always be the specific table name, e.g. "TargetTable")
SELECT ChildTable, 'SELECT TOP 100 *
' +JoinString
FROM joinCTE
WHERE depth = 1
ORDER BY ChildTable
GO