Get child hierarchy for a subset of users - sql

I have a customer table where there can be a hierarchy of parent-child records.
The customer table has a structure like,
customer {
id integer,
user_id integer,
parent_id integer
}
Given an user_id I know how to fetch all child users in hierachy that belong to the given user_id. For example for user_id value of 100,
WITH RECURSIVE subordinates AS (
SELECT c1.*
FROM customer c1
WHERE c1.user_id = 100
UNION
SELECT c2.*
FROM customer c2
INNER JOIN subordinates s
ON s.id = c2.parent_id)
SELECT * FROM subordinates;
But I would like to restrict the output of above query to a subset of users. So the first argument to the above query will be a parent user_id (let's call it parent_user) and the second argument will be a list of user_id (let's call it child_users).
What I would like to do is,
Iterate over child_users
Check if child_user in child_users is a child of parent_user
If yes, fetch entire child hierachy of child_user
Can above 3 steps be achieved in a single sql query. Plese suggest.
EDIT
The sample table data can be,
id user_id parent_id
-----------------------
1 100 null
2 101 100
3 102 101
4 103 102
5 104 100
6 105 null
So given above data in the table, for input values of parent_user = 100 and child_users = [101, 105] I am expetcting to retrieve following output,
id user_id parent_id
-----------------------
2 101 100
3 102 101
4 103 102
The record with id = 5 should't be part of the output even though it has parent_id = 100 since user_id 104 is not in the child_users list.

Please see the answers below and let me know if that works
declare #Customer table ( Id int,userID int,parentID int)
insert #Customer (Id, userID, parentID)
select 1,100,null union all <br/>
select 1,101,100 union all <br/>
select 1,102,101 union all <br/>
select 1,103,102 union all <br/>
select 1,104,100 union all <br/>
select 1,105,null <br/>
;with cte_emp as <br/>
( <br/>
select ID,userID,parentID
from #Customer
where parentID=100 and userID in (101,105) <br/>
union all <br/>
select e.ID,e.userID,e.parentID
from #Customer e join cte_emp cte
on cte.userid=e.parentID <br/>
) <br/>
select * from cte_emp <br/>

If I follow you correctly, you want to filter on the immediate parent of the starting node. You can just put that condition in the anchor of the CTE:
with recursive subordinates as (
select c.*
from customer
where user_id = any($1) and parent_id = $2
union all
select c.*
from customer c
inner join subordinates s on s.id = c.parent_id
)
select * from subordinates;
Note that I changed the query to accept to input parameters:
$1 is an array of child ids
$2 is the id of the parent that is used to filter

Related

Using recursion with a self Foreign key

I'm using PostgreSQL 10, and I have the following structure:
A table Type with a foreign key to itself.
id name parent_id
1 namea Null
2 nameb Null
3 namea1 1
4 namea11 3
5 namea111 4
6 nameb1 2
7 nameb2 2
A table Item_Type for a Many to Many relation
id type_id item_id
1 1 1
2 3 2
3 5 3
4 7 4
Table Item which has M2M relation to Type.
id name
1 item1
2 item2
3 item3
4 item4
At this moment, I'm using an additional path field, which I calculate every time I make operations(crud) with Type.
I'm wondering if is not faster and easy to try to use the PostgreSQL recursion.
I checked the documentation but I didn't understand very well, because I get an error, and I don't understate why.
WITH RECURSIVE descendants AS (
SELECT id, name FROM Type WHERE id = 1
UNION
SELECT t.id, t.name, t.parent_id FROM Type AS t
INNER JOIN descendants AS d ON d.id = t.parent_id
) SELECT * FROM descendants;
ERROR: each UNION query must have the same number of columns
What I need - Giving a Type name:
1) Get all names/id for the requested Type and is descendants
2) Get all Item for the requested Type and is descendants, and the number of Item per Type and descendants
For example:
If the requested Type name is 'namea1', I should get for Type ids 1,3,4,5 and
for Item ids 1,2,3
The error says it all. Your union is divided between:
SELECT <2 fields> from Type ...
SELECT <3 fields> from Type JOIN Descendant ...
Simply select 3 fields on both halves:
WITH RECURSIVE descendants AS (
SELECT id, name, parent_id FROM Type WHERE id = 1
UNION
SELECT t.id, t.name, t.parent_id FROM Type AS t
INNER JOIN descendants AS d ON d.id = t.parent_id
) SELECT * FROM descendants;

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

Query to List all hierarchical parents and siblings and their childrens, but not list own childrens

I've a basic SQL table with a simple heirarchial connection between each rows. That is there is a ParentID for Every rows and using that its connecting with another row. Its as follows
AccountID | AccountName | ParentID
---------------------------------------
1 Mathew 0
2 Philip 1
3 John 2
4 Susan 2
5 Anita 1
6 Aimy 1
7 Elsa 3
8 Anna 7
.............................
.................................
45 Kristoff 8
Hope the structure is clear
But my requirement of listng these is a little weird. That is when we pass an AccountID, it should list all its parents and siblings and siblings childrens. But it never list any child of that AccountID to any level. I can explain that in little more detail with a picture. Sorry for the clarity of the picture.. mine is an old phone cam..
When we pass the AccountID 4, it should list all Parents and its siblings, but it should not list 4,6,7,8,9,10. That means that account and any of it childrens should be avoid in the result (Based on the picture tree elements). Hope the explanation is clear.
If I've got it right and you need to output whole table except 4 and all of it's descendants then try this recursive query:
WITH CT AS
(
SELECT * FROM T WHERE AccountID=4
UNION ALL
SELECT T.* FROM T
JOIN CT ON T.ParentID = CT.AccountId
)
SELECT * FROM T WHERE AccountID
NOT IN (SELECT AccountID FROM CT)
SQLFiddle demo
Answering to the question in the comment:
So it will not traverse to the top. It only traverse to specified
account. For example if I pass 4 as first parameter and 2 as second
parameter, the result should be these values 2,5,11,12
You should start from the ID=2 and travel to the bottom exclude ID=4 so you cut whole subtree after ID=4:
WITH CT AS
(
SELECT * FROM T WHERE AccountID=2
UNION ALL
SELECT T.* FROM T
JOIN CT ON T.ParentID = CT.AccountId
WHERE T.AccountId<>4
)
SELECT * FROM CT
Try this:
;with cte as
(select accountid,parentid, 0 as level from tbl
where parentid = 0
union all
select t.accountid,t.parentid,(level+1) from
cte c inner join tbl t on c.accountid= t.parentid
)
select * from cte
where level < (select level from cte where accountid = #accountid)
When you pass in the parameter #accountid this will return the accountid values of all nodes on levels before that of the parameter.
If you want to return everything on the same level as input except input itself, you can change the where clause to;
where level <=(select level from cte where accountid= #accountid )
and accountid <> #accountid
In your example, if #accountid = 4, this will return the values 1,2,3 (ancestors) as well as 5,13,14 (siblings).
Does this return what you are after?
declare #AccountID int
set #AccountID = 4
;with parents
as (
select AccountID, AccountName, ParentID
from Account
where AccountID = (select ParentID from Account Where AccountID = #AccountID)
union all
select A.AccountID, A.AccountName, A.ParentID
from Account as A
join parents as P
on P.ParentID = A.AccountID
),
children
as (
select AccountID, AccountName, ParentID
from parents
union all
select A.AccountID, A.AccountName, A.ParentID
from Account as A
join children as C
on C.AccountID = A.ParentID
where A.AccountID <> #AccountID
)
select distinct AccountID, AccountName, ParentID
from children
order by AccountID
For me it sounds like you want to go up in the tree. So considering this test data
DECLARE #tbl TABLE(AccountID INT,AccountName VARCHAR(100),ParentID INT)
INSERT INTO #tbl
VALUES
(1,'Mathew',0),
(2,'Philip',1),
(3,'John',2),
(4,'Susan',2),
(5,'Anita',1),
(6,'Aimy',1),
(7,'Elsa',3),
(8,'Anna',7)
The I would write a query like this:
DECLARE #AcountID INT=4
;WITH CTE
AS
(
SELECT
tbl.AccountID,
tbl.AccountName,
tbl.ParentID
FROM
#tbl AS tbl
WHERE
tbl.AccountID=#AcountID
UNION ALL
SELECT
tbl.AccountID,
tbl.AccountName,
tbl.ParentID
FROM
#tbl AS tbl
JOIN CTE
ON CTE.ParentID=tbl.AccountID
)
SELECT
*
FROM
CTE
WHERE
NOT CTE.AccountID=#AcountID
This will return a result like this:
2 Philip 1
1 Mathew 0

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.