Tom
Tom

Reputation: 1381

SQLServer: How to sort table names ordered by their foreign key dependency

The following SQL separates tables according to their relationship. The problem is with the tables that sort under the 3000 series. Tables that are part of foreign keys and that use foreign keys. Anyone got some clever recursive CTE preferably or a stored procedure to do the necessary sorting?? Programs connectiong to the database are not considered a solution.

Edit: I posted the answer in the "answers" based on the first solution Free "right answer" to be had for anyone reposting my own "right" answer!

WITH 
 AllTables(TableName) AS
 (
 SELECT OBJECT_SCHEMA_NAME(so.id) +'.'+ OBJECT_NAME(so.id) 
 FROM dbo.sysobjects so 
 INNER JOIN sys.all_columns ac ON 
  so.ID = ac.object_id
 WHERE
  so.type = 'U'
 AND
  ac.is_rowguidcol = 1
 ),

  Relationships(ReferenceTableName, ReferenceColumnName, TableName, ColumnName)  AS
  (
  SELECT  
    OBJECT_SCHEMA_NAME (fkey.referenced_object_id) + '.' +  
    OBJECT_NAME (fkey.referenced_object_id) AS ReferenceTableName 
    ,COL_NAME(fcol.referenced_object_id, 
              fcol.referenced_column_id) AS ReferenceColumnName 
    ,OBJECT_SCHEMA_NAME (fkey.parent_object_id) + '.' +  
    OBJECT_NAME(fkey.parent_object_id) AS TableName 
    ,COL_NAME(fcol.parent_object_id, fcol.parent_column_id) AS ColumnName 
  FROM sys.foreign_keys AS fkey 
    INNER JOIN sys.foreign_key_columns AS fcol ON 
               fkey.OBJECT_ID = fcol.constraint_object_id 
  ),

 NotReferencedOrReferencing(TableName) AS
 (
 SELECT TableName FROM AllTables
 EXCEPT
 SELECT TableName FROM Relationships
 EXCEPT
 SELECT ReferenceTableName FROM Relationships
 ),

 OnlyReferenced(Tablename) AS
 (
 SELECT ReferenceTableName FROM Relationships
 EXCEPT
 SELECT TableName FROM Relationships
 ),
-- These need to be sorted based on theire internal relationships
 ReferencedAndReferencing(TableName, ReferenceTableName) AS
 (
 SELECT r1.Tablename, r2.ReferenceTableName FROM Relationships r1 
 INNER JOIN Relationships r2
 ON r1.TableName = r2.ReferenceTableName   
 ),

 OnlyReferencing(TableName) AS
 (
 SELECT Tablename FROM Relationships
 EXCEPT
 SELECT ReferenceTablename FROM Relationships
 )
SELECT TableName, 1000 AS Sorting FROM NotReferencedOrReferencing
UNION
SELECT TableName, 2000 AS Sorting FROM OnlyReferenced 
UNION
SELECT TableName, 3000 AS Sorting FROM ReferencedAndReferencing
UNION
SELECT TableName, 4000 AS Sorting FROM OnlyReferencing
ORDER BY Sorting

Upvotes: 21

Views: 14869

Answers (4)

Andrew Ryan
Andrew Ryan

Reputation: 1

This causes problems with self-referencing tables. You'll need to manually exclude any foreign keys that point to self referencing tables.

INNER JOIN sys.foreign_keys  f 
  ON (f.parent_object_id = so.id AND f.parent_object_id != f.referenced_object_id)

  /* Manually exclude self-referencing tables - they cause recursion problems*/      
    and f.object_id  not in /*Below are IDs of foreign keys*/
        ( 1847729685,   
        1863729742,     
        1879729799      
        )   
INNER JOIN TablesCTE tt

Upvotes: 0

NTDLS
NTDLS

Reputation: 4862

My rendition with moderate tweaks: This one is SQL-2005+ and works on databases without the "rowguidcol":

WITH TablesCTE(SchemaName, TableName, TableID, Ordinal) AS
(
    SELECT
        OBJECT_SCHEMA_NAME(so.object_id) AS SchemaName,
        OBJECT_NAME(so.object_id) AS TableName,
        so.object_id AS TableID,
        0 AS Ordinal
    FROM
        sys.objects AS so
    WHERE
        so.type = 'U'
        AND so.is_ms_Shipped = 0
    UNION ALL
    SELECT
        OBJECT_SCHEMA_NAME(so.object_id) AS SchemaName,
        OBJECT_NAME(so.object_id) AS TableName,
        so.object_id AS TableID,
        tt.Ordinal + 1 AS Ordinal
    FROM
        sys.objects AS so
    INNER JOIN sys.foreign_keys AS f
        ON f.parent_object_id = so.object_id
        AND f.parent_object_id != f.referenced_object_id
    INNER JOIN TablesCTE AS tt
        ON f.referenced_object_id = tt.TableID
    WHERE
        so.type = 'U'
        AND so.is_ms_Shipped = 0
)

SELECT DISTINCT
        t.Ordinal,
        t.SchemaName,
        t.TableName,
        t.TableID
    FROM
        TablesCTE AS t
    INNER JOIN
        (
            SELECT
                itt.SchemaName as SchemaName,
                itt.TableName as TableName,
                itt.TableID as TableID,
                Max(itt.Ordinal) as Ordinal
            FROM
                TablesCTE AS itt
            GROUP BY
                itt.SchemaName,
                itt.TableName,
                itt.TableID
        ) AS tt
        ON t.TableID = tt.TableID
        AND t.Ordinal = tt.Ordinal
ORDER BY
    t.Ordinal,
    t.TableName

Upvotes: 14

Tom
Tom

Reputation: 1381

Thank you for a working solution NXC. You put me on the right track to solve the problem using a recursive CTE.

WITH 
  TablesCTE(TableName, TableID, Ordinal) AS
  (
  SELECT 
    OBJECT_SCHEMA_NAME(so.id) +'.'+ OBJECT_NAME(so.id) AS TableName,
    so.id AS TableID,
    0 AS Ordinal
  FROM dbo.sysobjects so INNER JOIN sys.all_columns ac ON so.ID = ac.object_id
  WHERE
    so.type = 'U'
  AND
    ac.is_rowguidcol = 1
  UNION ALL
  SELECT 
    OBJECT_SCHEMA_NAME(so.id) +'.'+ OBJECT_NAME(so.id) AS TableName,
    so.id AS TableID,
    tt.Ordinal + 1 AS Ordinal
  FROM 
    dbo.sysobjects so 
    INNER JOIN sys.all_columns ac ON so.ID = ac.object_id
    INNER JOIN sys.foreign_keys f 
      ON (f.parent_object_id = so.id AND f.parent_object_id != f.referenced_object_id)
    INNER JOIN TablesCTE tt ON f.referenced_object_id = tt.TableID
  WHERE
    so.type = 'U'
  AND
    ac.is_rowguidcol = 1
)  
SELECT DISTINCT 
  t.Ordinal,
  t.TableName
  FROM TablesCTE t
  INNER JOIN 
    (
    SELECT 
      TableName as TableName,
      Max (Ordinal) as Ordinal
    FROM TablesCTE
    GROUP BY TableName
    ) tt ON (t.TableName = tt.TableName  AND t.Ordinal = tt.Ordinal)
ORDER BY t.Ordinal, t.TableName

For thoose wondering what this is useable for: I will use it to safely empty a database without violating any foreign key relations. (By truncating in descending order) I will also be able to safely fill the tables with data from another database by filling the tables in ascending order.

Upvotes: 9

ConcernedOfTunbridgeWells
ConcernedOfTunbridgeWells

Reputation: 66662

You can use an iterative algorithm, which is probably less convoluted than a CTE. Here's an example that sorts according to depth:

declare @level int      -- Current depth
       ,@count int      

-- Step 1: Start with tables that have no FK dependencies
--  
if object_id ('tempdb..#Tables') is not null
    drop table #Tables

select s.name + '.' + t.name  as TableName
      ,t.object_id            as TableID
      ,0                      as Ordinal
  into #Tables
  from sys.tables t
  join sys.schemas s
    on t.schema_id = s.schema_id
 where not exists
       (select 1
          from sys.foreign_keys f
         where f.parent_object_id = t.object_id)

set @count = @@rowcount         
set @level = 0


-- Step 2: For a given depth this finds tables joined to 
-- tables at this given depth.  A table can live at multiple 
-- depths if it has more than one join path into it, so we 
-- filter these out in step 3 at the end.
--
while @count > 0 begin

    insert #Tables (
           TableName
          ,TableID
          ,Ordinal
    ) 
    select s.name + '.' + t.name  as TableName
          ,t.object_id            as TableID
          ,@level + 1             as Ordinal
      from sys.tables t
      join sys.schemas s
        on s.schema_id = t.schema_id
     where exists
           (select 1
              from sys.foreign_keys f
              join #Tables tt
                on f.referenced_object_id = tt.TableID
               and tt.Ordinal = @level
               and f.parent_object_id = t.object_id
               and f.parent_object_id != f.referenced_object_id)
                   -- The last line ignores self-joins.  You'll
                   -- need to deal with these separately

   set @count = @@rowcount
   set @level = @level + 1
end

-- Step 3: This filters out the maximum depth an object occurs at
-- and displays the deepest first.
--
select t.Ordinal
      ,t.TableID
      ,t.TableName
  from #Tables t
  join (select TableName     as TableName
              ,Max (Ordinal) as Ordinal
          from #Tables
         group by TableName) tt
    on t.TableName = tt.TableName
   and t.Ordinal = tt.Ordinal
 order by t.Ordinal desc

Upvotes: 2

Related Questions