Hierarchical structure, new columns, denormalization - sql

Suppose I have table like this:
id parent_id name
11 NULL Company
33 11 Department 1
44 33 Department 2
I would like to transform it into:
id parent_id name Level1 Level2 Level3
11 NULL Company NULL NULL NULL
22 11 Company Department 1 NULL NULL
33 22 Company Department 1 Department 2 NULL
I am able to create a CTE and come up with Levels column showing a value in hierarchy, but I don't know how to make new columns for departments as presented.
with myCTE as (
select c.id, c."name", c.parent_id, 1 as Level
from table1 c
where c.parent_id IS NULL
UNION ALL
Select c1.id, c1."name", c1.parent_id, Level +1
from table1 c1
inner join myCTE on c1.parent_id = myCTE.id
where c1.parent_id IS NOT NULL
)
select * from myCTE
showing:
id parent_id name level
1 11 NULL Company 1
2 22 11 Department 2 2
3 33 22 Department 3 3

An (almost) fully generic approach:
DECLARE #tbl TABLE(id INT,parent_id INT,name VARCHAR(100));
INSERT INTO #tbl VALUES
(11,NULL,'Company')
,(33,11,'Department 1')
,(44,33,'Department 2a')
,(55,33,'Department 2b')
,(66,44,'SubDep 2a');
--The recursive CTE will build an XML fragment on a row-by-row level
--The SELECT will use XML method .nodes() and ROW_NUMBER to generate column names for PIVOT
WITH recCTE AS
(
SELECT id, parent_id,name,(SELECT name AS [*] FOR XML PATH('')) AS NameConcat
FROM #tbl WHERE parent_id IS NULL
UNION ALL
SELECT t.id,t.parent_id,t.name,recCTE.NameConcat + '</lvl><lvl>' + (SELECT t.name AS [*] FOR XML PATH(''))
FROM #tbl AS t
INNER JOIN recCTE ON recCTE.id=t.parent_id
)
SELECT p.*
FROM
(
SELECT id
,parent_id
,name
,'Level' + REPLACE(STR(ROW_NUMBER() OVER(PARTITION BY id ORDER BY (SELECT NULL)),2),' ','0') AS HierarchyRank
,lvl.value(N'(./text())[1]','nvarchar(max)') AS HierarchyName
FROM recCTE
CROSS APPLY (SELECT CAST('<lvl>' + NameConcat + '</lvl>' AS XML) AS PreLevels ) AS Casted
CROSS APPLY Casted.PreLevels.nodes(N'/lvl') AS A(lvl)
) AS tbl
PIVOT
(
MAX(HierarchyName) FOR HierarchyRank IN(Level01,Level02,Level03,Level04,Level05,Level06,Level07,Level08,Level09)
) AS p;
The result
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| id | parent_id | name | Level01 | Level02 | Level03 | Level04 | Level05 |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| 11 | NULL | Company | Company | NULL | NULL | NULL | NULL |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| 33 | 11 | Department 1 | Company | Department 1 | NULL | NULL | NULL |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| 44 | 33 | Department 2a | Company | Department 1 | Department 2a | NULL | NULL |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| 55 | 33 | Department 2b | Company | Department 1 | Department 2b | NULL | NULL |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
| 66 | 44 | SubDep 2a | Company | Department 1 | Department 2a | SubDep 2a | NULL |
+----+-----------+----------------+---------+----------------+---------------+-----------+---------+
If you need more levels, the only need was to add more column names into the PIVOT part...

You can calculate the rows for every level, and unite them :
with MyCTE as (
select id, parent_id, name, null as level1, null as level2, null as level3
from table1 as root
where root.parent_id is null
union
select level1.id, level1.parent_id, root.name, level1.name as level1, null as level2, null as level3
from table1 as level1
inner join table1 as root on root.id = level1.parent_id
where root.parent_id is null
union
select level2.id, level2.parent_id, root.name, level1.name as level1, level2.name as level2, null as level3
from table1 as level2
inner join table1 as level1 on level1.id = level2.parent_id
inner join table1 as root on root.id = level1.parent_id
where root.parent_id is null
union
select level3.id, level3.parent_id, root.name, level1.name as level1, level2.name as level2, level3.name as level3
from table1 as level3
inner join table1 as level2 on level2.id = level3.parent_id
inner join table1 as level1 on level1.id = level2.parent_id
inner join table1 as root on root.id = level1.parent_id
where root.parent_id is null
)
select * from MyCTE
If you need more levels, you will need to add more selects with additional joins

Related

Sub query in the where clause

The code below is working now in the VIEW based on Windows Authentication, users should able to see all the data that they own and data of those reports to them direct or indirect. Now another WHERE clause needed to handle the additional result of data that giving to the user in the Authorize column.
SAMPLE DATA: Table TORGANIZATION_HIERARCHY
ManagerID | ManagerEmail | Email | EmployeeID | Authorize | Level
---------------------------------------------------------------------------------
NULL | NULL | user0##abc.com | 1 | NULL | 0
1 | user0##abc.com | user1##abc.com | 273 | NULL | 1
273 | user1##abc.com | user2##abc.com | 16 | NULL | 2
273 | user1##abc.com | SJiang##abc.com | 274 | NULL | 2
273 | user1##abc.com | SAbbas#abc.com | 285 | user2##abc.com; user3#abc.com | 2
285 | SAbbas#abc.com | LTsoflias#abc.com | 286 | NULL | 3
274 | SJiang##abc.com | MBlythe#abc.com | 275 | NULL | 3
274 | SJiang##abc.com | LMitchell#abc.com | 276 | NULL | 3
16 | JWhite#abc.com | user3#abc.com | 23 | NULL | 3
SAMPLE DATA: Table TRANS
Email | Destination_account | Customer_service_rep_code
-----------------------------------------------------------
SAbbas#abc.com | Philippines | 12646
Junerk#abc.com | Canada | 95862
LTsoflias#abc.com | Italy | 98524
user2##abc.com | Italy | 29185
user3##abc.com | Brazil | 58722
The bottom query is working when user SAbbas#abc.com (285) log in. It can see all the data of EmployeeID 285 and 286. I need add another where statement that user (SAbbas#abc.com) authorized to see to see in column Authorize. So the result user SAbbas#abc.com should see EmployeeID 285, 286, 16, 23.
WITH CTE
AS (SELECT OH.employeeid,
OH.managerid,
OH.email AS EMPEMAIL,
1 AS level
FROM TORGANIZATION_HIERARCHY OH
WHERE OH.[email] = (SELECT SYSTEM_USER) --Example SAbbas#abc.com
UNION ALL
SELECT CHIL.employeeid,
CHIL.managerid,
CHIL.email,
level + 1
FROM TORGANIZATION_HIERARCHY CHIL
JOIN CTE PARENT
ON CHIL.[managerid] = PARENT.[employeeid]),
ANOTHERCTE
AS (SELECT
T.[email],
T.[destination_account],
T.[customer_service_rep_code]
FROM [KGFGJK].[DBO].[TRANS] AS T)
SELECT *
FROM ANOTHERCTE
INNER JOIN CTE
ON CTE.empemail = ANOTHERCTE.[email];
This will give you what you need based on column Authorize. The result should be 16 and 23
Select b.employeeid from TORGANIZATION_HIERARCHY a inner join TORGANIZATION_HIERARCHY b
on a.Authorize like '%' + b.Email + '%'
where a.Email = 'SAbbas#abc.com'
Let me know
Complete Solution:
For you to be able to see user3#abc.com, I had to correct the email in 6the table #TRANS. You worte in there user3##abc.com instead of user3#abc.com. # and not ##.
the code is below for your tests. After you can replace with you table names
IF OBJECT_ID('tempdb..#TORGANIZATION_HIERARCHY') IS NOT NULL DROP TABLE #TORGANIZATION_HIERARCHY;
select NULL as ManagerID ,NULL as ManagerEmail ,'user0##abc.com' as Email ,1 as EmployeeID ,NULL as Authorize , 0 as Level into #TORGANIZATION_HIERARCHY
union select 1 ,'user0##abc.com', 'user1##abc.com' ,273 ,NULL , 1
union select 273 ,'user1##abc.com', 'user2##abc.com' ,16 ,NULL , 2
union select 273 ,'user1##abc.com', 'SJiang##abc.com' ,274 ,NULL , 2
union select 273 ,'user1##abc.com', 'SAbbas#abc.com' ,285 ,'user2##abc.com; user3#abc.com' , 2
union select 285 ,'SAbbas#abc.com', 'LTsoflias#abc.com' ,286 ,NULL , 3
union select 274 ,'SJiang##abc.com', 'MBlythe#abc.com' ,275 ,NULL , 3
union select 274 ,'SJiang##abc.com', 'LMitchell#abc.com' ,276 ,NULL , 3
union select 16 ,'JWhite#abc.com', 'user3#abc.com' ,23 ,NULL , 3
--select * from #TORGANIZATION_HIERARCHY
IF OBJECT_ID('tempdb..#TRANS') IS NOT NULL DROP TABLE #TRANS;
select 'SAbbas#abc.com' as Email , 'Philippines' as Destination_account , 12646 as Customer_service_rep_code into #TRANS
union select 'Junerk#abc.com' , 'Canada' , 95862
union select 'LTsoflias#abc.com', 'Italy' , 98524
union select 'user2##abc.com' , 'Italy' , 29185
union select 'user3#abc.com' , 'Brazil' , 58722
;WITH CTE
AS (SELECT OH.employeeid,
OH.managerid,
OH.Authorize,
OH.email AS EMPEMAIL,
1 AS [level]
FROM #TORGANIZATION_HIERARCHY OH
WHERE OH.[email] = (SELECT 'SAbbas#abc.com') --Example
UNION ALL
SELECT CHIL.employeeid,
CHIL.managerid,
CHIL.Authorize,
CHIL.email,
CHIL.[level] + 1
FROM #TORGANIZATION_HIERARCHY CHIL
JOIN CTE PARENT
ON CHIL.[managerid] = PARENT.[employeeid]),
ANOTHERCTE
AS (SELECT
T.[email],
T.[destination_account],
T.[customer_service_rep_code]
FROM #TRANS AS T)
SELECT *
FROM ANOTHERCTE
RIGHT JOIN
(
select a.EmployeeID, a.ManagerID, a.Authorize, a.Email as empemail, a.[level] From CTE INNER JOIN #TORGANIZATION_HIERARCHY a on lower(CTE.Authorize) like '%' + lower(a.Email) + '%'
union
select * From CTE
) CTE
ON CTE.empemail = ANOTHERCTE.[email]
order by [level]
Output:

How to create a condition for this case?

Sample Table:
Id |Acc_Code|Description |Balance | Acclevel| Acctype| Exttype|
--- -------- ----------------- |-------- |-------- | -------| -------|
1 |SA |Sales | 0.00 | 1 | SA | |
2 |CS |Cost of Sales | 0.00 | 1 | CS | |
3 |5000/001|Revenue | 94.34 | 2 | SA | |
4 |5000/090|Sales(Local) | 62.83 | 2 | SA | |
5 |7000/000|Manufacturing Acc |-250.80 | 2 | CS | MA |
6 |7000/200|Manufacturing Acc | 178.00 | 2 | CS | |
This is a sample data of a temporary table which would be used to be inserted into another temporary table that would calculate the data for Profit and Loss Statement (For Manufacturing related Accounts only).
In this case, the acc_code for Manufacturing accounts start from 7000/000 and separated/partitioned for each following Exttype.
Eg: We start from the exttype of MA and based on its acclevel (could be 2 or more) until the next exttype.
The idea is we get the manufacturing accounts by SELECT FROM tmp_acc_list WHERE acc_code BETWEEN #start_acc_code (7000/000 in this case) AND #end_acc_code (the data before the next exttype)
I don't know what the exttype is, I'm still learning the tables.
How do we create the #end_acc_code part out from this sample table?
So here is a all in one script.
I created Your table for test:
create table #tmp_acc_list(
Id numeric,
Acc_Code nvarchar(100),
Acclevel numeric,
Acctype nvarchar(100),
Exttype nvarchar(100));
GO
insert into #tmp_acc_list(Id, Acc_Code, Acclevel, Acctype, Exttype)
select 1 , 'SA', 1,'SA', null union all
select 2 , 'CS', 1,'CS', null union all
select 3 , '5000/001', 2,'SA', null union all
select 4 , '5000/090', 2,'SA', null union all
select 5 , '7000/000', 2,'CS', 'MA' union all
select 6 , '7000/200', 2,'CS', null
;
Then comes the query:
with OrderedTable as -- to order the table is Id is not an order
(
select
t.*, ROW_NUMBER() over (
order by id asc --use any ordering You need here
)
as RowNum
from
#tmp_acc_list as t
),
MarkedTable as -- mark with common number
(
select
t.*,
Max(case when t.Exttype is null then null else t.RowNum end)
over (order by t.RowNum) as GroupRownum
from OrderedTable as t
),
GroupedTable as -- add group Exttype
(
select
t.Id, t.Acc_Code, t.Acclevel, t.Acctype, t.Exttype,
max(t.Exttype) over (partition by t.GroupRownum) as GroupExttype
from MarkedTable as t
)
select * from GroupedTable where GroupExttype = 'MA'
Is this what You need?
select *
from
(
select Id, Acc_Code
from tmp_acc_list
where Acc_Code = '7000/000'
) s
cross join tmp_acc_list a
cross apply
(
select top 1 x.Id, x.Acc_Code
from tmp_acc_list x
where x.Id >= a.Id
and x.AccLevel = a.AccLevel
and x.Acctype = a.Acctype
and x.Exttype = ''
order by Id desc
) e
where a.Id between s.Id and e.Id

Get hierarchical structure using SQL Server

I have a self-referencing table with a primary key, id and a foreign key parent_id.
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PK | NULL | IDENTITY |
| parent_id | int(11) | YES | | NULL | |
| name | varchar(255) | YES | | NULL | |
+------------+--------------+------+-----+---------+----------------+
I have got a table as following (reduce data for more clear)
Table MySiteMap
Id Name parent_id
1 A NULL
2 B 1
3 C 1
4 D 1
20 B1 2
21 B2 2
30 C1 3
31 C2 3
40 D1 4
41 D2 4
I would like get the hierarchical structure using SQL Server query:
A
|
B
|
| B1
| B2
C
|
| C1
| C2
D
|
| D1
| D2
Any suggestions?
You can use Common Table Expressions.
WITH LeveledSiteMap(Id, Name, Level)
AS
(
SELECT Id, Name, 1 AS Level
FROM MySiteMap
WHERE Parent_Id IS NULL
UNION ALL
SELECT m.Id, m.Name, l.Level + 1
FROM MySiteMap AS m
INNER JOIN LeveledSiteMap AS l
ON m.Parent_Id = l.Id
)
SELECT *
FROM LeveledSiteMap
Use this:
;WITH CTE(Id, Name, parent_id, [Level], ord) AS (
SELECT
MySiteMap.Id,
CONVERT(nvarchar(255), MySiteMap.Name) AS Name,
MySiteMap.parent_id,
1,
CONVERT(nvarchar(255), MySiteMap.Id) AS ord
FROM MySiteMap
WHERE MySiteMap.parent_id IS NULL
UNION ALL
SELECT
MySiteMap.Id,
CONVERT(nvarchar(255), REPLICATE(' ', [Level]) + '|' + REPLICATE(' ', [Level]) + MySiteMap.Name) AS Name,
MySiteMap.parent_id,
CTE.[Level] + 1,
CONVERT(nvarchar(255),CTE.ord + CONVERT(nvarchar(255), MySiteMap.Id)) AS ord
FROM MySiteMap
JOIN CTE ON MySiteMap.parent_id =CTE.Id
WHERE MySiteMap.parent_id IS NOT NULL
)
SELECT Name
FROM CTE
ORDER BY ord
For this:
A
| B
| B1
| B2
| C
| C1
| C2
| D
| D1
| D2
I started with a query, (but when I check it now it is similar to Mark.)
I will add it anyway, while I created also a sqlfiddle with mine and Mark query.
WITH tList (id,name,parent_id,nameLevel)
AS
(
SELECT t.id, t.name, t.parent_id, 1 AS nameLevel
FROM t as t
WHERE t.parent_id IS NULL
UNION ALL
SELECT tnext.id, tnext.name, tnext.parent_id, tList.nameLevel + 1
FROM t AS tnext
INNER JOIN tList AS tlist
ON tnext.parent_id = tlist.id
)
SELECT id,name,isnull(parent_id,0) 'parent_id',nameLevel FROM tList order by nameLevel;
A good blog:
SQL Query – How to get data in Hierarchical Structure?
i know changing the structure of a table is always a critical operation but since sql server 2008 introduced the HierarchyId Datatype i really like workig with it. Maybe have a look at:
http://www.codeproject.com/Articles/37171/HierarchyID-Data-Type-in-SQL-Server
http://www.codeproject.com/Tips/740553/Hierarchy-ID-in-SQL-Server
I am sure you will understand quickly how to use this datatype and his functions. The SQL Code using this datatype is more structured and has better performance than CTE's.

How to display parent id for itself and children with T-sql recursive query

I am working on recursive query which take table with parent-child relation
ID | ParentID | description
1 | null | Company
2 | 1 | Department
3 | 2 | Unit1
4 | 2 | Unit2
5 | 4 | Unit3
6 | 4 | Unit4
and is suppose to display following result:
ID | ParentID | description
1 | null | Company
2 | 2 | Department
3 | 2 | Unit1
4 | 2 | Unit2
5 | 2 | Unit3
6 | 2 | Unit4
Of course the number of Deparments and units is larger. The basic quest is to display parentId for parent and its child level. Do you have any ideas how to achive this?
So far I only made this query
WITH cte (ID, ParentID, description)
AS
(
SELECT ID, ParentID, description
FROM T1
UNION ALL
SELECT e.ID, e.ParentID, e.description
FROM T2 AS e
JOIN cte ON e.ID = cte.ParentID
)
SELECT
cte.ID, cte.ParentID, cte.description
FROM cte
cte.ParentID is not null
Your syntax isn't quite right, but the idea is in the right direction. In the end, you want to fetch the rows where the parent's parent is NULL. This might work (it is untested):
WITH cte(ID, ParentID, description, lev) AS
(SELECT ID, ParentID, description, 1 as lev
FROM table T1
UNION ALL
SELECT cte.ID, e.ParentID, cte.description, cte.lev + 1
FROM table e JOIN
cte
ON e.ID = cte.ParentID
)
SELECT cte.ID, cte.ParentID, cte.description
FROM cte left outer join
table t
on cte.ParentId = t.ParentId
WHERE t.ParentID is null;

SQL - Clone a record and its descendants

I would like to be able to clone a record and its descendants in the same table. An example of my table would be the following:
Table1
id | parentid | name
---------------------
1 | 0 | 'Food'
2 | 1 | 'Taste'
3 | 1 | 'Price'
4 | 2 | 'Taste Requirements'
The "id" column is the primary key and auto-increments. The 'Food' record (i.e. where id = 1) has two records underneath it called 'Taste' and 'Price'. The 'Taste' record has a record underneath it called 'Taste Requirements'. I would like to be able to clone the 'Food' record so that Table1 would look like the following:
Table1
id | parentid | name
---------------------
1 | 0 | 'Food'
2 | 1 | 'Taste'
3 | 1 | 'Price'
4 | 2 | 'Taste Requirements'
5 | 0 | 'Cookies'
6 | 5 | 'Taste'
7 | 5 | 'Price'
8 | 6 | 'Taste Requirements'
(where 'Cookies' is the name of the new category that I want to create). I am able to select all the descendants of 'Food' using:
with Table1_CTE( id, parentid, name )
as
(
select t.id, t.parentid, t.name from Table1 t
where t.id = 1
union all
select t.id, t.parentid,t. name from Table1 t
inner join Table1_CTE as tc
on t.parentid = tc.id
)
select id, parentid, name from Table1_CTE
and I am able to clone just the 'Food' record (i.e. where id = 1) using:
insert into Table1 ( parentid, name )
select ( parentid, 'Cookies' )
from Table1 where id = 1
but I am having problems trying to combine the two queries to clone the descendants of 'Food'. Also, I am trying to avoid using stored procedures, triggers, curosrs, etc. Is what I am trying to do possible? I have seen some examples on the web but have been unable to apply them to my requirements.
As Martin suggested, you need to enable IDENTITY_INSERT so that you can push your own identity values. You may also need to acquire a table lock to ensure that Max( Id ) returns the correct value.
If object_id('tempdb..#TestData') is not null
Drop Table #TestData
GO
Create Table #TestData
(
Id int not null identity(1,1) Primary Key
, ParentId int not null
, Name varchar(50) not null
)
GO
Set Identity_Insert #TestData On
GO
Insert #TestData( Id, ParentId, Name )
Values( 1,0,'Food' )
, ( 2,1,'Taste' )
, ( 3,1,'Price' )
, ( 4,2,'Taste Requirement' );
With Data As
(
Select Cast(MaxId.Id + 1 As int) As Id
, T.ParentId
, 'Copy Of ' + T.name As Name
, T.Id As OldId
, 0 As OldParentId
From #TestData As T
Cross Join( Select Max( id ) As Id From #TestData ) As MaxId
Where T.Name = 'Food'
Union All
Select Cast(Parent.id + Row_Number() Over( Order By Child.Id ) + 1 As int)
, Parent.Id
, 'Copy of ' + Child.Name
, Child.Id
, Child.ParentId
From Data As Parent
Join #TestData As Child
On Child.ParentId = Parent.OldId
)
Insert #TestData( Id, ParentId, Name )
Select Id, ParentId, Name
From Data
GO
Set Identity_Insert #TestData Off
GO
Results
id | parentid | name
-- | -------- | -----------------
1 | 0 | Food
2 | 1 | Taste
3 | 1 | Price
4 | 2 | Taste Requirement
5 | 0 | Copy Of Food
7 | 5 | Copy of Taste
8 | 5 | Copy of Price
9 | 7 | Copy of Taste Requirement
Assuming your CTE picks a root record and all it's descendents (it didn't seem to when I reproduced using your data above), then you can clone all selected records and insert like this:
with Table1_CTE( id, parentid, name )
as
(
select t.id, t.parentid, t.name from Table1 t
where c.icategoryid = 1
union all
select t.id, t.parentid,t. name from Table1
inner join Table1_CTE as tc
on t.parentid = tc.id
)
insert into dbo.testinsertheirarchy ( parentid, name )
select parentid, name from Table1_CTE