Get full hierarchy by children - sql-server

I have 2 tables:
contains full tree data
contains only specific childs
I need to get full hierarchy by child values. I can do it by one specific child node by following way:
;with tree as
(
select id, parent_id, name, level from f_all where id = #specefic_id
union all
select f.id, f.parent_id, f.name, f.level from f_all f
inner join tree t on f.id = t.parent_id and f.id <> f.parent_id
)
select *
from tree
OPTION (Maxrecursion 0)
I have an idea but I think it is not good. My idea is create function with above code. And call it by select my second table. I even didn't try it. Can you give me a right direction.

This is 2012+ ( Using concat() ... easily converted ).
Declare #f_all table (id int,parent_id int,name varchar(50))
Insert into #f_all values
(1,null,'1'),(2,1,'2'),(3,1,'3'),(4,2,'4'),(5,2,'5'),(6,3,'6'),(7,null,'7'),(8,7,'8')
Declare #Top int = null --<< Sets top of Hier Try 9
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
Declare #Filter varchar(25) = '4,6' --<< Empty for All or try 4,6
;with cteP as (
Select Seq = cast(1000+Row_Number() over (Order by name) as varchar(500))
,ID
,parent_id
,Lvl=1
,name
From #f_all
Where IsNull(#Top,-1) = case when #Top is null then isnull(parent_id,-1) else ID end
Union All
Select Seq = cast(concat(p.Seq,'.',1000+Row_Number() over (Order by r.name)) as varchar(500))
,r.ID
,r.parent_id
,p.Lvl+1
,r.name
From #f_all r
Join cteP p on r.parent_id = p.ID)
,cteR1 as (Select *,R1=Row_Number() over (Order By Seq) From cteP)
,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 Distinct
A.R1
,B.R2
,A.ID
,A.parent_id
,A.Lvl
,name = Replicate(#Nest,A.Lvl-1) + A.name
From cteR1 A
Join cteR2 B on A.ID=B.ID
Join (Select R1 From cteR1 where IIF(#Filter='',1,0)+CharIndex(concat(',',ID,','),concat(',',#Filter+','))>0) F on F.R1 between A.R1 and B.R2
Order By A.R1
Returns (#Top=null and #Filter='4,6')
Return Full Hier (#Top=null and #Filter='')
Returns Just a portion (#Top=2 and #Filter='')

The problem for me was that i didn't know how cte recursion works. Now i know how it works line by line: Recursive Queries Using Common Table Expressions.
Code below returns all hierarchy by children nodes:
;with tree as(
select id, parent_id, name, level from f_all fa
inner join #2nd_table_cildren_id c on c.id = fa.id
union all
select f.id, f.parent_id, f.name, f.level from f_all f
inner join tree t on f.id = t.parent_id and f.id <> f.parent_id
)
select distinct *
from tree
OPTION (Maxrecursion 0)

Related

Select all records when multiple value parameter is blank

I'm creating a report in SSRS and want to have a multiple value parameter but allow blank values (''), and display all records when blank.
The gist of it is:
SELECT *
FROM Products p
JOIN ProductCategories c on c.ProductId = p.Id
WHERE (c.Name IN (#Categories) OR #Categories = '')
Which works when blank, and works with 1 category, but errors out with 2 categories. We got around this by using a temp table, but that solution seemed sort of hacky, so I wanted to see if there was a better way to resolve this.
The temp table workaround we built was this:
CREATE TABLE #temp (ProductId INT, Category NVARCHAR(MAX))
INSERT INTO #temp
SELECT p.Id, c.Name
FROM Products p
JOIN ProductCategories c on c.ProductId = p.Id
WHERE c.Name IN (#Categories)
IF ((SELECT COUNT(*) FROM #temp) = 0)
BEGIN
INSERT INTO #temp
SELECT p.Id, c.Name
FROM Products p
JOIN ProductCategories c on c.ProductId = p.Id
WHERE c.Name LIKE '%'
END
SELECT * FROM #temp
Thanks in advance!
If you don't have a split/parse function
Example
...
Where #Categories = ''
or
C.Name in (
Select RetVal = ltrim(rtrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#Categories,',','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
)
Here is a dbFiddle ... You'll notice Poultry was excluded, then try it when #Categories=''

How to update a hierarchyid column

I have a table called items which has a parent/child relation that I am converting to a hierarchyid.
I have followed this tutorial to do so.
All the steps of the tutorial works, except the final update statement.
I get the error message:
Implicit conversion from data type hierarchyid to nvarchar(max) is not allowed. Use the CONVERT function to run this query.
But this makes no sense. The field I am updating is a hierarchyid, not an nvarchar(nax). So I don't see where an nvarchar(max) field is involved.
drop table #children
CREATE TABLE #Children
(
ID int,
TenantId int,
ParentID int,
Num int
);
GO
CREATE CLUSTERED INDEX tmpind ON #Children(TenantId, ParentID, ID);
GO
INSERT #Children (ID, TenantId,ParentID, Num)
SELECT ID, TenantId, ParentID,
ROW_NUMBER() OVER (PARTITION BY TenantId, ParentID ORDER BY ParentId)
FROM Items
GO
SELECT * FROM #Children ORDER BY TenantId, ParentID, Num
GO
WITH paths(path, ID, ParentId, TenantId)
AS (
-- This section provides the value for the root of the hierarchy
SELECT hierarchyid::GetRoot() AS OrgNode, ID, ParentId, TenantId
FROM #Children AS C
WHERE ParentId IS NULL
UNION ALL
-- This section provides values for all nodes except the root
SELECT
CAST(p.path.ToString() + CAST(C.Num AS varchar(30)) + '/' AS hierarchyid),
C.ID , C.ParentId, C.TenantId
FROM #Children AS C
JOIN paths AS p
ON C.ParentID = P.ID
)
-- This select statement runs just fine and shows expected data.
--Select i.Id as ItemId, p.path, p.path.ToString() as LogicalNode, p.Id, p.ParentId, p.TenantId from Paths P
--join Items I on p.Id = i.Id
--order by P.TenantId, P.path
--Note that I have tried using the convert function, but it still fails with the same error message.
UPDATE I Set OrgNode = Convert(hierarchyid, P.path)
FROM Items I
JOIN Paths AS P
ON I.ID = P.ID
GO
EDIT
Strangely, this DBFiddle works.
It looks like column OrgNode is not of type hierachyid. You could use ToString()
UPDATE I Set OrgNode = P.path.ToString()
FROM Items I
JOIN Paths AS P
ON I.ID = P.ID
or alter table Items and change column type.
It looks like you solved your problem, but I would suggest saving the conversion to hierarchyid to the end. Like this:
WITH paths(path, ID, ParentId, TenantId)
AS (
-- This section provides the value for the root of the hierarchy
SELECT cast('/' as varchar(max)) AS OrgNode, ID, ParentId, TenantId
FROM #Children AS C
WHERE ParentId IS NULL
UNION ALL
-- This section provides values for all nodes except the root
SELECT
CAST(concat(p.path.ToString(), C.Num, '/') AS varchar(max)),
C.ID , C.ParentId, C.TenantId
FROM #Children AS C
JOIN paths AS p
ON C.ParentID = P.ID
)
-- This select statement runs just fine and shows expected data.
--Select i.Id as ItemId, p.path, p.path.ToString() as LogicalNode, p.Id, p.ParentId, p.TenantId from Paths P
--join Items I on p.Id = i.Id
--order by P.TenantId, P.path
--Note that I have tried using the convert function, but it still fails with the same error message.
UPDATE I Set OrgNode = Convert(hierarchyid, P.path)
FROM Items I
JOIN Paths AS P
ON I.ID = P.ID
GO
Note, I also changed the + style of concatenation for the concat() function so you don't have to mess around with converting C.Num to a varchar.

Unexpected result using CTE to perform a random join on two tables for all rows one-to-many

I am attempting to randomly join the rows of two tables (TableA and TableB) such that each row in TableA is joined to only one row in TableB and every row in TableB is joined to at least one row in TableA.
For example, a random join of TableA with 5 distinct rows and TableB with 3 distinct rows should result in something like this:
TableA TableB
1 3
2 1
3 1
4 2
5 1
However, sometimes not all the rows from TableB are included in the final result; so in the example above might have row 2 from TableB missing because in its place is either row 1 or 3 joined to row 4 on TableA. You can see this occur by executing the script a number of times and checking the result. It seems that it is necessary for some reason to use an interim table (#Q) to be able to ensure that a correct result is returned which has all rows from both TableA and TableB.
Can someone please explain why this is happening?
Also, can someone please advise on what would be a better way to get the desired result?
I understand that sometimes no result is returned due to a failure of some kind in the cross apply and ordering which i have yet to identify and goes to the point that I am sure there is a better way to perform this operation. I hope that makes sense. Thanks in advance!
declare #TableA table (
ID int
);
declare #TableB table (
ID int
);
declare #Q table (
RN int,
TableAID int,
TableBID int
);
with cte as (
select
1 as ID
union all
select
ID + 1
from cte
where ID < 5
)
insert #TableA (ID)
select ID from cte;
with cte as (
select
1 as ID
union all
select
ID + 1
from cte
where ID < 3
)
insert #TableB (ID)
select ID from cte;
select * from #TableA;
select * from #TableB;
with cte as (
select
row_number() over (partition by TableAID order by newid()) as RN,
TableAID,
TableBID
from (
select
a.ID as TableAID,
b.ID as TableBID
from #TableA as a
cross apply #TableB as b
) as M
)
select --All rows from TableB not always included
TableAID,
TableBID
from cte
where RN in (
select
top 1
iCTE.RN
from cte as iCTE
group by iCTE.RN
having count(distinct iCTE.TableBID) = (
select count(1) from #TableB
)
)
order by TableAID;
with cte as (
select
row_number() over (partition by TableAID order by newid()) as RN,
TableAID,
TableBID
from (
select
a.ID as TableAID,
b.ID as TableBID
from #TableA as a
cross apply #TableB as b
) as M
)
insert #Q
select
RN,
TableAID,
TableBID
from cte;
select * from #Q;
select --All rows from both TableA and TableB included
TableAID,
TableBID
from #Q
where RN in (
select
top 1
iQ.RN
from #Q as iQ
group by iQ.RN
having count(distinct iQ.TableBID) = (
select count(1) from #TableB
)
)
order by TableAID;
See if this gives you what you're looking for...
DECLARE
#CountA INT = (SELECT COUNT(*) FROM #TableA ta),
#CountB INT = (SELECT COUNT(*) FROM #TableB tb),
#MinCount INT;
SELECT #MinCount = CASE WHEN #CountA < #CountB THEN #CountA ELSE #CountB END;
WITH
cte_A1 AS (
SELECT
*,
rn = ROW_NUMBER() OVER (ORDER BY NEWID())
FROM
#TableA ta
),
cte_B1 AS (
SELECT
*,
rn = ROW_NUMBER() OVER (ORDER BY NEWID())
FROM
#TableB tb
),
cte_A2 AS (
SELECT
a1.ID,
rn = CASE WHEN a1.rn > #MinCount THEN a1.rn - #MinCount ELSE a1.rn end
FROM
cte_A1 a1
),
cte_B2 AS (
SELECT
b1.ID,
rn = CASE WHEN b1.rn > #MinCount THEN b1.rn - #MinCount ELSE b1.rn end
FROM
cte_B1 b1
)
SELECT
A = a.ID,
B = b.ID
FROM
cte_A2 a
JOIN cte_B2 b
ON a.rn = b.rn;

Efficient way to get ancestors - materialized path

I have a hierarchical data structure stored using materialized paths.
Table:Files
node parentNode name path
100 NULL f1 /f1/
101 100 f2 /f1/f2/
102 101 f3 /f1/f2/f3/
I have the node column as primary key(clustered)
Now if I want to find the ancestors of f3, given the path, I do something like this:
SELECT * FROM Files WHERE '/f1/f2/f3/' LIKE [path] + '%'
The problem with this is, the execution plan uses a clustered index scan( which I think SQL server defaults to for table scans)
Is there anyway I can find the ancestors of a node, given the path in a more efficient manner, preferably not using a CTE? I also have a depth column at my disposal if required.
If you have slow moving hierarchies, consider adding Range Keys. They facilitate navigation, filtration, and/or aggregration without the need of recursion.
The Range keys indicate ownership between X and Y. The range keys are especially helpful when dealing with large hierarchies (180K nodes).
The following is a simplified example, but may help.
Sample Hier Build
--Drop Table #MyHier
Declare #YourTable table (id int,ParentId int,Name varchar(50))
Insert into #YourTable values
(11, NULL,'A')
,(12, 11 ,'B')
,(13, 12 ,'F')
,(14, 13 ,'C')
,(15, 13 ,'D')
,(16, 11 ,'E')
,(17, 12 ,'G')
,(18, NULL ,'M')
,(19, 18 ,'N')
,(20, 18 ,'O')
,(21, 20 ,'P')
Declare #Top int = null --<< Sets top of Hier Try 3
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Name) as varchar(500))
,ID
,ParentId
,Lvl=1
,Name
,Path = cast('/'+Name+'/' as varchar(500))
From #YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(ParentId ,-1) else ID end
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Name)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Name
,cast(p.path + '/'+r.Name+'/' as varchar(500))
From #YourTable r
Join cteP p on r.ParentId = p.ID)
,cteR1 as (Select *,R1=Row_Number() over (Order By Seq) From cteP)
,cteR2 as (Select 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 A.R1
,B.R2
,A.ID
,A.ParentId
,A.Lvl
,Name = Replicate(#Nest,A.Lvl-1) + A.Name
,Path
Into #MyHier
From cteR1 A
Join cteR2 B on A.ID=B.ID
Select Full Hier
-- Get The Full Hier
Select *
From #MyHier
Order By R1
Returns
Get Ancestors
-- Get Ancestors of a Node
Declare #GetAncestors int = 15
Select A.*
From #MyHier A
Join (Select R1 From #MyHier Where ID=#GetAncestors) B
on B.R1 between A.R1 and A.R2
Order By A.R1
Returns
Select Descendants
-- Get Descendants of a Node
Declare #GetDesendants int = 12
Select A.*
From #MyHier A
Join (Select R1,R2 From #MyHier Where ID=#GetDesendants) B
on A.R1 between B.R1 and B.R2
Order By A.R1
Returns

How to join first two child records to parent?

I am trying to select the top 25 parent records and join to it the first two child records ordered by date. The parent record can have 0 to n children.
The end result would be something like:
P1, C1, C2
P2, C1, C2
...
P25, C1, C2
I have found an example using max date, but I am having trouble getting a specific row number
select top 25 *
from parentTable p
left join childTable c
on p.Key = c.Key
and c.dateColumn = (
select Max(c.dateColumn)
from c
where p.Key = c.Key
)
You should be able to get what you are looking for using ROW_NUMBER():
WITH c AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY [key] ORDER BY [date] asc) as rn
,[key]
,[date]
from child
)
SELECT top 25 p.[key], c1.[key], c1.[date], c2.[key], c2.[date]
FROM parent p
LEFT JOIN c c1
ON p.[key] = c1.[key]
AND c1.rn = 1
LEFT JOIN c c2
ON p.[key] = c2.[key]
AND c2.rn = 2
Check SQLFiddle for test data/results
You can use CTE.
;with cte as (
select top (25) *
from parentTable
)
select *
from cte p
left join childTable c
on p.[Key] = c.[Key];
First solution (because data set seems to be small) one solution is to use OUTER APPLY:
select top (25) ... columns ...
from parentTable p
outer apply (
select top (2) ... columns ...
from childTable c
where p.[Key] = c.[Key]
order by c.dateColumn desc -- asc ?
) a
-- Most of the times, when top filter is used order by clause should be also used
order by p.dateColumn desc -- asc ?
-- order by p.idColumn desc -- asc ?
Second solution (could be less eficient):
select top (25) ... columns ...
from parentTable p
left join (
select top (2) ... columns ..., ROW_NUMBER() over(partition by c.[Key] order by c.dateColumn desc) as rn -- asc ?
from childTable c
) a on p.[Key] = a.[Key] and a.rn < 3
-- Most of the times, when top filter is used order by clause should be also used
order by p.dateColumn desc -- asc ?
-- order by p.idColumn desc -- asc ?
Note: at least for the first solution one of following indices could help from the point of view of performance:
create index ix_name on dbo.childTable ([Key], [dateColumn])
--or
create index ix_name on dbo.childTable ([Key], [dateColumn])
include (... columns from select top(2) clause ...)

Resources