Tuesday, March 17, 2015

SQL-queries-to-manage-hierarchical-or-parent-child

http://www.codeproject.com/Articles/818694/SQL-queries-to-manage-hierarchical-or-parent-child

Introduction

Here in this post we will try to manage data’s with hierarchical relation or parent-child relation of a specific table in SQL server. Our special concentration would be over,
  • Show Generations of each row
  • Find all possible parents of a specific row
  • Find all possible child’s of a specific row
  • Show all possible parents at a column with a separator
  • Show all possible child’s at a column with a separator

Background

Let’s pretend
  • A row can have only one parent or Null as no parent
  • There is at least a row, without parent (parentId is null)
  • And at least a row, without any child
Here is our table schema.
/*drop the tbl*/
--DROP TABLE UserType

/*create the tbl*/
CREATE TABLE UserType(
    Id BIGINT NOT NULL,
    Name VARCHAR(100) NOT NULL,
    ParentId BIGINT NULL    
)
Let’s populate the table with data’s
/*insert data into table*/
DECLARE @maxCount BIGINT,
        @count BIGINT,
        @parentId BIGINT;        
SET @maxCount = 10;        /*change to input more*/
SET @count = 1;

WHILE @count <= @maxCount
BEGIN
    If @count = 1
        SET @parentId = NULL;
    ELSE
        SET @parentId = @count - 1;
        
    INSERT INTO UserType(Id, Name, ParentId)
        VALUES (@count, 'User_' + CONVERT(VARCHAR(400), @count), @parentId)
    SET @count = @count + 1;
END
So our populated table would be like
/*show inserted datas*/
SELECT *
    FROM UserType;
Now how to find all this generations, parents or child’s using SQL for a specific row …!!!
The answer is using recursion. But to use this recursion we need some Thing Called CTE (Common Table Expressions) or in syntax “WITH” in SQL. If we don’t have any idea about it, we can start with the links or Google for few moments.
So let’s start with pretty basics.

Regular Join

Joining table with itself based on condition, where ones parentId is equal to another’s Id
/*regular join to get detail*/    
SELECT ChildUserType.Id, ChildUserType.Name, ParentUserType.Id, ParentUserType.Name
    FROM UserType AS ChildUserType
    LEFT JOIN UserType AS ParentUserType ON ChildUserType.ParentId = ParentUserType.Id;

Row Generation

The procedure is something like
  • All rows with no parent (NULL), assign generation 0 to them
  • Find rows where parent belongs to the generation 0, and assign increased generation to itself
  • Do until the recursion is finished.
/*row generations*/
WITH Hierarchy(ChildId, ChildName, Generation, ParentId)
AS
(
    SELECT Id, Name, 0, ParentId
        FROM UserType AS FirtGeneration
        WHERE ParentId IS NULL        
    UNION ALL
    SELECT NextGeneration.Id, NextGeneration.Name, Parent.Generation + 1, Parent.ChildId
        FROM UserType AS NextGeneration
        INNER JOIN Hierarchy AS Parent ON NextGeneration.ParentId = Parent.ChildId    
)
SELECT *
    FROM Hierarchy
    OPTION(MAXRECURSION 32767)

All Possible Parents

Here we are trying to find all possible parents of a row where it's Id = 5
  • Starts with selecting the row where Id = 5
  • Find other rows where its id is equal to previously selected’s ParentId
  • And continue reduction
--all posible parents of @id
DECLARE @id BIGINT;
SET @id = 5;
WITH tblParent AS
(
    SELECT *
        FROM UserType WHERE Id = @id
    UNION ALL
    SELECT UserType.*
        FROM UserType  JOIN tblParent  ON UserType.Id = tblParent.ParentId
)
SELECT * FROM  tblParent
    WHERE Id <> @id
OPTION(MAXRECURSION 32767)

All Possible Childs

Here we are trying to find all possible child’s of a row where it’s Id = 5
  • Starts with selecting the row where Id = 5
  • Find other rows where its ParentId is equal to previously selected’s Id
  • And continue reduction
-- all posible childs of @userTypeId
DECLARE @userTypeId BIGINT;
SET @userTypeId = 5;
WITH tblChild AS
(
    SELECT *
        FROM UserType WHERE ParentId = @userTypeId
    UNION ALL
    SELECT UserType.* FROM UserType  JOIN tblChild  ON UserType.ParentId = tblChild.Id
)
SELECT *
    FROM tblChild
OPTION(MAXRECURSION 32767)

All Possible Parents In A Column

Here we are showing all the possible parents Ids at the column with a specific separator ‘.’
/*row posible parents in a column*/
WITH Hierarchy(ChildId, ChildName, ParentId, Parents)
AS
(
    SELECT Id, Name, ParentId, CAST('' AS VARCHAR(MAX))
        FROM UserType AS FirtGeneration
        WHERE ParentId IS NULL    
    UNION ALL
    SELECT NextGeneration.Id, NextGeneration.Name, Parent.ChildId,
    CAST(CASE WHEN Parent.Parents = ''
        THEN(CAST(NextGeneration.ParentId AS VARCHAR(MAX)))
        ELSE(Parent.Parents + '.' + CAST(NextGeneration.ParentId AS VARCHAR(MAX)))
    END AS VARCHAR(MAX))
        FROM UserType AS NextGeneration
        INNER JOIN Hierarchy AS Parent ON NextGeneration.ParentId = Parent.ChildId    
)
SELECT *
    FROM Hierarchy
OPTION(MAXRECURSION 32767)

All Possible Childs In A Column

Here we are showing all the possible child’s Ids at the column with a specific separator ‘.’
/*row posible childs in a column*/
WITH Hierarchy(ChildId, ChildName, ParentId, Childs)
AS
(
    SELECT Id, Name, ParentId, CAST('' AS VARCHAR(MAX))
        FROM UserType AS LastGeneration
        WHERE Id NOT IN (SELECT COALESCE(ParentId, 0) FROM UserType)     
    UNION ALL
    SELECT PrevGeneration.Id, PrevGeneration.Name, PrevGeneration.ParentId,
    CAST(CASE WHEN Child.Childs = ''
        THEN(CAST(Child.ChildId AS VARCHAR(MAX)))
        ELSE(Child.Childs + '.' + CAST(Child.ChildId AS VARCHAR(MAX)))
    END AS VARCHAR(MAX))
        FROM UserType AS PrevGeneration
        INNER JOIN Hierarchy AS Child ON PrevGeneration.Id = Child.ParentId    
)
SELECT *
    FROM Hierarchy
OPTION(MAXRECURSION 32767)

Recursion limit !!!

In all of the previous queries we have used syntax like
OPTION(MAXRECURSION 32767)
This specifies the maximum number of recursions in CTE. Now if we don’t use this OPTION(MAXRECURSION 32767) , by default it is 100.
We need to specify this number depending on the recursion requirement.

Generate Database Design Document

USE [pOrbis1]
GO
/****** Object:  StoredProcedure [dbo].[GenerateDDD]    Script Date: 3/5/2015 4:52:56 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER procedure [dbo].[GenerateDDD] (@tabName as varchar(100)=null)
as
begin

Declare @Table_Name as varchar(200)
Declare @Table_Desc as varchar(2000)


CREATE TABLE [dbo].[#DDD](
       [ID] [int] IDENTITY(1,1) NOT NULL,
       [SNo] [varchar](500) NULL,
       [FieldName] [varchar](100) NULL,
       [Key] [varchar](20) NULL,
       [DataType] [varchar](50) NULL,
       [Require] [varchar](50) NULL,
       [DefaultValue] [varchar](50) NULL,
       [ColumnDescription] [sql_variant] NULL,
       [ForeignKeyColumn] [varchar](500) NULL
) ON [PRIMARY]

--exec GenerateDDD 'FileImportLog'
Declare rsTable Cursor For select  TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_TYPE= 'BASE TABLE'
and table_name = isnull(@tabName,table_name) and TABLE_NAME not in ('abc','sysdiagrams')
order by table_name
OPEN rsTable
FETCH NEXT FROM rsTable INTO @Table_Name
WHILE @@FETCH_STATUS=0
BEGIN
       print @Table_Name
       sELECT        @Table_Desc = isnull(t.name  + '->' + convert(varchar(2000),td.value ),@Table_Name)
       FROM          sysobjects t
       INNER JOIN  sysusers u
              ON            u.uid = t.uid
       LEFT OUTER JOIN sys.extended_properties td
              ON            td.major_id = t.id
              AND    td.minor_id = 0
              AND           td.name = 'MS_Description'
       WHERE t.type = 'u' and t.name=@Table_Name
     
     
       insert into #DDD(SNo,FieldName,[Key],DataType,Require,DefaultValue,ColumnDescription,ForeignKeyColumn)
     

       select convert(varchar(500),@Table_Desc)SNo,''FieldName,''[Key],''DataType,''Require,''DefaultValue,''ColumnDescription,''ForeignKeyColumn
       union all
       select quotename('SNo'),quotename('FieldName'),quotename('Key'),quotename('DataType'),quotename('Require'),quotename('DefaultValue'),quotename('ColumnDescription'),quotename('ForeignKeyColumn')
       union all


       select colInfo.* , replace(isnull(quotename(convert(varchar(5000),coldescription.column_desc),'"'),''),char(13),', ')  as [ColumnDescription],isnull(FKDetails.PK_Table + '-> ' + FKDetails.PK_Column,'') as 'ForeignKeyColumn'
       from
       (SELECT
       Convert(varchar(50),ordinal_position) as SNo
       , i.Column_Name as [FieldName], isnull(GetInfo .[key],'') as 'Key'
       , [DataType] = CASE when Data_Type in ( 'varchar' , 'char' ) then Data_Type + '(' + convert( varchar (10), character_maximum_length) + ')' else Data_Type end
       , CASE when is_nullable= 'NO' then 'Yes' else 'No' end as 'Require'
       , CASE when column_default='(getdate())' then 'getdate()' else replace ( replace ( replace ( isnull (column_default ,'' ),'(' , '' ), ')' ,'' ),'''' , '' ) end as [DefaultValue]
       --, isnull ( GetInfo. Description, '' ) as 'Description'
       FROM information_schema.columns i left join
       ( SELECT b.column_name,
       [key] = CASE a.constraint_type when 'PRIMARY KEY' then 'PK' else 'FK' end
       --,description = replace(replace(replace(replace( b. constraint_name,'FK_',''),'PK_',''),@Table_Name,''),'_','')
       FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS a inner join INFORMATION_SCHEMA. KEY_COLUMN_USAGE b on a .constraint_name = b. constraint_name
       AND a.table_name=b. table_name
       WHERE a.table_name= @Table_Name ) GetInfo
       on i.column_name = getinfo .column_name
       WHERE i .table_name =@Table_Name)colInfo
       left join
       (sELECT       t.name AS [table],
                           td.value AS [table_desc],
                     c.name AS [column],
                     cd.value AS [column_desc]
       FROM          sysobjects t
       INNER JOIN  sysusers u
              ON            u.uid = t.uid
       LEFT OUTER JOIN sys.extended_properties td
              ON            td.major_id = t.id
              AND    td.minor_id = 0
              AND           td.name = 'MS_Description'
       INNER JOIN  syscolumns c
              ON            c.id = t.id
       LEFT OUTER JOIN sys.extended_properties cd
              ON            cd.major_id = c.id
              AND           cd.minor_id = c.colid
              AND           cd.name = 'MS_Description'
       WHERE t.type = 'u' and cd.value is not null and t.name=@Table_Name) coldescription
       on colInfo.[FieldName]=coldescription.[column]
       left join
       (SELECT
              K_Table = FK.TABLE_NAME,
              FK_Column = CU.COLUMN_NAME,
              PK_Table = PK.TABLE_NAME,
              PK_Column = PT.COLUMN_NAME,
              Constraint_Name = C.CONSTRAINT_NAME
       FROM
              INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C
       INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK
              ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME
       INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK
              ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME
       INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU
              ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME
       INNER JOIN (
                           SELECT
                                  i1.TABLE_NAME,
                                  i2.COLUMN_NAME
                           FROM
                                  INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1
                           INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2
                                  ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME
                           WHERE
                                  i1.CONSTRAINT_TYPE = 'PRIMARY KEY'
                        ) PT
              ON PT.TABLE_NAME = PK.TABLE_NAME) FKDetails
              on colInfo.[FieldName]=FKDetails.FK_Column and FKDetails.K_Table=@Table_Name
             
       union all    
       select '','','','','','','',''

       FETCH NEXT FROM rsTable INTO @Table_Name
END
CLOSE rsTable
DEALLOCATE rsTable
select * from #DDD




end