Custom hierarchy - sql

I've a table source
idGeo GEO PARENTID
1 EMEA NULL
2 France 1
3 mIDCAPSfRANCE 2
4 Germany 1
5 France exl midcaps 2
6 Amercias NULL
7 US 6
The expected result of the hierarchy
I tried to do left join(self join) but I'm not able to get exactly as expected.

Here is a generic method regardless of the level of the hierarchy.
SQL
-- DDL and sample data population, start
DECLARE #tbl table (
idGeo INT IDENTITY PRIMARY KEY,
GEO VARCHAR(64),
PARENTID INT
);
insert into #tbl (GEO, PARENTID) values
( 'EMEA', NULL),
( 'France', 1),
( 'mIDCAPSfRANCE', 2),
( 'Germany', 1),
( 'France exl midcaps', 2),
( 'Amercias', NULL),
( 'US', 6);
-- DDL and sample data population, end
--SELECT * FROM #tbl;
WITH cte AS
(
-- Anchor query
SELECT idGEO, GEO, ParentID, 1 AS [Level]
, CAST('/' + GEO AS VARCHAR(1000)) AS XPath
FROM #tbl
WHERE ParentID IS NULL
UNION ALL
-- Recursive query
SELECT t.idGEO, t.GEO, t.ParentID, cte.[Level] + 1 AS [Level]
, CAST(cte.[XPath] + '/' + t.GEO AS VARCHAR(1000)) AS [XPath]
FROM #tbl AS t
INNER JOIN cte ON t.ParentID = cte.idGEO
WHERE t.ParentID IS NOT NULL
)
SELECT idGEO
, REPLICATE(' ',[Level]-1) + GEO AS GEOHierarchy
, GEO, ParentID, [Level], [XPath]
FROM cte
ORDER BY XPath;
Output

So if I understand you want to generate columns as the levels go on.
You cannot dynalically generate columns, SQL is a fixed column language.

Related

TSQL - How to avoid UNION ALL

Sample Data:
DECLARE #Parent TABLE
(
[Id] INT
, [Misc_Val] VARCHAR(5)
) ;
DECLARE #Children TABLE
(
[Id] INT
, [P_ID] INT
) ;
INSERT INTO #Parent
VALUES
( 1, 'One' )
, ( 2, 'Two' )
, ( 3, 'Three' )
, ( 5, 'Four' ) ;
INSERT INTO #Children
VALUES
( 10, 1 )
, ( 11, 1 )
, ( 21, 2 )
, ( 23, 2 )
, ( 30, 3 )
, ( 40, 4 ) ;
Goal:
To efficiently output three fields ( [Id] and [IsChild], [Misc_Val] ). Output all records from #Parent table with [IsChild] = 0 and output all MATCHING records from #Child table (#Parent.Id = #Children.P_Id) with [IsChild] = 1.
Expected Output
Id IsChild Misc_Val
1 0 One
2 0 Two
3 0 Three
5 0 Four
10 1 One
11 1 One
21 1 Two
23 1 Two
30 1 Three
My try:
SELECT [P].[Id]
, 0 AS [IsChild]
, [P].[Misc_Val]
FROM #Parent AS [P]
UNION ALL
SELECT [C].[Id]
, 1
, [P].[Misc_Val]
FROM #Parent AS [P]
JOIN #Children AS [C]
ON [C].[P_ID] = [P].[Id] ;
Is there a better way to do this than using UNION ALL? #Parent and #Children tables are quite big and so am trying to avoid querying the #Parent table twice.
UPDATE: The below answer made me realized something I missed out when creating the post with mocked data. We do need some additional data from #Parent table regardless in the final output.
You can use CROSS APPLY to add the child table to the parent table.
This may or may not be faster, it can depend on indexing and so forth. You need to check the query plan.
SELECT v.Id
, v.IsChild
, P.Misc_Val
FROM #Parent AS P
CROSS APPLY (
SELECT
P.Id,
0 AS IsChild
UNION ALL
SELECT
C.Id,
1
FROM #Children AS C
WHERE C.P_ID = P.Id
) v;
Note that the first SELECT in the apply has no FROM and therefore does not do any table access.

Get hierarchical data is SQL SERVER with fallback logic

Consider the below schema
dbo.Cultures (Id, CultureCode, ParentId)
Culture table stores the data in the parent-child relationship.
Suppose we have below demo data
5 es-ES 3
Now I have another table which stores the multilingual data for the different cultures.
Schema for the table is as following
dbo.LangData(KeyName, CultureId, Value)
here cultureId is the foreign key of dbo.Cultures table.
Suppose this table has following data
Now I require to fetch the data for all the cultures which are in the Culture table and the corresponding value column in the LangData table.
The culture Ids which are not in the LangData table, for those the Value column will the value of the corresponding parent culture Id columns value. I.e. Data will be retrieved using fallback logic
E.g. For the above values the Result set will be following.
5 es-ES Colour_IN
Here for de-DE data is missing in LangData so it's value will be the data in it's parent culture i.e. en-IN, if in case data also not found in en-IN then it will pick the data of it's parent en-US.
Tried Soloution
First I fetch the culture hierarchy using CTE
CREATE FUNCTION [dbo].[ufnGetCultureHierarchyAll] ()
RETURNS #hierarchyResult TABLE(RowNo INT, CultureId INT, ParentCultureId INT)
AS
BEGIN
WITH CultureHierarchy_CTE(RowNo, CultureId, ParentCultureId)
AS (
SELECT 1,
Id,
ParentId
FROM [dbo].Cultures
UNION ALL
SELECT RowNo + 1,
ou.Id,
ou.ParentId
FROM [dbo].Cultures ou
JOIN CultureHierarchy_CTE cte
ON ou.Id = cte.ParentCultureId
)
-- inserting desired records into table and returning
INSERT INTO #hierarchyResult (RowNo,CultureId,ParentCultureId )
SELECT RowNo, CultureId , ParentCultureId FROM CultureHierarchy_CTE
RETURN;
END
This will return the hierarchy of the all the cultures
Now I attempted to apply join of the result set with the LangData table,
DECLARE #cultureHierarchy AS TABLE(
RowNumber INT,
CultureId INT,
ParentCultureId INT
)
--SELECT * FROM master.Cultures
----Get and store culture hierarchy
INSERT INTO #cultureHierarchy
SELECT RowNo, CultureId, ParentCultureId
FROM ufnGetCultureHierarchyAll()
SELECT c.Code AS [CultureCode],
c.CultureId AS [CultureId],
rv.Value
FROM dbo.LangData rv WITH (NOLOCK)
JOIN #cultureHierarchy c ON rv.CultureId = c.CultureId
END
but it is not working.
Is someone have any Idea regarding same.
Solution using Itzik Ben-Gan's hierarchy model. If you can extend the dbo.Cultures table with Hierarchy, Lvl and Root columns and index on Hierarchy, query will be faster. It has to be rewrited in that case though.
drop table if exists dbo.Cultures;
create table dbo.Cultures (
ID int
, Code varchar(50)
, ParentID int
);
insert into dbo.Cultures (ID, Code, ParentID)
values (1, 'en-US', null), (2, 'en-IN', 1), (3, 'de-DE', 2), (4, 'hi-HI', 2)
drop table if exists dbo.LangData;
create table dbo.LangData (
KeyName varchar(100)
, CultureID int
, Value varchar(100)
);
insert into dbo.LangData (KeyName, CultureID, Value)
values ('lblColourName', 1, 'Color'), ('lblColourName', 2, 'Colour-IN');
with cteCultures as (
select
c.ID, c.Code, c.ParentID, 0 as Lvl
, convert(varchar(max), '.' + CONVERT(varchar(50), c.ID) + '.') as Hierarchy
, c.ID as Root
from dbo.Cultures c
where c.ParentID is null
union all
select
c.ID, c.Code, c.ParentID, cc.Lvl + 1 as Lvl
, cc.Hierarchy + convert(varchar(50), c.ID) + '.' as Hierarchy
, cc.Root as Root
from dbo.Cultures c
inner join cteCultures cc on c.ParentID = cc.ID
)
select
ccr.ID
, ccr.Code
, coalesce(ld.Value, ld2.Value) as Value
from cteCultures ccr
left join dbo.LangData ld on ccr.ID = ld.CultureID
outer apply (
select
top (1) tcc.ID
from cteCultures tcc
inner join dbo.LangData tld on tcc.ID = tld.CultureID
where ld.KeyName is null
and ccr.Hierarchy like tcc.Hierarchy + '%'
and ccr.Hierarchy <> tcc.Hierarchy
order by tcc.Lvl desc
) tt
left join dbo.LangData ld2 on tt.ID = ld2.CultureID
If I understand your question:
We just build your hierarchy (SEQ and Lvl are optional) and then perform TWO left joins in concert with a Coalesce().
Example
Declare #Cultures table (id int,ParentId int,Code varchar(50))
Insert into #Cultures values
( 1, NULL,'en-US')
,( 2, 1 ,'en-IN')
,( 3, 2 ,'de-DE')
,( 4, 2 ,'hi-HI')
Declare #LangData table (keyName varchar(50),CultureId int,Value varchar(50))
Insert Into #LangData values
('lblColourName',1,'Color')
,('lblColourName',2,'Color_IN')
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Code) as varchar(500))
,ID
,ParentId
,Lvl=1
,Code
From #Cultures
Where ParentId is null
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Code)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Code
From #Cultures r
Join cteP p on r.ParentId = p.ID)
Select CultureId = A.ID
,A.Code
,Value = Coalesce(C.Value,B.Value)
From cteP A
Left Join #LangData B on (A.ParentId=B.CultureId)
Left Join #LangData C on (A.Id=C.CultureId)
Order By Seq
Returns
CultureId Code Value
1 en-US Color
2 en-IN Color_IN
3 de-DE Color_IN
4 hi-HI Color_IN

Recursive select in SQL

I have an issue I just can't get my head around. I know what I want, just simply can't get it out on the screen.
What I have is a table looking like this:
Id, PK UniqueIdentifier, NotNull
Name, nvarchar(255), NotNull
ParentId, UniqueIdentifier, Null
ParentId have a FK to Id.
What I want to accomplish is to get a flat list of all the id's below the Id I pass in.
example:
1 TestName1 NULL
2 TestName2 1
3 TestName3 2
4 TestName4 NULL
5 TestName5 1
The tree would look like this:
-1
-> -2
-> -3
-> -5
-4
If I now ask for 4, I would only get 4 back, but if I ask for 1 I would get 1, 2, 3 and 5.
If I ask for 2, I would get 2 and 3 and so on.
Is there anyone who can point me in the right direction. My brain is fried so I appreciate all help I can get.
declare #T table(
Id int primary key,
Name nvarchar(255) not null,
ParentId int)
insert into #T values
(1, 'TestName1', NULL),
(2, 'TestName2', 1),
(3, 'TestName3', 2),
(4, 'TestName4', NULL),
(5, 'TestName5', 1)
declare #Id int = 1
;with cte as
(
select T.*
from #T as T
where T.Id = #Id
union all
select T.*
from #T as T
inner join cte as C
on T.ParentId = C.Id
)
select *
from cte
Result
Id Name ParentId
----------- -------------------- -----------
1 TestName1 NULL
2 TestName2 1
5 TestName5 1
3 TestName3 2
Here's a working example:
declare #t table (id int, name nvarchar(255), ParentID int)
insert #t values
(1, 'TestName1', NULL),
(2, 'TestName2', 1 ),
(3, 'TestName3', 2 ),
(4, 'TestName4', NULL),
(5, 'TestName5', 1 );
; with rec as
(
select t.name
, t.id as baseid
, t.id
, t.parentid
from #t t
union all
select t.name
, r.baseid
, t.id
, t.parentid
from rec r
join #t t
on t.ParentID = r.id
)
select *
from rec
where baseid = 1
You can filter on baseid, which contains the start of the tree you're querying for.
Try this:
WITH RecQry AS
(
SELECT *
FROM MyTable
UNION ALL
SELECT a.*
FROM MyTable a INNER JOIN RecQry b
ON a.ParentID = b.Id
)
SELECT *
FROM RecQry
Here is a good article about Hierarchy ID models. It goes right from the start of the data right through to the query designs.
Also, you could use a Recursive Query using a Common Table Expression.
I'm guessing that the easiest way to accomplish what you're looking for would be to write a recursive query using a Common Table Expression:
MSDN - Recursive Queries Using Common Table Expressions

Help with recursive CTE query joining to a second table

My objective is to recurse through table tbl and while recursing through that table select a country abbreviation (if it exists) from another table tbl2 and append those results together which are included in the final output.
The example I'll use will come from this post
tbl2 has a Foreign Key 'tbl_id' to tbl and looks like this
INSERT INTO #tbl2( Id, Abbreviation, tbl_id )
VALUES
(100, 'EU', 1)
,(101, 'AS', 2)
,(102, 'DE', 3)
,(103, 'CN', 5)
*Note: not all the countries have abbreviations.
The trick is, I want all the countries in Asia to at least show the abbreviation of Asia which is 'AS' even if a country doesn't have an abbreviation (like India for example). If the country does have an abbreviation the result needs to look like this: China:CN,AS
I've got it partly working using a subquery, but India always returns NULL for the abbreviation. It's acting like if there isn't a full recursive path back to the abbreviation, then it returns null. Maybe the solution is to use a left outer join on the abbreviation table? I've tried for hours many different variations and the subquery is as close as I can get.
WITH abcd
AS (
-- anchor
SELECT id, [Name], ParentID,
CAST(([Name]) AS VARCHAR(1000)) AS "Path"
FROM #tbl
WHERE ParentId IS NULL
UNION ALL
--recursive member
SELECT t.id, t.[Name], t.ParentID,
CAST((a.path + '/' + t.Name + ':' +
(
select t2.abbreviation + ','
from #tbl2
where t.id = t2.id
)) AS VARCHAR(1000)) AS "Path"
FROM #tbl AS t
JOIN abcd AS a
ON t.ParentId = a.id
)
SELECT * FROM abcd
btw, I'm using sql server 2005 if that matters
Try this example, which will give you the output (1 sample row)
id Name ParentID Path abbreviation (No column name)
5 China 2 Asia/China CN,AS Asia/China:CN,AS
The TSQL being
DECLARE #tbl TABLE (
Id INT
,[Name] VARCHAR(20)
,ParentId INT
)
INSERT INTO #tbl( Id, Name, ParentId )
VALUES
(1, 'Europe', NULL)
,(2, 'Asia', NULL)
,(3, 'Germany', 1)
,(4, 'UK', 1)
,(5, 'China', 2)
,(6, 'India', 2)
,(7, 'Scotland', 4)
,(8, 'Edinburgh', 7)
,(9, 'Leith', 8)
;
DECLARE #tbl2 table (id int, abbreviation varchar(10), tbl_id int)
INSERT INTO #tbl2( Id, Abbreviation, tbl_id )
VALUES
(100, 'EU', 1)
,(101, 'AS', 2)
,(102, 'DE', 3)
,(103, 'CN', 5)
;WITH abbr AS (
SELECT a.*, isnull(b.abbreviation,'') abbreviation
FROM #tbl a
left join #tbl2 b on a.Id = b.tbl_id
), abcd AS (
-- anchor
SELECT id, [Name], ParentID,
CAST(([Name]) AS VARCHAR(1000)) [Path],
cast(abbreviation as varchar(max)) abbreviation
FROM abbr
WHERE ParentId IS NULL
UNION ALL
--recursive member
SELECT t.id, t.[Name], t.ParentID,
CAST((a.path + '/' + t.Name) AS VARCHAR(1000)) [Path],
isnull(nullif(t.abbreviation,'')+',', '') + a.abbreviation
FROM abbr AS t
JOIN abcd AS a
ON t.ParentId = a.id
)
SELECT *, [Path] + ':' + abbreviation
FROM abcd

t-sql recursive query

Based on an existing table I used CTE recursive query to come up with following data. But failing to apply it a level further.
Data is as below
id name parentid
--------------------------
1 project 0
2 structure 1
3 path_1 2
4 path_2 2
5 path_3 2
6 path_4 3
7 path_5 4
8 path_6 5
I want to recursively form full paths from the above data. Means the recursion will give the following output.
FullPaths
-------------
Project
Project\Structure
Project\Structure\Path_1
Project\Structure\Path_2
Project\Structure\Path_3
Project\Structure\Path_1\path_4
Project\Structure\Path_2\path_5
Project\Structure\Path_3\path_6
Thanks
Here's an example CTE to do that:
declare #t table (id int, name varchar(max), parentid int)
insert into #t select 1, 'project' , 0
union all select 2, 'structure' , 1
union all select 3, 'path_1' , 2
union all select 4, 'path_2' , 2
union all select 5, 'path_3' , 2
union all select 6, 'path_4' , 3
union all select 7, 'path_5' , 4
union all select 8, 'path_6' , 5
; with CteAlias as (
select id, name, parentid
from #t t
where t.parentid = 0
union all
select t.id, parent.name + '\' + t.name, t.parentid
from #t t
inner join CteAlias parent on t.parentid = parent.id
)
select *
from CteAlias
Try something like this:
WITH Recursive AS
(
SELECT
ID,
CAST(PathName AS VARCHAR(500)) AS 'FullPaths',
1 AS 'Level'
FROM
dbo.YourTable
WHERE
ParentID = 0
UNION ALL
SELECT
tbl.ID,
CAST(r.FullPaths + '\' + tbl.PathName AS VARCHAR(500)) AS 'FullPaths',
r.Level + 1 AS 'Level'
FROM
dbo.YourTable tbl
INNER JOIN
Recursive r ON tbl.ParentID = r.ID
)
SELECT * FROM Recursive
ORDER BY Level, ID
Output:
ID FullPaths Level
1 project 1
2 project\structure 2
3 project\structure\path_1 3
4 project\structure\path_2 3
5 project\structure\path_3 3
6 project\structure\path_1\path_4 4
7 project\structure\path_2\path_5 4
8 project\structure\path_3\path_6 4
try this:
DECLARE #YourTable table (id int, nameof varchar(25), parentid int)
INSERT #YourTable VALUES (1,'project',0)
INSERT #YourTable VALUES (2,'structure',1)
INSERT #YourTable VALUES (3,'path_1',2)
INSERT #YourTable VALUES (4,'path_2',2)
INSERT #YourTable VALUES (5,'path_3',2)
INSERT #YourTable VALUES (6,'path_4',3)
INSERT #YourTable VALUES (7,'path_5',4)
INSERT #YourTable VALUES (8,'path_6',5)
;WITH Rec AS
(
SELECT
CONVERT(varchar(max),nameof) as nameof,id
FROM #YourTable
WHERE parentid=0
UNION ALL
SELECT
CONVERT(varchar(max),r.nameof+'\'+y.nameof), y.id
FROM #yourTable y
INNER jOIN Rec r ON y.parentid=r.id
)
select * from rec
output:
nameof
-----------------------------------------------
project
project\structure
project\structure\path_1
project\structure\path_2
project\structure\path_3
project\structure\path_3\path_6
project\structure\path_2\path_5
project\structure\path_1\path_4
(8 row(s) affected)
Something like
;WITH MyCTE AS
(
SELECT
name AS FullPaths, id
FROM
MyTable
WHERE
parentid = 0 /*Normally it'd be IS NULL with an FK linking the 2 columns*/
UNION ALL
SELECT
C.FullPaths + '\' + M.name, M.id
FROM
MyCTE C
JOIN
MyTable M ON M.parentid = C.id
)
SELECT FullPaths FROM MyCTE
You'll have to change the name of #test table I was using.
WITH cte(id, name, parentid) AS
(
SELECT id, convert(varchar(128), name), parentid
FROM #test
WHERE parentid = 0
UNION ALL
SELECT t.id, convert(varchar(128), c.name +'\'+t.name), t.parentid
FROM #test t
INNER JOIN cte c
ON c.id = t.parentid
)
SELECT name as FullPaths
FROM cte
order by id