Tôi đã có một kịch bản thực hiện một phiên bản thô sơ của truyền tải khóa ngoại. Tôi đã điều chỉnh nó một cách nhanh chóng (xem bên dưới) và bạn có thể sử dụng nó như một điểm khởi đầu.
Đưa ra một bảng mục tiêu, tập lệnh cố gắng in chuỗi tham gia cho đường dẫn ngắn nhất (hoặc một trong số chúng trong trường hợp quan hệ) cho tất cả các bảng nguồn có thể sao cho các khóa ngoại cột đơn có thể đi qua để đến bảng đích. Kịch bản có vẻ hoạt động tốt trên cơ sở dữ liệu với vài nghìn bảng và nhiều kết nối FK mà tôi đã thử.
Như những người khác đề cập trong các bình luận, bạn cần làm cho vấn đề này phức tạp hơn nếu bạn cần xử lý các khóa ngoại nhiều cột. Ngoài ra, xin lưu ý rằng đây không phải là mã được kiểm tra đầy đủ, sẵn sàng sản xuất. Hy vọng nó là điểm khởi đầu hữu ích nếu bạn quyết định xây dựng chức năng này!
-- 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