Recherche de toutes les jointures nécessaires pour joindre par programme une table


8

Étant donné un SourceTable et un TargetTable, je voudrais créer par programme une chaîne avec toutes les jointures requises.

En bref, j'essaie de trouver un moyen de créer une chaîne comme celle-ci:

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

J'ai une requête qui renvoie toutes les clés étrangères pour une table donnée, mais je suis confronté à des limitations en essayant de parcourir tout cela récursivement pour trouver le chemin de jointure optimal et créer la chaîne.

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

Je suis sûr que cela a déjà été fait, mais je n'arrive pas à trouver d'exemple.


2
Que se passe-t-il s'il y a 2 chemins ou plus de la source à la cible?
ypercubeᵀᴹ

2
Ouais, je serais préoccupé par plusieurs chemins potentiels, et aussi par un seul chemin de plus de 2 étapes. En outre, les clés se composent de plusieurs colonnes. Ces scénarios jetteront tous une clé dans n'importe quelle solution automatisée.
Aaron Bertrand

Notez que même une seule clé étrangère entre deux tables autorisera 2 chemins ou plus (en fait un nombre illimité de chemins de longueur arbitraire). Considérez la requête "rechercher tous les éléments qui ont été placés au moins une fois dans le même ordre avec l'élément X". Vous devrez vous joindre OrderItemsà Orderset revenir avec OrderItems.
ypercubeᵀᴹ

2
@ypercube Exactement, que signifie exactement "le chemin optimal"?
Aaron Bertrand

"Chemin de jointure optimal" signifie "la plus courte série de jointures qui joindra la table cible à la table source". Si T1 est référencé en T2 et T3, T2 est référencé en T4 et T3 est référencé en T4. Le chemin optimal de T1 à T3 est T1, T2, T3. Le chemin T1, T2, T4, T3 ne serait pas optimal car il est plus long.
Métaphore

Réponses:


4

J'avais un script qui fait une version rudimentaire de la traversée de clé étrangère. Je l'ai adapté rapidement (voir ci-dessous), et vous pourrez peut-être l'utiliser comme point de départ.

Étant donné une table cible, le script tente d'imprimer la chaîne de jointure pour le chemin le plus court (ou l'un d'entre eux dans le cas de liens) pour toutes les tables source possibles de sorte que les clés étrangères à une seule colonne puissent être parcourues pour atteindre la table cible. Le script semble bien fonctionner sur la base de données avec quelques milliers de tables et de nombreuses connexions FK sur lesquelles je l'ai essayé.

Comme d'autres le mentionnent dans les commentaires, vous devrez rendre cela plus complexe si vous devez gérer des clés étrangères multi-colonnes. Sachez également que ce code n'est en aucun cas prêt pour la production et entièrement testé. J'espère que c'est un point de départ utile si vous décidez de développer cette fonctionnalité!

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

Vous pouvez mettre la liste des clés d'une table avec deux champs TAB_NAME, KEY_NAME pour toutes les tables que vous souhaitez connecter.

Exemple, pour table City

  • Ville | City_name
  • Ville | Country_name
  • Ville | Province_name
  • Ville | City_Code

de même Provinceet Country.

Collectez les données des tableaux et mettez-les dans un seul tableau (par exemple, tableau de métadonnées)

Maintenant rédigez la requête comme ci-dessous

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

Cela vous permettra de savoir comment lier les tables en fonction des clés correspondantes (mêmes noms de clés)

Si vous pensez que le nom des clés peut ne pas correspondre, vous pouvez inclure un autre champ de clé et essayer de l'utiliser dans la condition where.


Notez que le demandeur souhaitait utiliser les systables existantes dans SQL Server qui décrivent les colonnes d'une table, comment les tables sont liées entre elles, etc. Tout ce qui existe déjà. Construire vos propres tables qui définissent la structure de votre table pour répondre à un besoin spécifique pourrait être une position de repli, mais la réponse préférée utiliserait ce qui existe déjà, comme le fait la réponse acceptée .
RDFozz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.