Iterate through records to build hierarchy - tsql

Please consider the following tables. They describe a schools' hierarchy and the notes per student.
users
-------------------------------------------------------------
root_id obj_id obj_type obj_ref_id obj_role
-------------------------------------------------------------
1 2 student 7 learn
1 3 student 7 learn
1 1 principal 1 lead
1 4 mentor 1 train teachers
1 5 trainee 4 learn teaching
1 6 trainee 4 learn teaching
1 7 teacher 1 teach
2 8 student 9 learn
2 9 principal 9 lead
notes
--------------------------------------------------------------
note_id obj_id note
--------------------------------------------------------------
1 2 foo
2 2 bar
3 2 baz
4 3 lorem
5 8 ipsum
I need to write out the hierarchy and number of notes per user as follows:
-------------------------------------------------------------------------------------------
obj_id notes obj_path
-------------------------------------------------------------------------------------------
1 0 principal 1 (lead)
2 3 student 2 (learn) > teacher 7 (teach) > principal 1 (lead)
3 1 student 3 (learn) > teacher 7 (teach) > principal 1 (lead)
4 0 mentor 4 (train teachers) > principal 1 (lead)
5 0 trainee 5 (learn teaching) > mentor 4 (train teachers) > principal 1 (lead)
6 0 trainee 6 (learn teaching) > mentor 4 (train teachers) > principal 1 (lead)
7 0 teacher 7 (teach) > principal 1 (lead)
8 1 student 8 (learn) > principal 2 (lead)
9 0 principal 9 (lead)
For this, I understand that I need to use a loop as follows:
declare cur cursor for
select obj_id from users order by root_id
open cur
declare #obj_id int
fetch next from cur into #id
while (##FETCH_STATUS = 0)
begin
select obj_role from users where obj_id = #obj_id
fetch next from cur into #obj_id
end
close cur
deallocate cur
This is what I have until now, but I do not understand how to go from here. Can someone help me on my way?

Understand that using a cursor is going to process each individual record one by one.
Recursive CTE would be a better solution:
Sql server CTE and recursion example
CTE Recursion to get tree hierarchy
How does a Recursive CTE run, line by line?
Something like:
DECLARE #User TABLE
(
[root_id] INT
, [obj_id] INT
, [Obj_type] NVARCHAR(100)
, [obj_ref_id] INT
, [obj_role] NVARCHAR(100)
);
DECLARE #Notes TABLE
(
[note_id] INT
, [obj_id] INT
, [note] NVARCHAR(255)
);
INSERT INTO #Notes (
[note_id]
, [obj_id]
, [note]
)
VALUES ( 1, 2, 'foo ' )
, ( 2, 2, 'bar ' )
, ( 3, 2, 'baz ' )
, ( 4, 3, 'lorem' )
, ( 5, 8, 'ipsum' );
INSERT INTO #User (
[root_id]
, [obj_id]
, [Obj_type]
, [obj_ref_id]
, [obj_role]
)
VALUES ( 1, 2, 'student', 7, 'learn' )
, ( 1, 3, 'student', 7, 'learn' )
, ( 1, 1, 'principal', 1, 'lead' )
, ( 1, 4, 'mentor', 1, 'train teachers' )
, ( 1, 5, 'trainee', 4, 'learn teaching' )
, ( 1, 6, 'trainee', 4, 'learn teaching' )
, ( 1, 7, 'teacher', 1, 'teach' )
, ( 2, 8, 'student', 9, 'learn' )
, ( 2, 9, 'principal', 9, 'lead' );
WITH [Hierarchy]
AS ( SELECT [obj_id] AS [root_obj]
, [obj_ref_id] AS [root_obj_ref]
, [obj_id]
, [obj_ref_id]
, CONVERT(
NVARCHAR(MAX)
, [Obj_type] + ' ' + CONVERT(NVARCHAR, [obj_id]) + ' ('
+ [obj_role] + ')'
) AS [obj_path]
FROM #User
UNION ALL
SELECT [a].[root_obj]
, [a].[root_obj_ref]
, [b].[obj_id]
, [b].[obj_ref_id]
, [a].[obj_path] + ' > ' + [b].[Obj_type]
+ CONVERT(NVARCHAR, [b].[obj_id]) + ' (' + [b].[obj_role] + ')' AS [obj_path]
FROM [Hierarchy] [a]
INNER JOIN #User [b]
ON [b].[obj_id] = [a].[obj_ref_id]
WHERE [a].[obj_id] <> [a].[obj_ref_id] ) --Here, basically continue the recursion while the record isn't referencing itself. The final will include that self referencing record.
SELECT [Hierarchy].[root_obj] AS [obj_id]
, (
SELECT COUNT(*)
FROM #Notes
WHERE [obj_id] = [Hierarchy].[root_obj]
) AS [notes] --Here we'll go out and get the count of notes.
, [Hierarchy].[obj_path]
FROM [Hierarchy]
WHERE [Hierarchy].[obj_id] = [Hierarchy].[obj_ref_id] --Then we only went those records built up to the final record that was referencing itself.
ORDER BY [Hierarchy].[root_obj];

Related

Columns to Rows in T-SQL

I have table tbl_Survey:
SurveyID 1 2 3 4
7 4 4 4 4
8 3 3 3 3
9 2 2 2 2
My goal is to transfer table headers - 1 2 3 4 into rows, as the following:
enter
SurveyID Ouestion Rating
7 1 4
7 2 4
7 3 4
7 4 4
8 1 3
8 2 3
8 3 3
8 4 3
9 1 2
9 2 2
9 3 2
9 4 2
My code is (trying to follow help recommendations):
SELECT [SurveyID]
,[Question]
,[Rating]
FROM
[tbl_Survey]
cross apply
(
values
('1', 1 ),
('2', 2 ),
('3', 3 ),
('4', 4 )
) c (Question, Rating);
Results are not fully correct (Rating column is a problem):
SurveyID Ouestion Rating
7 1 1
7 2 2
7 3 3
7 4 4
8 1 1
8 2 2
8 3 3
8 4 4
9 1 1
9 2 2
9 3 3
9 4 4
Please, help...
My problem (because of which I couldn't proceed) was that I haven't used brackets for my code.
Here is the updated code for this:
SELECT [SurveyID], [Question], [Rating]
FROM [dbo].[tbl_Survey]
UNPIVOT
(
[Rating]
FOR [Question] in ([1], [2], [3], [4])
) AS SurveyUnpivot
How about this:
DECLARE #T TABLE (SurveyID int, q1 int, q2 int, q3 int, q4 int)
INSERT #T (SurveyID, q1, q2, q3, q4)
VALUES (7,4,4,4,4), (8,3,3,3,3), (9, 2, 2, 2, 2)
SELECT SurveyID, REPLACE(Question,'q','') as Question, Rating
FROM #T UNPIVOT (Rating FOR Question in (q1, q2, q3, q4)) as UPV
Same approach. Just make sure you use a global temporary table as a temp table will not be visible in the scope of the EXEC statement. This should work with any column name and any number of columns.
IF OBJECT_ID('tempdb..##T') IS NOT NULL DROP TABLE ##T
CREATE TABLE ##T (SurveyID int, xxxxx int, yyyyy int, zzzzzz int, tttttt int)
INSERT ##T VALUES (7,4,4,4,4), (8,3,3,3,3), (9, 2, 2, 2, 2)
DECLARE #Colnames nvarchar(4000)
SELECT #Colnames = STUFF((SELECT ',[' + [name] +']' FROM tempdb.sys.columns where object_id = object_id('tempdb..##T') AND name <> 'SurveyID' FOR XML PATH('') ),1,1,'')
DECLARE #SQL nvarchar(4000) SET #SQL = 'SELECT SurveyID, Question, Rating FROM ##T UNPIVOT (Rating FOR Question in ('+#colnames+')) as UPV'
EXEC(#SQL)

Tree structure in tsql - How get data by the special class in branches

I have structure like that(as example):
ID ClassId Name Parent
--------------------------------------
1 12 Boss
2 13 Manager1 1
3 13 Manager2 1
4 13 Manager3 1
5 14 SubManager1 3
6 15 UnderSubManager1 5
7 16 Worker1 2
8 16 Worker2 6
9 14 SubManager2 4
10 16 Worker3 9
Than, we have this:
Boss->Manager1->Worker1
Boss->Manager2->SubManager1->UnderSubManager1->Worker2
Boss->Manager3->SubManager2->Worker3
I need query, that give me a this reult:
Boss->Manager1->worker1
Boss->Manager2->worker2
Boss->Manager3->worker3
I try do this witch CTE using ClassId but with poor result :(
Assuming you want to show the 2 top levels (Boss, and ManagerX), and then the lowest level (WorkerX) -
create table #tmp (ID int, ClassID int, Name varchar(32), Parent int)
go
insert into #tmp (ID, ClassID, Name, Parent)
values
(1, 12, 'Boss', null)
, (2, 13, 'Manager1', 1)
, (3, 13, 'Manager2', 1)
, (4, 13, 'Manager3', 1)
, (5, 14, 'SubManager1', 2)
, (6, 15, 'UnderSubManager1', 5)
, (7, 16, 'Worker1', 2)
, (8, 16, 'Worker2', 6)
, (9, 14, 'SubManager2', 4)
, (10, 16, 'Worker3', 9)
go
with cte as (
select t.ID, t.ClassID, t.Name, t.Parent
, Path = cast(case when t.ClassID in (12, 13) then t.Name else '' end as varchar(max))
, NestLevel = 0
, IsWorker = case t.ClassID when 16 then 1 else 0 end
from #tmp t
where t.Parent is null
union all
select t.ID, t.ClassID, t.Name, t.Parent
, Path = cte.Path + cast(case when t.ClassID in (12, 13, 16) then '->' + t.Name else '' end as varchar(max))
, NestLevel = cte.NestLevel + 1
, IsWorker = case t.ClassID when 16 then 1 else 0 end
from #tmp t
inner join cte on t.Parent = cte.ID
)
select cte.Path
from cte
where cte.IsWorker = 1
order by cte.Path
drop table #tmp
go
The result:
Boss->Manager1->Worker1
Boss->Manager1->Worker2
Boss->Manager3->Worker3

Improve performance on CTE with sub-queries

I have a table with this structure:
WorkerID Value GroupID Sequence Validity
1 '20%' 1 1 2018-01-01
1 '10%' 1 1 2017-06-01
1 'Yes' 1 2 2017-06-01
1 '2018-01-01' 2 1 2017-06-01
1 '17.2' 2 2 2017-06-01
2 '10%' 1 1 2017-06-01
2 'No' 1 2 2017-06-01
2 '2016-03-01' 2 1 2017-06-01
2 '15.9' 2 2 2017-06-01
This structure was created so that the client can create customized data for a worker. For example Group 1 can be something like "Salary" and Sequence is one value that belongs to that Group like "Overtime Compensation". The column Value is a VARCHAR(150) field and the correct validation and conversation is done in another part of the application.
The Validity column exist mainly for historical reasons.
Now I would like to show, for the different workers, the information in a grid where each row should be one worker (displaying the one with the most recent Validity):
Worker 1_1 1_2 2_1 2_2
1 20% Yes 2018-01-01 17.2
2 10% No 2016-03-01 15.9
To accomplish this I created a CTE that looks like this:
WITH CTE_worker_grid
AS
(
SELECT
worker,
/* 1 */
(
SELECT top 1 w.Value
FROM worker_values AS w
WHERE w.GroupID = 1
AND w.Sequence = 1
ORDER BY w.Validity DESC
) AS 1_1,
(
SELECT top 1 w.Value
FROM worker_values AS w
WHERE w.GroupID = 1
AND w.Sequence = 2
ORDER BY w.Validity DESC
) AS 1_2,
/* 2 */
(
SELECT top 1 w.Value
FROM worker_values AS w
WHERE w.GroupID = 2
AND w.Sequence = 1
ORDER BY w.Validity DESC
) AS 2_1,
(
SELECT top 1 w.Value
FROM worker_values AS w
WHERE w.GroupID = 2
AND w.Sequence = 2
ORDER BY w.Validity DESC
) AS 2_2
)
GO
This produces the correct result but it's very slow as it creates this grid for over 18'000 worker with almost 30 Groups and up to 20 Sequences in each Group.
How could one speed up the process of a CTE of this magnitude? Should CTE even be used? Can the sub-queries be changed or re-factored out to speed up the execution?
Use a PIVOT!
+----------+---------+---------+------------+---------+
| WorkerId | 001_001 | 001_002 | 002_001 | 002_002 |
+----------+---------+---------+------------+---------+
| 1 | 20% | Yes | 2018-01-01 | 17.2 |
| 2 | 10% | No | 2016-03-01 | 15.9 |
+----------+---------+---------+------------+---------+
SQL Fiddle: http://sqlfiddle.com/#!18/6e768/1
CREATE TABLE WorkerAttributes
(
WorkerID INT NOT NULL
, [Value] VARCHAR(50) NOT NULL
, GroupID INT NOT NULL
, [Sequence] INT NOT NULL
, Validity DATE NOT NULL
)
INSERT INTO WorkerAttributes
(WorkerID, Value, GroupID, Sequence, Validity)
VALUES
(1, '20%', 1, 1, '2018-01-01')
, (1, '10%', 1, 1, '2017-06-01')
, (1, 'Yes', 1, 2, '2017-06-01')
, (1, '2018-01-01', 2, 1, '2017-06-01')
, (1, '17.2', 2, 2, '2017-06-01')
, (2, '10%', 1, 1, '2017-06-01')
, (2, 'No', 1, 2, '2017-06-01')
, (2, '2016-03-01', 2, 1, '2017-06-01')
, (2, '15.9', 2, 2, '2017-06-01')
;WITH CTE_WA_RANK
AS
(
SELECT
ROW_NUMBER() OVER (PARTITION BY WorkerID, GroupID, [Sequence] ORDER BY Validity DESC) AS VersionNumber
, WA.WorkerID
, WA.GroupID
, WA.[Sequence]
, WA.[Value]
FROM
WorkerAttributes AS WA
),
CTE_WA
AS
(
SELECT
WA_RANK.WorkerID
, RIGHT('000' + CAST(WA_RANK.GroupID AS VARCHAR(3)), 3)
+ '_'
+ RIGHT('000' + CAST(WA_RANK.[Sequence] AS VARCHAR(3)), 3) AS SMART_KEY
, WA_RANK.[Value]
FROM
CTE_WA_RANK AS WA_RANK
WHERE
WA_RANK.VersionNumber = 1
)
SELECT
WorkerId
, [001_001] AS [001_001]
, [001_002] AS [001_002]
, [002_001] AS [002_001]
, [002_002] AS [002_002]
FROM
(
SELECT
CTE_WA.WorkerId
, CTE_WA.SMART_KEY
, CTE_WA.[Value]
FROM
CTE_WA
) AS WA
PIVOT
(
MAX([Value])
FOR
SMART_KEY IN
(
[001_001]
, [001_002]
, [002_001]
, [002_002]
)
) AS PVT

TSQL recursive CTE order

I am having trouble figuring out how I can use recursive CTEs to order my results recursively. Here is what I mean (this is a simplified dataset):
I have this as input:
declare #sections table (id int, parent int);
insert into #sections values (1, 1);
insert into #sections values (2, 2);
insert into #sections values (3, 2);
insert into #sections values (4, 2);
insert into #sections values (5, 4);
insert into #sections values (6, 1);
insert into #sections values (7, 6);
insert into #sections values (8, 6);
insert into #sections values (9, 6);
insert into #sections values (10, 9);
-- hierarchical view
--1
-- 6
-- 7
-- 8
-- 10
-- 9
--2
-- 3
-- 4
-- 5
And I want this as output
EDIT: The order of the rows is the important part here
-- id parent depth
-- 1 1 0
-- 6 1 1
-- 7 6 2
-- 8 6 2
-- 10 8 3
-- 9 6 2
-- 2 2 0
This is the best I can do:
with section_cte as
(
select id, parent, 0 'depth' from #sections where id = parent
union all
select cte.id, cte.parent, depth + 1
from #sections s join section_cte cte on s.parent = cte.id where s.id <> s.parent
)
select *from section_cte
Can anyone please help me tweak this query to get what I need?
Thanks!
You missed the part where you need to identify the depth is from the cte
WITH CTE AS (
SELECT
id
, parent
, 0 AS depth
FROM
#sections
WHERE
id=parent
UNION ALL
SELECT
s.id
, s.parent
, c.depth + 1
FROM
#sections s
JOIN CTE c ON s.parent=c.id AND s.id <> s.parent
)
SELECT *
FROM CTE;
During the recursive build, it is a small matter to add a sequence. In the example below, the order is driven by the alphabetical order of the title, but you can use any other available key/sequence.
Declare #Table table (ID int,Pt int,Title varchar(50))
Insert into #Table values (0,null,'Tags'),(1,0,'Transportation'),(2,1,'Boats'),(3,1,'Cars'),(4,1,'Planes'),(5,1,'Trains'),(6,0,'Technology'),(7,6,'FTP'),(8,6,'HTTP'),(9,0,'Finance'),(10,9,'FTP'),(11,9,'401K'),(12,2,'Sail'),(13,2,'Powered'),(14,6,'Internet'),(15,6,'Database'),(16,15,'SQL Server'),(17,15,'MySQL'),(18,15,'MS Access')
Declare #Top int = null --<< Sets top of Hier Try 9
Declare #Nest varchar(25) =' ' --<< Optional: Added for readability
;with cteHB (Seq,ID,Pt,Lvl,Title) as (
Select Seq = cast(1000+Row_Number() over (Order by Title) as varchar(500))
,ID
,Pt
,Lvl=1
,Title
From #Table
Where IsNull(#Top,-1) = case when #Top is null then isnull(Pt,-1) else ID end
Union All
Select Seq = cast(concat(cteHB.Seq,'.',1000+Row_Number() over (Order by cteCD.Title)) as varchar(500))
,cteCD.ID
,cteCD.Pt,cteHB.Lvl+1
,cteCD.Title
From #Table cteCD
Join cteHB on cteCD.Pt = cteHB.ID)
,cteR1 as (Select Seq,ID,R1=Row_Number() over (Order By Seq) From cteHB)
,cteR2 as (Select A.Seq,A.ID,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.ID )
Select Hier='HierName'
,B.R1
,C.R2
,A.ID
,A.Pt
,A.Lvl
,Title = Replicate(#Nest,A.Lvl) + A.Title
--,A.Seq --<< Normally Excluded, but you can see how the sequence is built
From cteHB A
Join cteR1 B on A.ID=B.ID
Join cteR2 C on A.ID=C.ID
Order By A.Seq --<< Use R1 if Range Keys are used
Returns
Hier R1 R2 ID Pt Lvl Title
HierName 1 19 0 NULL 1 Tags
HierName 2 4 9 0 2 Finance
HierName 3 3 11 9 3 401K
HierName 4 4 10 9 3 FTP
HierName 5 12 6 0 2 Technology
HierName 6 9 15 6 3 Database
HierName 7 7 18 15 4 MS Access
HierName 8 8 17 15 4 MySQL
HierName 9 9 16 15 4 SQL Server
HierName 10 10 7 6 3 FTP
HierName 11 11 8 6 3 HTTP
HierName 12 12 14 6 3 Internet
HierName 13 19 1 0 2 Transportation
HierName 14 16 2 1 3 Boats
HierName 15 15 13 2 4 Powered
HierName 16 16 12 2 4 Sail
HierName 17 17 3 1 3 Cars
HierName 18 18 4 1 3 Planes
HierName 19 19 5 1 3 Trains
Now, you may have noticed R1 and R2. These are my range keys, and are often used to aggregate data without recursion. If you don't need or want these, just remove the cteR1 and cteR2 (and the corresponding references in the final SELECT).
EDIT
You also have the option of selecting a portion of the hierarchy (i.e. Technology and its children).
The trick you need is to create a sortstring which match your business rule. Something like this.
WITH CTE AS (
SELECT
id, parent, 0 AS depth
--For MS SQL Server 2012+
,cast(format(id,'0000') as varchar(max)) sort
--For previous versions
,cast(stuff('0000',5-len(cast(id as varchar)),len(cast(id as varchar)),id) as varchar(max)) sort1
FROM #sections
WHERE id=parent
UNION ALL
SELECT s.id, s.parent, c.depth + 1
,sort + cast(format(s.id,'0000') as varchar(max)) sort
,sort + cast(stuff('0000',5-len(cast(s.id as varchar)),len(cast(s.id as varchar)),s.id) as varchar(max)) sort1
FROM #sections s
inner join CTE c ON s.parent=c.id AND s.id <> s.parent
)
SELECT *
FROM CTE
order by sort --or by sort1 depending on version

Postgresql: only keep unique values from integer array

Let's say I have an array of integers
1 6 6 3 3 8 4 4
It will be always of the form n*(pairs of number) + 2 (unique numbers).
Is there an efficient way of keeping only the 2 uniques values (i.e. the 2 with single occurence)?
Here, I would like to get 1 and 8.
So far is what I have:
SELECT node_id
FROM
( SELECT node_id, COUNT(*)
FROM unnest(array[1, 6, 6 , 3, 3 , 8 , 4 ,4]) AS node_id
GROUP BY node_id
) foo
ORDER BY count LIMIT 2;
You are very close, I think:
SELECT node_id
FROM (SELECT node_id, COUNT(*)
FROM unnest(array[1, 6, 6 , 3, 3 , 8 , 4 ,4]) AS node_id
GROUP BY node_id
HAVING count(*) = 1
) foo ;
You can group these back into an array, if you like, using array_agg().