Recursive SQL with 1:n relation to another table - sql

I have a problem that is giving me a headache:
We work with T-SQL (MS SQL-Server).
I have a ragged parent/child hierachy in one table. Each row in the table with the parent/child relation (T1) has multiple values in another table (T2).
My goal is to get the values from table T2 for each row of table T1, inluding those of it's ancenstors.
Here is an example:
T1 has the ragged parent child hierarchy.
ClassID | ParentclassID
____________________|___________________________
1 | NULL
--------------------|---------------------------
2 | 1
--------------------|---------------------------
3 | 2
-------------------|---------------------------
4 | 1
T2 has multiple values for each of the values from table T1
ClassID | FeatureID
____________________|___________________________
1 | A
--------------------|---------------------------
1 | B
--------------------|---------------------------
2 | C
--------------------|---------------------------
2 | D
--------------------|---------------------------
3 | E
--------------------|---------------------------
4 | F
My goal is the following Output:
ClassID | FeatureID
____________________|___________________________
1 | A
--------------------|---------------------------
1 | B
--------------------|---------------------------
2 | A
--------------------|---------------------------
2 | B
--------------------|---------------------------
2 | C
--------------------|---------------------------
2 | D
--------------------|---------------------------
3 | A
--------------------|---------------------------
3 | B
--------------------|---------------------------
3 | C
--------------------|---------------------------
3 | D
--------------------|---------------------------
3 | E
--------------------|---------------------------
4 | A
--------------------|---------------------------
4 | B
--------------------|---------------------------
4 | F
If it would only be the ragged hierarchy, I could solve this with an recursive cte. But it is the 1:n relation to the table T2 that is causing the problems.
Any suggestions will be highly appreciated.

Join on tree
declare #H table (id int primary key, par int);
insert into #H values
(1, NULL)
, (2, 1)
, (3, 2)
, (4, 1);
DECLARE #Feature AS TABLE
(
ClassID int,
FeatureID char(1)
)
INSERT INTO #Feature (ClassID, FeatureID) VALUES
(1, 'A'), (1, 'B'),
(2, 'C'), (2, 'D'),
(3, 'E'),
(4, 'F'),
(5, 'G');
with cte as
( select h.id, h.par, h.id as tree
from #H h
union all
select cte.id, cte.par, h.par
from cte
join #H H
on cte.tree = h.id
)
select * from cte
join #Feature f
on f.ClassID = cte.tree
where cte.tree is not null
order by cte.id, cte.par, cte.tree

Related

Roll up multiple rows into one when joining in SQL Server

I have a table, Foo
ID | Name
-----------
1 | ONE
2 | TWO
3 | THREE
And another, Bar:
ID | FooID | Value
------------------
1 | 1 | Alpha
2 | 1 | Alpha
3 | 1 | Alpha
4 | 2 | Beta
5 | 2 | Gamma
6 | 2 | Beta
7 | 3 | Delta
8 | 3 | Delta
9 | 3 | Delta
I would like a query that joins these tables, returning one row for each row in Foo, rolling up the 'value' column from Bar. I can get back the first Bar.Value for each FooID:
SELECT * FROM Foo f OUTER APPLY
(
SELECT TOP 1 Value FROM Bar WHERE FooId = f.ID
) AS b
Giving:
ID | Name | Value
---------------------
1 | ONE | Alpha
2 | TWO | Beta
3 | THREE | Delta
But that's not what I want, and I haven't been able to find a variant that will bring back a rolled up value, that is the single Bar.Value if it is the same for each corresponding Foo, or a static string something like '(multiple)' if not:
ID | Name | Value
---------------------
1 | ONE | Alpha
2 | TWO | (multiple)
3 | THREE | Delta
I have found some solutions that would bring back concatenated values (albeit not very elegant) 'Alpha' Alpha, Alpha', 'Beta, Gamma, Beta' &c, but that's not what I want either.
One method, using a a CASE expression and assuming that [Value] cannot have a value of NULL:
WITH Foo AS
(SELECT *
FROM (VALUES (1, 'ONE'),
(2, 'TWO'),
(3, 'THREE')) V (ID, [Name])),
Bar AS
(SELECT *
FROM (VALUES (1, 1, 'Alpha'),
(2, 1, 'Alpha'),
(3, 1, 'Alpha'),
(4, 2, 'Beta'),
(5, 2, 'Gamma'),
(6, 2, 'Beta'),
(7, 3, 'Delta'),
(8, 3, 'Delta'),
(9, 3, 'Delta')) V (ID, FooID, [Value]))
SELECT F.ID,
F.[Name],
CASE COUNT(DISTINCT B.[Value]) WHEN 1 THEN MAX(B.Value) ELSE '(Multiple)' END AS [Value]
FROM Foo F
JOIN Bar B ON F.ID = B.FooID
GROUP BY F.ID,
F.[Name];
You can also try below:
SELECT F.ID, F.Name, (case when B.Value like '%,%' then '(Multiple)' else B.Value end) as Value
FROM Foo F
outer apply
(
select SUBSTRING((
SELECT distinct ', '+ isnull(Value,',') FROM Bar WHERE FooId = F.ID
FOR XML PATH('')
), 2 , 9999) as Value
) as B

SQL aggregates over 3 tables

Well, this is annoying the hell out of me. Any help would be much appreciated.
I'm trying to get a count of how many project Ids and Steps there are. The relationships are:
Projects (n-1) Pages
Pages (n-1) Status Steps
Sample Project Data
id name
1 est et
2 quia nihil
Sample Pages Data
id project_id workflow_step_id
1 1 1
2 1 1
3 1 2
4 1 1
5 2 3
6 2 3
7 2 4
Sample Steps Data
id name
1 a
2 b
3 c
4 d
Expected Output
project_id name count_steps
1 a 3
1 b 1
2 c 2
2 d 1
Thanks!
An approach to meet the expected result. See it also at SQL Fiddle
CREATE TABLE Pages
("id" int, "project_id" int, "workflow_step_id" int)
;
INSERT INTO Pages
("id", "project_id", "workflow_step_id")
VALUES
(1, 1, 1),
(2, 1, 1),
(3, 1, 2),
(4, 1, 1),
(5, 2, 3),
(6, 2, 3),
(7, 2, 4)
;
CREATE TABLE workflow_steps
("id" int, "name" varchar(1))
;
INSERT INTO workflow_steps
("id", "name")
VALUES
(1, 'a'),
(2, 'b'),
(3, 'c'),
(4, 'd')
;
CREATE TABLE Projects
("id" int, "name" varchar(10))
;
INSERT INTO Projects
("id", "name")
VALUES
(1, 'est et'),
(2, 'quia nihil')
;
Query 1:
select pg.project_id, s.name, pg.workflow_step_id, ws.count_steps
from (
select distinct project_id, workflow_step_id
from pages ) pg
inner join (
select workflow_step_id, count(*) count_steps
from pages
group by workflow_step_id
) ws on pg.workflow_step_id = ws.workflow_step_id
inner join workflow_steps s on pg.workflow_step_id = s.id
order by project_id, name, workflow_step_id
Results:
| project_id | name | workflow_step_id | count_steps |
|------------|------|------------------|-------------|
| 1 | a | 1 | 3 |
| 1 | b | 2 | 1 |
| 2 | c | 3 | 2 |
| 2 | d | 4 | 1 |

Dynamic table created from CTE (parent/child)

If I have a very simple table called tree
create table if not exists tree (id int primary key, parent int, name text);
And a few rows of data
insert into tree values (1, null, 'A');
insert into tree values (2, 1, 'B');
insert into tree values (3, 1, 'C');
insert into tree values (4, 2, 'D');
insert into tree values (5, 2, 'E');
insert into tree values (6, 3, 'F');
insert into tree values (7, 3, 'G');
I can easily run CTEs on it, and produce an output giving me path like this
with recursive R(id, level, path, name) as (
select id,1,name,name from tree where parent is null
union select tree.id, level + 1, path || '.' || tree.name, tree.name from tree join R on R.id=tree.parent
) select level,path,name from R;
Which gives the output
level | path | name
-------+-------+------
1 | A | A
2 | A.B | B
2 | A.C | C
3 | A.B.D | D
3 | A.B.E | E
3 | A.C.F | F
3 | A.C.G | G
What I'm wondering, is it possible to somehow project this output into another table, dynamically creating columns based on level (level1, level2, level3 etc), giving me something like this in return
id | level1 | level2 | level3
---+--------+--------+-------
1 | A | |
2 | A | B |
3 | A | C |
4 | A | B | D
5 | A | B | E
6 | A | C | F
7 | A | C | G
Any help would be appreciated.
If you know the maximum depth of your tree, I'd keep your approach and simplify it using array concatenation to produce the desired output.
So for a 5 level tree, that would look like this :
WITH RECURSIVE R(id, path) AS (
SELECT id, ARRAY[name::text] FROM tree WHERE parent IS NULL
UNION SELECT tree.id, path || tree.name FROM tree JOIN R ON R.id=tree.parent
)
SELECT id,
path[1] AS l1,
path[2] AS l2,
path[3] AS l3,
path[4] AS l4,
path[5] AS l5
FROM R;
PS : sorry for not commenting on Ziggy's answer which is very close, but I don't have enough reputation to do so. I don't see why you would need a windowing function here ?
PostgreSQL requires to always define the type of the output, so you can't have the columns levelX produced dynamically. However, you can do the following:
with recursive
R(id, path) as (
select id,ARRAY[name::text] from tree where parent is null
union
select tree.id, path || tree.name::text from tree join R on R.id=tree.parent
)
select row_number() over (order by cardinality(path), path), id,
path[1] as level1, path[2] as level2, path[3] as level3
from R
order by 1
In the example above, the column row_number happens to match id, but probably that wouldn't happen with your real data.

Count Based on Columns in SQL Server

I have 3 tables:
SELECT id, letter
FROM As
+--------+--------+
| id | letter |
+--------+--------+
| 1 | A |
| 2 | B |
+--------+--------+
SELECT id, letter
FROM Xs
+--------+------------+
| id | letter |
+--------+------------+
| 1 | X |
| 2 | Y |
| 3 | Z |
+--------+------------+
SELECT id, As_id, Xs_id
FROM A_X
+--------+-------+-------+
| id | As_id | Xs_id |
+--------+-------+-------+
| 9 | 1 | 1 |
| 10 | 1 | 2 |
| 11 | 2 | 3 |
| 12 | 1 | 2 |
| 13 | 2 | 3 |
| 14 | 1 | 1 |
+--------+-------+-------+
I can count all As and Bs with group by. But I want to count As and Bs based on X,Y and Z. What I want to get is below:
+-------+
| X,Y,Z |
+-------+
| 2,2,0 |
| 0,0,2 |
+-------+
X,Y,Z
A 2,2,0
B 0,0,2
What is the best way to do this at MSSQL? Is it an efficent way to use foreach for example?
edit: It is not a duplicate because I just wanted to know the efficent way not any way.
For what you're trying to do without knowing what is inefficient with your current code (because none was provided), a Pivot is best. There are a million resources online and here in the stack overflow Q/A forums to find what you need. This is probably the simplest explanation of a Pivot which I frequently need to remind myself of the complicated syntax of a pivot.
To specifically answer your question, this is the code that shows how the link above applies to your question
First Tables needed to be created
DECLARE #AS AS TABLE (ID INT, LETTER VARCHAR(1))
DECLARE #XS AS TABLE (ID INT, LETTER VARCHAR(1))
DECLARE #XA AS TABLE (ID INT, AsID INT, XsID INT)
Values were added to the tables
INSERT INTO #AS (ID, Letter)
SELECT 1,'A'
UNION
SELECT 2,'B'
INSERT INTO #XS (ID, Letter)
SELECT 1,'X'
UNION
SELECT 2,'Y'
UNION
SELECT 3,'Z'
INSERT INTO #XA (ID, ASID, XSID)
SELECT 9,1,1
UNION
SELECT 10,1,2
UNION
SELECT 11,2,3
UNION
SELECT 12,1,2
UNION
SELECT 13,2,3
UNION
SELECT 14,1,1
Then the query which does the pivot is constructed:
SELECT LetterA, [X],[Y],[Z]
FROM (SELECT A.LETTER AS LetterA
,B.LETTER AS LetterX
,C.ID
FROM #XA C
JOIN #AS A
ON A.ID = C.ASID
JOIN #XS B
ON B.ID = C.XSID
) Src
PIVOT (COUNT(ID)
FOR LetterX IN ([X],[Y],[Z])
) AS PVT
When executed, your results are as follows:
Letter X Y Z
A 2 2 0
B 0 0 2
As i said in comment ... just join and do simple pivot
if object_id('tempdb..#AAs') is not null drop table #AAs
create table #AAs(id int, letter nvarchar(5))
if object_id('tempdb..#XXs') is not null drop table #XXs
create table #XXs(id int, letter nvarchar(5))
if object_id('tempdb..#A_X') is not null drop table #A_X
create table #A_X(id int, AAs int, XXs int)
insert into #AAs (id, letter) values (1, 'A'), (2, 'B')
insert into #XXs (id, letter) values (1, 'X'), (2, 'Y'), (3, 'Z')
insert into #A_X (id, AAs, XXs)
values (9, 1, 1),
(10, 1, 2),
(11, 2, 3),
(12, 1, 2),
(13, 2, 3),
(14, 1, 1)
select LetterA,
ISNULL([X], 0) [X],
ISNULL([Y], 0) [Y],
ISNULL([Z], 0) [Z]
from (
select distinct a.letter [LetterA], x.letter [LetterX],
count(*) over (partition by a.letter, x.letter order by a.letter) [Counted]
from #A_X ax
join #AAs A on ax.AAs = A.ID
join #XXs X on ax.XXs = X.ID
)src
PIVOT
(
MAX ([Counted]) for LetterX in ([X], [Y], [Z])
) piv
You get result as you asked for
LetterA X Y Z
A 2 2 0
B 0 0 2

(CTE) Recursive SQL Query

I'm confused about some problem that related with recursive query.
I'm using SQL SERVER 2012
My scnerio,
locations are defined hierarchical,
Each locations has their own asset.
My Locations table like;
Id | Name | ParentLocationId
----+------+-----------------
1 | L1 | NULL
2 | L2 | 1
3 | L3 | 1
4 | L4 | 1
5 | L5 | 1
6 | L6 | 4
7 | L7 | 4
8 | L8 | 4
9 | L9 | 2
10 | L10 | 2
11 | L11 | 6
12 | L12 | 6
13 | L13 | 6
My Asset table like;
Id | AssetNo | Description | CurrentLocationId
-------+---------+-------------+------------------
1 | AN001 | ADesc | 1
2 | AN002 | BDesc | 1
L1 has 1, L2 has 2, L3 has 0, L4 has 3, L5 has 5, L6 has 5, L7 has 1,
L8 has 0, L9 has 3, L10 has 2, L11 has 5, L12 has 3, L13 has 6 Assets
My question is, how can I take the total number of selected Location's
1 level down asset count?
For Example; Selected LocationId = 1 (L1)
Sample Output is;
Id | Name | Qty
-------+------+-----
2 | L2 | 7
3 | L3 | 0
4 | L4 | 23
5 | L5 | 5
Another Example; Selected LocationId = 4 (L4)
Id | Name | Qty
---+------+-----
6 | L6 | 19
7 | L7 | 1
8 | L8 | 0
I try wrote a query,
WITH recursiveTable
AS (SELECT *
FROM location l
WHERE ParentLocationId = 1
UNION ALL
SELECT l.*
FROM location l
INNER JOIN recursiveTable r
ON r.Id = l.ParentLocationId),
allLocations
AS (SELECT *
FROM recursiveTable
UNION
SELECT *
FROM Location
WHERE Id = 0),
resultset
AS (SELECT r.NAME AS LocationName,
a.*
FROM allLocations r
INNER JOIN Asset a ON a.CurrentLocationId = r.Id
WHERE r.DataStatus = 1)
select CurrentLocationId
,min(LocationName) as LocationName
,count(Id) as NumberOfAsset
from resultset
group by CurrentLocationId
Additional;
Create Table Location
(
Id int,
Name nvarchar(100),
Description nvarchar(250),
ParentLocationId int,
DataStatus int
)
Create Table Asset
(
Id int,
AssetNo nvarchar(50),
Description nvarchar(250),
CurrentLocationId int,
DataStatus int
)
Insert Into Location Values(1,'L1','LDesc1',NULL,1)
Insert Into Location Values(2,'L2','LDesc2',1,1)
Insert Into Location Values(3,'L3','LDesc3',1,1)
Insert Into Location Values(4,'L4','LDesc4',1,1)
Insert Into Location Values(5,'L5','LDesc5',1,1)
Insert Into Location Values(6,'L6','LDesc6',4,1)
Insert Into Location Values(7,'L7','LDesc7',4,1)
Insert Into Location Values(8,'L8','LDesc8',4,1)
Insert Into Location Values(9,'L9','LDesc9',2,1)
Insert Into Location Values(10,'L10','LDesc10',2,1)
Insert Into Location Values(11,'L11','LDesc11',6,1)
Insert Into Location Values(12,'L12','LDesc12',6,1)
Insert Into Location Values(13,'L13','LDesc13',6,1)
Insert Into Asset Values (1,'FDB-001','Desc1',1,1)
Insert Into Asset Values (2,'FDB-002','Desc2',2,1)
Insert Into Asset Values (3,'FDB-003','Desc3',2,1)
Insert Into Asset Values (4,'FDB-004','Desc4',4,1)
Insert Into Asset Values (5,'FDB-005','Desc5',4,1)
Insert Into Asset Values (6,'FDB-006','Desc6',4,1)
Insert Into Asset Values (7,'FDB-007','Desc7',5,1)
Insert Into Asset Values (8,'FDB-008','Desc8',5,1)
Insert Into Asset Values (9,'FDB-009','Desc9',5,1)
Insert Into Asset Values (10,'FDB-010','Desc10',5,1)
Insert Into Asset Values (11,'FDB-011','Desc11',5,1)
Insert Into Asset Values (12,'FDB-012','Desc12',6,1)
Insert Into Asset Values (13,'FDB-013','Desc13',6,1)
Insert Into Asset Values (14,'FDB-014','Desc14',6,1)
Insert Into Asset Values (15,'FDB-015','Desc15',6,1)
Insert Into Asset Values (16,'FDB-016','Desc16',6,1)
Insert Into Asset Values (17,'FDB-017','Desc17',7,1)
Insert Into Asset Values (18,'FDB-018','Desc18',9,1)
Insert Into Asset Values (19,'FDB-019','Desc19',9,1)
Insert Into Asset Values (20,'FDB-020','Desc20',9,1)
Insert Into Asset Values (21,'FDB-021','Desc21',10,1)
Insert Into Asset Values (22,'FDB-022','Desc22',10,1)
Insert Into Asset Values (23,'FDB-023','Desc23',11,1)
Insert Into Asset Values (24,'FDB-024','Desc24',11,1)
Insert Into Asset Values (25,'FDB-025','Desc25',11,1)
Insert Into Asset Values (26,'FDB-026','Desc26',11,1)
Insert Into Asset Values (27,'FDB-027','Desc27',11,1)
Insert Into Asset Values (28,'FDB-028','Desc28',12,1)
Insert Into Asset Values (29,'FDB-029','Desc29',12,1)
Insert Into Asset Values (30,'FDB-030','Desc30',12,1)
Insert Into Asset Values (31,'FDB-031','Desc31',13,1)
Insert Into Asset Values (32,'FDB-032','Desc32',13,1)
Insert Into Asset Values (33,'FDB-033','Desc33',13,1)
Insert Into Asset Values (34,'FDB-034','Desc34',13,1)
Insert Into Asset Values (35,'FDB-035','Desc35',13,1)
Insert Into Asset Values (36,'FDB-036','Desc36',13,1)
Best Regards,
We can apply a Level and a Path to try and get your child counts, but only display the first level of children. We end up grouping the count of assets by the path, which is the ID of the first level of children. Then select only the first Level at the end
DECLARE #LocationID INT = 1;
WITH recursiveCTE AS
(
SELECT
*,
1 AS [Level],
Id [Path]
FROM
location l
WHERE
l.ParentLocationId = #LocationID
UNION ALL
SELECT
l.*,
[Level] + 1,
[Path]
FROM
location l
JOIN recursiveCTE r ON l.ParentLocationId = r.Id
),
countCte AS (
SELECT
[Path] Id,
COUNT(a.AssetNo) Qty
FROM recursiveCTE c
JOIN Asset a ON c.Id = a.CurrentLocationId
GROUP BY [Path]
)
SELECT r.Id,
r.[Name],
COALESCE(c.Qty,0) Qty
FROM recursiveCTE r
LEFT JOIN countCte c ON r.Id = c.Id
WHERE r.[Level] = 1;