recursive sql, sql server 2012 - sql

I'm trying to solve a recursive SQL problem essentially aggregating the status of a group of records.
For the purposes of the question - there are two tables. One that maintains the aggregation/hierarchy "GROUP_MEMBERS" and one that contains the individual items "ITEMS".
"GROUP_MEMBERS" looks similar (ID being GROUPID, CHILDTYPE being 0 for a group, 1 for individual item, and ID being the child items ID (so groupid for type 0, itemid for type 1)
ID | CHILDTYPE | CHILDID
1 0 2
1 1 1
2 1 2
2 1 3
2 1 4
In this example, my "ITEMS" table would have only two columns:
ID | STATUS
1 0
2 1
3 0
4 0
5 0
Effectively what I am trying to do is pull back all "ITEMS", ID and STATUS, under a group recursively (because groups can contain other groups). So for the example data I provided, if I passed it the GROUPID of 1, it would return ITEMS 1-4 with their statuses; GROUPID 2 would be ITEMS 2-4 with their statuses, etc.
I'm assuming I need to do this via a function and return a table, but I'm not even sure where to begin.

This was a nice puzzle, gotta tell ya :)
Hopefully it does you want it to do.
DECLARE #GroupRootID INT = 1
DECLARE #GROUP_MEMBERS TABLE (ID int, CHILDTYPE int, CHILDID int)
DECLARE #ITEMS TABLE (ID int, STATUS int)
INSERT INTO #GROUP_MEMBERS VALUES
(1,0,2), (1,1,1), (2,1,2), (2,0,3), (2,0,4), (2,1,3), (2,1,4), (3,1,5), (4,1,5)
INSERT INTO #ITEMS VALUES
(1,0), (2,1), (3,0), (4,0), (5,1)
-- 1
-- / \
-- 2 items items: 1 => 1,1,1
-- / | \
-- 3 4 items items: 2,3,4 => 2,1,2 - 2,1,3 - 2,1,4
-- | \
-- items items items (3): 5 => 3,1,5
-- items (4); 5 => 4,1,5
/* Recursivly build the GROUP tree (groups that have subgroups, CHILDTYPE=0), but NOT the lead nodes (CHILDTYPE = 1) */
;WITH GROUP_TREE
AS
(
/* SELECT all parents */
SELECT ParentGroups.*, 0 AS LEVEL
FROM #GROUP_MEMBERS AS ParentGroups
WHERE ParentGroups.CHILDTYPE = 0
AND ParentGroups.ID = #GroupRootID
UNION ALL
/* SELECT all childs groups for the parents */
SELECT ChildGroups.*, LEVEL + 1
FROM #GROUP_MEMBERS AS ChildGroups
INNER JOIN GROUP_TREE AS Parent ON Parent.CHILDID = ChildGroups.ID
WHERE ChildGroups.CHILDTYPE = 0
)
/* We now have all groups with their subgroups (not leaf nodes) */
/* Then join the leaf nodes (groups that are no subtree) */
/* Finally union the items from the root node and join the ITEMS to the leaf nodes to get the status */
/* Mind you though that ITEM 5 is linked double and will be returned NON-distinct */
SELECT ITEMS.*
FROM (
SELECT GROUPS.*
FROM #GROUP_MEMBERS AS GROUPS
INNER JOIN GROUP_TREE ON GROUP_TREE.CHILDID = GROUPS.ID
WHERE GROUPS.CHILDTYPE = 1
UNION ALL
SELECT GROUPS.*
FROM #GROUP_MEMBERS AS GROUPS
WHERE GROUPS.CHILDTYPE = 1
AND GROUPS.ID = #GroupRootID
) AS GROUP_ITEMS
INNER JOIN #ITEMS AS ITEMS ON GROUP_ITEMS.CHILDID = ITEMS.ID

Related

LAG All Previous Rows - Parent Child Database Find All Total Children Count From Each Parent

I'm trying to obtain all children count from each parent (where subchildren counts).
This is the sample database (MSSQL).
INSERT INTO NODES VALUES(NULL, 1);
INSERT INTO NODES VALUES(NULL, 2);
INSERT INTO NODES VALUES(2, 3);
INSERT INTO NODES VALUES(1, 4);
INSERT INTO NODES VALUES(1, 5);
INSERT INTO NODES VALUES(3, 6);
INSERT INTO NODES VALUES(NULL, 7);
INSERT INTO NODES VALUES(NULL, 8);
INSERT INTO NODES VALUES(8, 9);
INSERT INTO NODES VALUES(7, 10);
INSERT INTO NODES VALUES(9, 11);
INSERT INTO NODES VALUES(11, 12);
INSERT INTO NODES VALUES(10, 13);
INSERT INTO NODES VALUES(10, 14);
INSERT INTO NODES VALUES(4, 15);
Where the hierarchy is;
- 1
- 4
- 15
- 5
- 2
- 3
- 6
- 7
- 10
- 13
- 14
- 8
- 9
- 11
- 12
And the desired result is:
id
Children Count
1
3
4
1
5
0
2
2
3
1
6
0
7
3
10
2
13
1
14
0
8
3
9
2
11
1
12
0
Every time I make a strategy to formulate a query, I get to the point where I must iterate the table formed by the query at running time.
If I go deep out grouping the results (with the parentid) I could generate a count of only the children (not the subchildren), so that we can add up to the root. But clearly I would need to iterate the table that I have been forming in the query, I don't know if it would be correct to say recursively.
To express myself better I will show it with the part I have reached and what I want to do;
WITH tree AS
(
SELECT n1.parentid, n1.id, 1 AS level
FROM NODES AS n1
WHERE n1.parentid IS NULL
UNION ALL
SELECT n2.parentid, n2.id, level + 1 AS level
FROM NODES AS n2
INNER JOIN tree ON n2.parentid = tree.id
), levels AS
(
SELECT *
FROM tree
)
SELECT parentid, id, (COUNT(*) OVER(PARTITION BY parentid ORDER BY parentid)) AS childrencountofparentid,
ROW_NUMBER() OVER(ORDER BY parentid DESC) AS rownumber
FROM levels
Where the output is:
parentid
id
childrencountofparentid
rownumber
11
12
1
1
10
13
2
2
10
14
2
3
9
11
1
4
8
9
1
5
7
10
1
6
4
15
1
7
3
6
1
8
2
3
1
9
1
4
2
10
1
5
2
11
null
1
4
12
null
2
4
13
null
7
4
14
null
8
4
15
I want to do this:
Full Image
I want to use the results of the previous rows, similar to lag but i must iterate all previous rows.
I'll solve this with the hierarchyid datatype. First, a recursive CTE to calculate that value for each row in your dataset.
with cte as (
select ID, ParentID,
h = cast(concat('/', ID, '/') as varchar(100))
from NODES
where ParentID is null
union all
select child.ID, child.ParentID,
h = cast(concat(Parent.h, child.ID, '/') as varchar(100))
from NODES as child
join cte as Parent
on child.ParentID = Parent.ID
)
select ID, ParentID,
h = cast(h as hierarchyid)
into #t
from cte;
The only "tricky" thing here is I'm building a string as I'm traversing the hierarchy that can be converted to the hierarchyid datatype.
From there, it's fairly easy to count children with what amounts to a self-join.
select ID, h.ToString(), count(child.a)
from #t as parent
outer apply (
select a = 1
from #t as child
where child.h.IsDescendantOf(parent.h) = 1
and child.ID <> Parent.ID
) as child
group by parent.ID, h
order by h;
Note - I'm only including the h column in the result set to a) show the path back to parent and b) to provide ordering. If you need neither of those, it's not necessary to include. One note - in your desired results, you have 13 as having a child count of 1. I don't see anywhere in the data (or in your helpfully provided visualization of the hierarchy) that 13 has any children. I do see that it has a sibling in ID 14 - if that needs to be counted, the approach needs to change slightly.
One other thing to note here - if you can maintain the hierarchyid column in the source data (which isn't hard to do, by the way), there's no need for a recursive CTE at all. I like a mix of the two approaches. Which is to say keep the notion of ParentID as a column but use the hierarchyid in calculations. If the hierarchyid gets "corrupted" (which is possible given that it's really a cached/computed value that's derived from base data and application logic), it can be recalculated from the ID/ParentID data.
Here is solution that works if you don't care for id order. Unfortunately, I don't know how to make it ordered as you did in desired output. If nothing I hope it helps you come to solution as data is good.
WITH cte as (
SELECT p.Id as 'PID', c.Id as 'CID' FROM nodes p left join nodes c on p.id = c.ParentId
UNION ALL
SELECT c.PID,p.ID FROM cte c JOIN nodes p ON c.CID=p.ParentId
)
SELECT PID as id, count(*) as 'Children Count' FROM cte where CID IS NOT NULL GROUP BY PID
UNION ALL
SELECT PID, 0 FROM cte WHERE CID IS NULL GROUP BY PID
ORDER BY PID ASC

In SQL, Select all FKs from one-to-many table where table data for FK Exists in each of several lists/joins

My database has recipes with associated data (within a range of categories, each recipe has [0 - many] options selected for each category). To search the recipes, a user can select [0 - many] options from [0 - many] categories.
I'm trying to construct a stored procedure that returns all RecipeIDs that match at least one OptionID for each Category where the user selected at least one OptionID.
So - if you want to find all main dishes and desserts with fruit, the proc needs to return all RecipeIDs where:
in RecipeData, for all entries with the same RecipeID (for all Options for a single Recipe)
at least one OptionID is for 'Main Dish' OR at least one OptionID is for 'Dessert'
AND at least one OptionID is for 'Fruit'
AND ignore 'Ranking' (user didn't select any Options in this Category)
The number of Categories is finite and limited (currently 12). Right now, I have the user's search query supplied as 12 table variables - one for each Category, listing the selected OptionIDs for that Category. I'd prefer to submit the user's search query to the proc as a single table, but I'm not sure if that would be possible. Regardless, that's a lower priority.
It must be possible to construct a query to return what I'm looking for, but I have no idea how to do this. Everything I can think of involves looping through groups (RecipeData for each Recipe, Options for each Category), and from what I know, SQL isn't built to do this.
Can I do this in SQL, or will I have to do this in my C# code? If I can do this in SQL - how?
Parameters:
DECLARE #MealTypeOptionID TABLE ( OptionID INT )
DECLARE #IngredientOptionID TABLE ( OptionID INT )
DECLARE #RankingOptionID TABLE ( OptionID INT )
-- all 'Main Dish' or 'Dessert' recipes that have 'Fruit'
INSERT INTO #MealTypeOptionID (OptionID) VALUES (1), (2)
INSERT INTO #IngredientOptionID (OptionID) VALUES (4)
Tables:
Recipe
---------------------------------------------------------------
RecipeID RecipeName
---------------------------------------------------------------
1 'Apple Pie'
2 'Blueberry Ice Cream'
3 'Brownies'
4 'Tuna Casserole'
5 'Pork with Apples'
6 'Fruit Salad'
Category
---------------------------------------------------------------
CategoryID CategoryName
---------------------------------------------------------------
1 'Meal Type'
2 'Ingredients'
3 'Ranking'
Option
---------------------------------------------------------------
OptionID CategoryID OptionName
---------------------------------------------------------------
1 1 'Main Dish'
2 1 'Dessert'
3 1 'Side Dish'
4 2 'Fruit'
5 2 'Meat'
6 3 'Meh'
7 3 'Great'
RecipeData
---------------------------------------------------------------
RecipeDataID RecipeID OptionID
---------------------------------------------------------------
1 1 2
2 1 4
3 1 7
4 2 2
5 2 4
6 3 2
7 4 1
8 4 5
9 4 6
10 5 1
11 5 4
12 5 5
13 6 3
14 6 4
My solution:
-- #optionsToInclude is a parameter of the proc
DECLARE #optionsToInclude TABLE (CategoryID INT, OptionID INT)
-- result table
DECLARE #recipeIDs TABLE (RecipeID INT)
-- get CategoryID FOR first select
DECLARE #categoryID INT
SELECT TOP 1 #categoryID = CategoryID FROM #optionsToInclude GROUP BY CategoryID
-- insert into result table all RecipeIDs that contain any OptionIDs within CategoryID
INSERT INTO #recipeIDs (RecipeID)
SELECT DISTINCT d.RecipeID
FROM RecipeData d
INNER JOIN #optionsToInclude c
ON c.CategoryID = #categoryID
AND c.OptionID = d.OptionID
-- delete from #optionsToInclude all entries where CategoryID = #categoryID
DELETE FROM #optionsToInclude WHERE CategoryID = #categoryID
-- check if any more Categories exist to loop through
DECLARE #exists BIT = 1
IF (NOT EXISTS (SELECT * FROM #optionsToInclude))
SET #exists = 0
WHILE #exists = 1
BEGIN
-- get CategoryID for select
SELECT TOP 1 #categoryID = CategoryID FROM #optionsToInclude GROUP BY CategoryID
-- delete from result table all RecipeIDs that do not contain any OptionIDs within CategoryID
DELETE FROM #recipeIDs
WHERE RecipeID NOT IN
(
SELECT DISTINCT d.RecipeID
FROM dbo.RecipeData d
INNER JOIN #optionsToInclude i
ON i.CategoryID = #categoryID
AND i.OptionID = d.OptionID
)
-- delete from #optionsToInclude all entries where CategoryID = #categoryID
DELETE FROM #optionsToInclude WHERE CategoryID = #categoryID
-- check if any more Categories exist to loop through
IF (NOT EXISTS (SELECT * FROM #optionsToInclude))
SET #exists = 0
END

What is the query to get parent records against specific child or get child records against parent?

Scenario:
I have data in the following hierarchy format in my table:
PERSON_ID Name PARENT_ID
1 Azeem 1
2 Farooq 2
3 Ahsan 3
4 Waqas 1
5 Adnan 1
6 Talha 2
7 Sami 2
8 Arshad 2
9 Hassan 8
E.g
Hassan is child of parent_id 8 which is (Arshad)
and Arshad is child of parent_id 2 which is (Farooq)
What I want:
First of all, I want to find all parent of parent of specific parent_id.
For Example: If I want to find the parent of Hassan then I also get the Parent of Hassan and also get its parent (Hassan -> Arshad -> Farooq)
Second, I want to find all child of Farooq like (Farooq -> Arshad -> Hassan)
Third, If Azeem is also have same parent like (Azeem -> Azeem) then show me this record.
What I've tried yet:
DECLARE #id INT
SET #id = 9
;WITH T AS (
SELECT p.PERSON_ID,p.Name, p.PARENT_ID
FROM hierarchy p
WHERE p.PERSON_ID = #id AND p.PERSON_ID != p.PARENT_ID
UNION ALL
SELECT c.PERSON_ID,c.Name, c.PARENT_ID
FROM hierarchy c
JOIN T h ON h.PARENT_ID = c.PERSON_ID)
SELECT h.PERSON_ID,h.Name FROM T h
and Its shows me below error:
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
If I understand your question correctly that you don't want to insert null values in Parent_ID column then you should replace NULL with 0 and your updated code will be like:
;WITH DATA AS (
SELECT p.PERSON_ID,p.Name, p.PARENT_ID
FROM hierarchy p
WHERE p.PERSON_ID = 9
UNION ALL
SELECT c.PERSON_ID,c.Name, c.PARENT_ID
FROM hierarchy c
JOIN DATA h
ON c.PERSON_ID = h.PARENT_ID
)
select * from DATA;
You have an infinite loop in your data: Azeem is his own parent. You need to either make his value NULL or change your condition to WHERE p.parent_id = #id AND p.parent_id != p.child_id.
Also, I feel you have your columns named the wrong way around - the primary-key should be named person_id instead of parent_id and your column named child_id actually points to that person's parent, so it should be named parent_id instead.
Well I found a way for my above case which is:
If I have below table structure:
PERSON_ID Name PARENT_ID
1 Azeem NULL
2 Farooq NULL
3 Ahsan NULL
4 Waqas 1
5 Adnan 1
6 Talha 2
7 Sami 2
8 Arshad 2
9 Hassan 8
Then I tried below query which working fine in case when Parent_ID have NULL values means there is no more parent of that record.
DECLARE #id INT
SET #id = 2
Declare #Table table(
PERSON_ID bigint,
Name varchar(50),
PARENT_ID bigint
);
;WITH T AS (
SELECT p.PERSON_ID,p.Name, p.PARENT_ID
FROM hierarchy p
WHERE p.PERSON_ID = #id AND p.PERSON_ID != p.PARENT_ID
UNION ALL
SELECT c.PERSON_ID,c.Name, c.PARENT_ID
FROM hierarchy c
JOIN T h ON h.PARENT_ID = c.PERSON_ID)
insert into #table
select * from T;
IF exists(select * from #table)
BEGIN
select PERSON_ID,Name from #table
End
Else
Begin
select PERSON_ID,Name from Hierarchy
where PERSON_ID = #id
end
Above query show me the desire output when I set the parameter value #id = 1
Above query show me the desire output when I set the parameter value #id = 9
Issue:
I don't want to insert null values in Parent_ID like if there is no more Parent of that Person then I insert same Person_ID in Parent_ID column.
If I replace null values with there person_id then I got below error.
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

SQL Select with Priority

I need to select top 1 most valid discount for a given FriendId.
I have the following tables:
DiscountTable - describes different discount types
DiscountId, Percent, Type, Rank
1 , 20 , Friend, 2
2 , 10 , Overwrite, 1
Then I have another two tables (both list FriendIds)
Friends
101
102
103
Overwrites
101
105
I have to select top 1 most valid discount for a given FriendId. So for the above data this would be sample output
Id = 101 => gets "Overwrite" discount (higher rank)
Id = 102 => gets "Friend" discount (only in friends table)
Id = 103 => gets "Friend" discount (only in friends table)
Id = 105 => gets "Overwrite" discount
Id = 106 => gets NO discount as it does not exist in neither Friend and overwrite tables
INPUT => SINGLE friendId (int).
OUTPUT => Single DISCOUNT Record (DiscountId, Percent, Type)
Overwrites and Friend tables are the same. They only hold list of Ids (single column)
Having multiple tables of identical structure is usually bad practice, a single table with ID and Type would suffice, you could then use it in a JOIN to your DiscountTable:
;WITH cte AS (SELECT ID,[Type] = 'Friend'
FROM Friends
UNION ALL
SELECT ID,[Type] = 'Overwrite'
FROM Overwrites
)
SELECT TOP 1 a.[Type]
FROM cte a
JOIN DiscountTable DT
ON a.[Type] = DT.[Type]
WHERE ID = '105'
ORDER BY [Rank]
Note, non-existent ID values will not return.
This will get you all the FriendIds and the associate discount of the highest rank. It's an older hack that doesn't require using top or row numbering.
select
elig.FriendId,
min(Rank * 10000 + DiscountId) % 10000 as DiscountId
min(Rank * 10000 + Percent) % 10000 as Percent,
from
DiscountTable as dt
inner join (
select FriendId, 'Friend' as Type from Friends union all
select FriendId, 'Overwrite' from Overwrites
) as elig /* for eligible? */
on elig.Type = dt.Type
group by
elig.FriendId
create table discounts (id int, percent1 int, type1 varchar(12), rank1 int)
insert into discounts
values (1 , 20 , 'Friend', 2),
(2 , 10 , 'Overwrite', 1)
create table friends (friendid int)
insert into friends values (101),(102), (103)
create table overwrites (overwriteid int)
insert into overwrites values (101),(105)
select ids, isnull(percent1,0) as discount from (
select case when friendid IS null and overwriteid is null then 'no discount'
when friendid is null and overwriteid is not null then 'overwrite'
when friendid is not null and overwriteid is null then 'friend'
when friendid is not null and overwriteid is not null then (select top 1 TYPE1 from discounts order by rank1 desc)
else '' end category
,ids
from tcase left outer join friends
on tcase.ids = friends.friendid
left join overwrites
on tcase.ids = overwrites.overwriteid
) category1 left join discounts
on category1.category=discounts.type1

Delete all level child item using sql query

I have a table where I have menus listed where I can insert and delete.
Structure goes like:-
ID Name ParentId
1 1. Home 0
2 2. Products 0
3 a. SubProduct1 2
4 b. SubProduct2 2
5 i. Subsub 4
6 ii. ...... 4
7 3. About 0
Top-level menu ParentId is always 0 as displayed in 1, 2 and 7.
Child level items would have ParentId of their parent for ex. Subproduct has 2 as its parentId.
When I delete menu item that time all level child item should be delete irrespective of there levels using SQL query.
There can be any number of levels
The levels can go upto subsubsubsub...... any number.
How about this query:
DECLARE #DelID INT
SET #DelID=1
;WITH T(xParent, xChild)AS
(
SELECT ParentID, ChildId FROM Table WHERE ParentID=#DelID
UNION ALL
SELECT ParentID, ChildId FROM TABLE INNER JOIN T ON ParentID=xChild
)
DELETE FROM TABLE WHERE ParentID IN (SELECT xParent FROM T)
You can use a common table expression to get all the heirarchy items from the item you want to delete to the end of the tree hten
;WITH ParentChildsTree
AS
(
SELECT ID, Name, ParentId
FROM MenuItems
WHERE Id = #itemToDelete
UNION ALL
SELECT ID, Name, ParentId
FROM ParentChildsTree c
INNER JOIN MenuItems t ON c.ParentId = t.Id
)
DELETE FROM MenuItems
WHERE ID IN (SELECT ID FROM ParentChildsTree);
Here is a Demo.
For example if you pass a parameter #itemToDelete = 4 to the query the the items with ids 2 and 4 will be deleted.