Finden aller Verknüpfungen, die zum programmgesteuerten Verknüpfen einer Tabelle erforderlich sind


8

Bei einer SourceTable und einer TargetTable möchte ich programmgesteuert eine Zeichenfolge mit allen erforderlichen Verknüpfungen erstellen.

Kurz gesagt, ich versuche einen Weg zu finden, um einen String wie diesen zu erstellen:

FROM SourceTable t
JOIN IntermediateTable t1 on t1.keycolumn = t.keycolumn
JOIN TargetTable t2 on t2.keycolumn = t1.keycolumn

Ich habe eine Abfrage, die alle Fremdschlüssel für eine bestimmte Tabelle zurückgibt, stoße jedoch auf Einschränkungen, wenn ich versuche, all dies rekursiv durchzugehen, um den optimalen Verknüpfungspfad zu finden und die Zeichenfolge zu erstellen.

SELECT 
    p.name AS ParentTable
    ,pc.name AS ParentColumn
    ,r.name AS ChildTable
    ,rc.name AS ChildColumn
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('aTable')
ORDER BY ChildTable, fk.referenced_column_id

Ich bin mir sicher, dass dies schon einmal gemacht wurde, aber ich kann anscheinend kein Beispiel finden.


2
Was ist, wenn es zwei oder mehr Pfade von der Quelle zum Ziel gibt?
Ypercubeᵀᴹ

2
Ja, ich würde mir Sorgen um mehrere mögliche Pfade machen und auch um einen einzelnen Pfad, der mehr als zwei Schritte umfasst. Außerdem bestehen Schlüssel aus mehr als einer Spalte. Diese Szenarien werden in jeder automatisierten Lösung einen Schraubenschlüssel werfen.
Aaron Bertrand

Beachten Sie, dass bereits ein einziger Fremdschlüssel zwischen zwei Tabellen zwei oder mehr Pfade zulässt (tatsächlich eine unbegrenzte Anzahl von Pfaden beliebiger Länge). Betrachten Sie die Abfrage "Finden Sie alle Artikel, die mindestens einmal in derselben Reihenfolge mit Artikel X platziert wurden". Sie müssen OrderItemsmit Ordersund zurück mit OrderItems.
Ypercubeᵀᴹ

2
@ypercube Richtig, was genau bedeutet "der optimale Pfad"?
Aaron Bertrand

"Optimaler JOIN-Pfad" bedeutet "die kürzeste Reihe von Verknüpfungen, die die Zieltabelle mit der Quelltabelle verbinden". Wenn T1 in T2 und T3 referenziert ist, wird T2 in T4 referenziert und T3 wird in T4 referenziert. Der optimale Weg von T1 nach T3 ist T1, T2, T3. Der Pfad T1, T2, T4, T3 wäre nicht optimal, da er länger ist.
Metapher

Antworten:


4

Ich hatte ein Skript, das eine rudimentäre Version der Fremdschlüsselüberquerung ausführt. Ich habe es schnell angepasst (siehe unten), und Sie können es möglicherweise als Ausgangspunkt verwenden.

Bei einer gegebenen Zieltabelle versucht das Skript, die Verknüpfungszeichenfolge für den kürzesten Pfad (oder einen von ihnen im Fall von Bindungen) für alle möglichen Quelltabellen zu drucken, sodass einspaltige Fremdschlüssel durchlaufen werden können, um die Zieltabelle zu erreichen. Das Skript scheint in der Datenbank mit ein paar tausend Tabellen und vielen FK-Verbindungen, an denen ich es ausprobiert habe, gut zu funktionieren.

Wie andere in den Kommentaren erwähnen, müssten Sie dies komplexer gestalten, wenn Sie mehrspaltige Fremdschlüssel verarbeiten müssen. Bitte beachten Sie auch, dass dies keinesfalls produktionsbereiter, vollständig getesteter Code ist. Ich hoffe, es ist ein hilfreicher Ausgangspunkt, wenn Sie sich entscheiden, diese Funktionalität auszubauen!

-- 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

0

Sie können die Liste der Schlüssel einer Tabelle mit zwei Feldern TAB_NAME, KEY_NAME für alle Tabellen, die Sie verbinden möchten, einfügen.

Beispiel für Tabelle City

  • Stadt | Stadtname
  • Stadt | Ländername
  • Stadt | Provinzname
  • Stadt | Stadtcode

ebenso Provinceund Country.

Sammeln Sie die Daten für die Tabellen und fügen Sie sie in eine einzelne Tabelle ein (z. B. Metadatentabelle).

Zeichnen Sie nun die Abfrage wie folgt

select * from
(Select Table_name,Key_name from Meta_Data 
where Table_name in ('City','Province','Country')) A,
(Select Table_name,Key_name from Meta_Data 
where Table_name in ('City','Province','Country')) B,
(Select Table_name,Key_name from Meta_Data 
where Table_name in ('City','Province','Country')) C

where

A.Table_Name <> B.Table_name and
B.Table_name <> C.Table_name and
C.Table_name <> A.Table_name and
A.Column_name = B.Column_name and
B.Column_name = C.Column_name

Auf diese Weise erfahren Sie, wie Sie die Tabellen anhand der übereinstimmenden Schlüssel (gleiche Schlüsselnamen) verknüpfen können.

Wenn Sie der Meinung sind, dass der Schlüsselname möglicherweise nicht übereinstimmt, können Sie ein alternatives Schlüsselfeld einfügen und versuchen, es in der where-Bedingung zu verwenden.


Beachten Sie, dass der Fragesteller die vorhandenen sysTabellen in SQL Server verwenden wollte, die die Spalten in einer Tabelle beschreiben, wie Tabellen miteinander verknüpft sind usw. Alles, was bereits vorhanden ist. Das Erstellen eigener Tabellen, die Ihre Tabellenstruktur definieren, um einen bestimmten Bedarf zu erfüllen, könnte eine Ersatzposition sein, aber die bevorzugte Antwort würde das verwenden, was bereits vorhanden ist, wie es die akzeptierte Antwort tut.
RDFozz
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.