SQL Building Pathway using With Union in SQL Server - sql

This is SQL Server Question
I have a set of categories, and their relationship results in nested categories.
I want to build a pathway keeping the relationship and build the SEF urls. Here is what I am looking for:
Category table:
ID, Name
1, Root
2, Cat1
3, Cat2
4, Cat1.1
5, Cat1.2
6, Cat2.1
7, Cat2,2
CategoryChild table: ParentCategoryID, ChildCategoryID
1, 2
1, 3
2, 4
2, 5
3, 6
3, 7
It is an unlimited nested structure. Here is what I am doing (I know its wrong but want something like this):
WITH MenuItems
AS (
SELECT
CAST((ItemPath) AS VARCHAR(1000)) AS 'ItemPath',
CategoryID, Category, ChildID
FROM #Mapping
WHERE CategoryID = 1
UNION ALL
SELECT
CAST((items.ItemPath + '-/' + MenuItem.Category) AS VARCHAR(1000)) AS 'ItemPath',
MenuItem.CategoryID, MenuItem.Category, MenuItem.ChildID
FROM #Mapping AS MenuItem
JOIN MenuItems AS items
ON items.ChildID = MenuItem.CategoryID
)
select * from MenuItems
It gives me something like this:
root--------|1---|root---|2
root--------|1---|root---|3
root/Cat2---|3---|Cat2---|6
root/Cat2---|3---|Cat2---|7
root/Cat1---|2---|Cat1---|4
root/Cat1---|2---|Cat1---|5
So ideally the path should be like this:
root/parent/child (and so on)!

I'm not sure if this is what you're looking for but I've played with recursive cte's in the past and so this might be helpful in building the items path.
NOTE: I've included additional information like the Root Id and Level for each item, so that you can change the ordering of the output.
declare #Category table (Id int, Name varchar(10))
insert into #Category values (1, 'Root'),(2, 'Cat1'), (3, 'Cat2'), (4, 'Cat1.1'), (5, 'Cat1.2'), (6, 'Cat2.1'), (7, 'Cat2.2')
declare #CategoryChild table (ParentCategoryID int, ChildCategoryID int)
insert into #CategoryChild values (1, 2), (1, 3), (2, 4), (2, 5), (3, 6), (3, 7)
;with cte as
(
-- root part
select
ccParent.ChildCategoryID Id,
ccParent.ParentCategoryID ParentId,
c.Name Name,
CAST(parentCategory.Name + '/' + c.Name as varchar(1000)) as Path,
ccParent.ChildCategoryID Root,
0 as Level
from
#CategoryChild ccParent
inner join
#Category c on c.Id = ccParent.ChildCategoryID
inner join
#Category parentCategory on parentCategory.Id = ccParent.ParentCategoryID
where
ccParent.ParentCategoryID = 1
union all
-- recursive part
select
ccChild.ChildCategoryID Id,
ccChild.ParentCategoryID ParentId,
c.Name Name,
CAST((cte.Path + '/' + c.Name) as varchar(1000)) as Path,
cte.Root Root,
cte.Level + 1 as Level
from
#CategoryChild ccChild
inner join
#Category c on c.Id = ccChild.ChildCategoryID
inner join
cte on cte.Id = ccChild.ParentCategoryID
)
select cte.Path
from cte
order by cte.Root, cte.Level
Running the above within my environment gives the following results
Root/Cat1
Root/Cat1/Cat1.1
Root/Cat1/Cat1.2
Root/Cat2
Root/Cat2/Cat2.1
Root/Cat2/Cat2.2
If you were looking to include the Root category in your result set as a standalone item then you can change the first part of the cte to hard code the select of the root item.
;with cte as
(
-- root part
select
c.Id Id,
null ParentId,
c.Name Name,
CAST(c.Name as varchar(1000)) as Path,
c.Id Root,
0 as Level
from
#Category c
where
c.Name = 'Root'
union all
... same as before
Giving the follow
Root
Root/Cat1
Root/Cat1/Cat1.1
Root/Cat1/Cat1.2
Root/Cat2
Root/Cat2/Cat2.1
Root/Cat2/Cat2.2

Related

SQL Server CTE: How to select the lowest level only

I want to select/display the lowest level of the CTE only. Please help. I am using SQL Server 2016.
Create Table Location
(
Id int
Name varchar(20)
Parent int
)
Insert into location
values (1, Location1, null), (2, Location1child, 1),
(3, Location1grandchild, 2), (4, Location2, null),
(5, Location3, null), (6, Locationchild3, 5)
I need to display only records 3, 4, 6 which is the lowest level.
Update: I already created the query, but record number 4 didn't display. I am expecting record number 4 to be displayed because the record is the lowest level in the group.
With CTE (id, cte_level, cte_name, cte_longname) as
(
Select
A.ID, 1,
cast(A.name as varchar(max)),
cast(A.name as varchar(max))
from
Location A
Union All
Select
A.ID, cte_level + 1,
replicate(' ยท ' , cte_level ) + cast(A.name as varchar(max)),
cte.cte_longname + ' . ' + cast(A.name as varchar(max))
from
Location A
inner join
CTE ON A.Parent = CTE.id
)
select
CTE_2.id,
CTE_2.cte_longname [name]
--, A.cte_name [name]
from
CTE as CTE_1
inner join
CTE as CTE_2 on CTE_1.id = cte_2.id
where
CTE_1.cte_level = 1
And CTE_2.cte_level = (Select MAX(CTE.cte_level) From CTE)
order by
cte_2.cte_longname
It has nothing to do with CTE. Just use LEFT OUTER JOIN with IS NULL check.
SELECT P.*
FROM Location P
LEFT OUTER JOIN Location C ON P.Id = C.Parent
WHERE C.Id IS NULL;

How to find nth child of a record in sql query

I have a table which stores categories and sub categories (self join)
The table structure is like this:
-CategoryId
-CategoryName
-ParentCategoryId
How to find nth child/childs of a category in sql query
How about something like this
DECLARE #Categories TABLE(
CategoryId INT,
CategoryName VARCHAR(20),
ParentCategoryId INT
)
INSERT INTO #Categories SELECT 1, '1',NULL
INSERT INTO #Categories SELECT 2, '2',NULL
INSERT INTO #Categories SELECT 3, '1.3',1
INSERT INTO #Categories SELECT 4, '1.4',1
INSERT INTO #Categories SELECT 5, '1.3.5',3
INSERT INTO #Categories SELECT 6, '1.3.6',3
INSERT INTO #Categories SELECT 7, '1.3.6.7',6
INSERT INTO #Categories SELECT 8, '1.4.8',4
DECLARE #CatID INT,
#NthLevel INT
SELECT #CatID = 1,
#NthLevel = 2
;WITH Vals AS (
SELECT *,
1 AS CatLevel
FROM #Categories c
WHERE CategoryId IS NULL
UNION ALL
SELECT c.*,
CatLevel + 1 AS CatLevel
FROM Vals v INNER JOIN
#Categories c ON c.ParentCategoryId = v.CategoryID
)
SELECT *
FROM Vals
WHERE CatLevel = #NthLevel
This will recursively build the tree structure, and limit it on the tree level you are looking for.
SELECT ParentCategoryId,
, CategoryId AS [n-th child]
FROM (
SELECT CategoryId
, ParentCategoryId
, ROW_NUMBER() OVER (PARTITION BY ParentCategoryId ORDER BY CategoryId) as ChildNumber
FROM Categories
) p
WHERE ChildNumber = %N
To find the Nth child, ordered by CategoryId, of category M:
select *
from (
select row_number() over (order by c.CategoryId) as rn
, c.*
from Categories p
join Categories c
on p.CategoryId = c.ParentCategoryId
where p.CategoryId = M
)
where rn = N

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

SQL - Ordering by multiple criteria

I have a table of categories. Each category can either be a root level category (parent is NULL), or have a parent which is a root level category. There can't be more than one level of nesting.
I have the following table structure:
Categories Table Structure http://img16.imageshack.us/img16/8569/categoriesi.png
Is there any way I could use a query which produced the following output:
Free Stuff
Hardware
Movies
CatA
CatB
CatC
Software
Apples
CatD
CatE
So the results are ordered by top level category, then after each top level category, subcategories of that category are listed?
It's not really ordering by Parent or Name, but a combo of the two. I'm using SQL Server.
It seems to me like you are looking to flatten and order your hierarchy, the cheapest way to get this ordering would be to store an additional column in the table that has the full path.
So for example:
Name | Full Path
Free Stuff | Free Stuff
aa2 | Free Stuff - aa2
Once you store the full path, you can order on it.
If you only have a depth of one you can auto generate a string to this effect with a single subquery (and order on it), but this solution does not work that easily when it gets deep.
Another option, is to move this all over to a temp table and calculate the full path there, on demand. But it is fairly expensive.
You could make the table look at itself, ordering by the parent Name then the child Name.
select categories.Name AS DisplayName
from categories LEFT OUTER JOIN
categories AS parentTable ON categories.Parent = parentTable.ID
order by parentTable.Name, DisplayName
Ok, here we go :
with foo as
(
select 1 as id, null as parent, 'CatA' as cat from dual
union select 2, null, 'CatB' from dual
union select 3, null, 'CatC' from dual
union select 4, 1, 'SubCatA_1' from dual
union select 5, 1, 'SubCatA_2' from dual
union select 6, 2, 'SubCatB_1' from dual
union select 7, 2, 'SubCatB_2' from dual
)
select child.cat
from foo parent right outer join foo child on parent.id = child.parent
order by case when parent.id is not null then parent.cat else child.cat end,
case when parent.id is not null then 1 else 0 end
Result :
CatA
SubCatA_1
SubCatA_2
CatB
SubCatB_1
SubCatB_2
CatC
Edit - Solution change inspire from van's order by ! Much simpler that way.
Not entirely sure of your questions but it sounds like PARTITION BY might be useful for you. There's a good introductory post on PARTITION BY here.
Here you have a complete working example using a resursive common table expression.
DECLARE #categories TABLE
(
ID INT NOT NULL,
[Name] VARCHAR(50),
Parent INT NULL
);
INSERT INTO #categories VALUES (4, 'Free Stuff', NULL);
INSERT INTO #categories VALUES (1, 'Hardware', NULL);
INSERT INTO #categories VALUES (3, 'Movies', NULL);
INSERT INTO #categories VALUES (2, 'Software', NULL);
INSERT INTO #categories VALUES (10, 'a', 0);
INSERT INTO #categories VALUES (12, 'apples', 2);
INSERT INTO #categories VALUES (8, 'catD', 2);
INSERT INTO #categories VALUES (9, 'catE', 2);
INSERT INTO #categories VALUES (5, 'catA', 3);
INSERT INTO #categories VALUES (6, 'catB', 3);
INSERT INTO #categories VALUES (7, 'catC', 3);
INSERT INTO #categories VALUES (11, 'aa2', 4);
WITH categories(ID, Name, Parent, HierarchicalName)
AS
(
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(c.[Name] AS VARCHAR(200)) AS HierarchicalName
FROM #categories c
WHERE c.Parent IS NULL
UNION ALL
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(pc.HierarchicalName + c.[Name] AS VARCHAR(200))
FROM #categories c
JOIN categories pc ON c.Parent = pc.ID
)
SELECT c.*
FROM categories c
ORDER BY c.HierarchicalName
SELECT
ID,
Name,
Parent,
RIGHT(
'000000000000000' +
CASE WHEN Parent IS NULL
THEN CONVERT(VARCHAR, Id)
ELSE CONVERT(VARCHAR, Parent)
END, 15
)
+ '_' + CASE WHEN Parent IS NULL THEN '0' ELSE '1' END
+ '_' + Name
FROM
categories
ORDER BY
4
The long padding is to account for the fact that SQL Server's INT data type goes from 2,147,483,648 through 2,147,483,647.
You can ORDER BY the expression directly, no need to use ORDER BY 4. It was just to show what it is sorting on.
It is worth noting that this expression cannot use any index. This means sorting a large table will be slow.