Postgresql query to get sum of tree - sql

Hi guys I would like to ask about postgresql and what could be the best query to get sum of column when you have table of elements that has some descendants of more levels ie.
id id_parentvalue
1 null 3
2 null 4
3 1 2
4 2 3
5 3 4
6 3 2
7 4 5
8 4 7
so the result would be rows with sum of all of their tree as follows
value of ids 5 and 6 together is 6 plus value of their parent would be 8 plus his parent would be 11, and same for items with id 7 and 8 so the grandĖ‡parent with id=2 would have value 19
id id_parentvalue
1 null 11
2 null 19
thanks in advance

Use recursive CTEs:
with recursive cte as (
select t.id, t.value, ultimate_parent as id
from t
where id_parent is null
union all
select t.id, t.value, cte.ultimate_parent
from cte join
t
on t.id_parent = cte.id
)
select ultimate_parent, sum(value)
from cte
group by ultimate_parent;
The recursive part starts with the ultimate parents -- the records whose parent is NULL. It then brings in lower levels, step-by-step, keeping the id of the ultimate parent.
The final aggregation just sums the values together.

Related

SQL Server (terminal result) hierarchy map

In SQL Server 2016, I have a table with the following chaining structure:
dbo.Item
OriginalItem
ItemID
NULL
7
1
2
NULL
1
5
6
3
4
NULL
8
NULL
5
9
11
2
3
EDIT NOTE: Bold numbers were added as a response to #lemon comments below
Importantly, this example is a trivialized version of the real data, and the neatly ascending entries is not something that is present in the actual data, I'm just doing that to simplify the understanding.
I've constructed a query to get what I'm calling the TerminalItemID, which in this example case is ItemID 4, 6, and 7, and populated that into a temporary table #TerminalItems, the resultset of which would look like:
#TerminalItems
TerminalItemID
4
6
7
8
11
What I need, is a final mapping table that would look something like this (using the above example -- note that it also contains for 4, 6, and 7 mapping to themselves, this is needed by the business logic):
#Mapping
ItemID
TerminalItemID
1
4
2
4
3
4
4
4
5
6
6
6
7
7
8
8
9
11
11
11
What I need help with is how to build this last #Mapping table. Any assistance in this direction is greatly appreciated!
This should do:
with MyTbl as (
select *
from (values
(NULL, 1 )
,(1, 2 )
,(2, 3 )
,(3, 4 )
,(NULL, 5 )
,(5, 6 )
,(NULL, 7 )
) T(OriginalItem, ItemID)
)
, TerminalItems as (
/* Find all leaf level items: those not appearing under OriginalItem column */
select LeafItem=ItemId, ImmediateOriginalItem=M.OriginalItem
from MyTbl M
where M.ItemId not in
(select distinct OriginalItem
from MyTbl AllParn
where OriginalItem is not null
)
), AllLevels as (
/* Use a recursive CTE to find and report all parents */
select ThisItem=LeafItem, ParentItem=ImmediateOriginalItem
from TerminalItems
union all
select ThisItem=AL.ThisItem, M.OriginalItem
from AllLevels AL
inner join
MyTbl M
on M.ItemId=AL.ParentItem
)
select ItemId=coalesce(ParentItem,ThisItem), TerminalItemId=ThisItem
from AllLevels
order by 1,2
Beware of the MAXRECURSION setting; by default SQLServer iterates through recursion 100 times; this would mean that the depth of your tree can be 100, max (the maximum number of nodes between a terminal item and its ultimate original item). This can be increased by OPTION(MAXRECURSION nnn) where nnn can be adjusted as needed. It can also be removed entirely by using 0 but this is not recommended because your data can cause infinite loops.
This is a typical gaps-and-islands problem and can also be carried out without recursion in three steps:
assign 1 at the beginning of each partition
compute a running sum over your flag value (generated at step 1)
extract the max "ItemID" on your partition (generated at step 2)
WITH cte1 AS (
SELECT *, CASE WHEN OriginalItem IS NULL THEN 1 ELSE 0 END AS changepartition
FROM Item
), cte2 AS (
SELECT *, SUM(changepartition) OVER(ORDER BY ItemID) AS parts
FROM cte1
)
SELECT ItemID, MAX(ItemID) OVER(PARTITION BY parts) AS TerminalItemID
FROM cte2
Check the demo here.
Assumption: Your terminal id items correspond to the "ItemID" value preceding a NULL "OriginalItem" value.
EDIT: "Fixing orphaned records."
The query works correctly when records are not orphaned. The only way to deal them, is to get missing records back, so that the query can work correctly on the full data.
This is carried out by an extra subquery (done at the beginning), that will apply a UNION ALL between:
the available records of the original table
the missing records
WITH fix_orphaned_records AS(
SELECT * FROM Item
UNION ALL
SELECT NULL AS OriginalItem,
i1.OriginalItem AS ItemID
FROM Item i1
LEFT JOIN Item i2 ON i1.OriginalItem = i2.ItemID
WHERE i1.OriginalItem IS NOT NULL AND i2.ItemID IS NULL
), cte AS (
...
Missing records correspond to "OriginalItem" values that are never found within the "ItemID" field. A self left join will uncover these missing records.
Check the demo here.
You can use a recursive CTE to compute the last item in the sequence. For example:
with
n (orig_id, curr_id, lvl) as (
select itemid, itemid, 1 from item
union all
select n.orig_id, i.itemid, n.lvl + 1
from n
join item i on i.originalitem = n.curr_id
)
select *
from (
select *, row_number() over(partition by orig_id order by lvl desc) as rn from n
) x
where rn = 1
Result:
orig_id curr_id lvl rn
-------- -------- ---- --
1 4 4 1
2 4 3 1
3 4 2 1
4 4 1 1
5 6 2 1
6 6 1 1
7 7 1 1
See running example at db<>fiddle.

SQL products contain other

I have a table look like this
Parent Child
10 2
10 3
10 4
10 5
11 2
11 3
as you can see parent 10 also contain parent 11 and thats what I want to display in the table, I want to add to it a row with that data :
Parent Child
10 2
10 3
10 4
10 5
**10 11**
11 2
11 3
You can get parents that "contain" other parents using a self-join and aggregation:
with t as (
select t.*, count(*) over (partition by parent) as num_child
from yourtable
)
select tp2.parent, tp.parent
from t tp join
t tp2
on tp.child = tc2.child and tp.parent <> tp2.parent
group by tp.parent, tp2.parent, tp.num_child
having count(*) = tp.num_child -- all children match
(This version assumes no duplicate rows.)
You can then use insert to add these into the table.
Note: If two parents have the same children, two rows will be inserted.

How to select parent child till end using root parent id in sql?

Below the table :
I need to get without recursion, but use any other join and union.
Id ParentId
1 0
2 1
3 2
4 2
5 4
6 5
7 6
8 7
9 8
N N
Without recursion use any other join queries
DECLARE #Nvalue INT = 10
SELECT NUMBER AS ID,NUMBER-1 AS ParentID FROM (
SELECT DISTINCT NUMBER FROM master..spt_values WHERE number BETWEEN 1 AND #Nvalue
)T

Parent and child relationship is broken when do the sort based on parent name

I want to sort the name based on 1st level (sort only applicable root id is null or level ==1). If I search based on name the result set is broken the parent and child relationship.
I have 500 000 records. What is the best way to do this performance wise?
ID PARENT ID ROOT ID NAME level
===============================================
1 NULL NULL FIRST 1
2 1 1 SECOND 2
3 2 1 THIRD 3
4 1 1 FORTH 4
5 4 1 FIFTH 5
6 NULL NULL SIXTH 1
7 6 6 SEVENTH 2
8 7 6 EIGHTH 2
9 NULL NULL NINTH 1
10 NULL NULL TENTH 1
11 NULL NULL ELEVEN 1
12 11 11 TWELVE 2
13 12 11 THIRTEEN 3
14 13 11 FOURTEEN 4
EXPECTED OUTPUT - SORT BY NAME ASC
ID PARENT ID ROOT ID NAME level
===============================================
11 NULL NULL ELEVEN 1
12 11 11 TWELVE 2
13 12 11 THIRTEEN 3
14 13 11 FOURTEEN 4
1 NULL NULL FIRST 1
2 1 1 SECOND 2
3 2 1 THIRD 3
4 1 1 FORTH 4
5 4 1 FIFTH 5
9 NULL NULL NINTH 1
6 NULL NULL SIXTH 1
7 6 6 SEVENTH 2
8 7 6 EIGHTH 2
10 NULL NULL TENTH 1
It appears that you want to sort the results by two keys:
first, by the NAME column of the root row associated with each row, which may be that row itself, and
second, by the level column.
You can achieve this by joining the table to itself to make the root-NAME association. For example,
select a.*
from
my_table a
join my_table b
on isnull(a.[root id], a.id) = b.id
order by b.name, a.level
Notice the isnull() -- although you generally want to join based on b.id = a.[root id], you need to avoid excluding the root rows, whose root_id is NULL. With the isnull(), you join those rows based on id instead (i.e. you join them to themselves).
You can use a LEFT JOIN to get the parent's name, then use the parent's name to ORDER:
SELECT t1.ID, t1.[PARENT ID], t1.[ROOT ID], t1.NAME, t1.level
FROM mytable AS t1
LEFT JOIN mytable AS t2 ON t2.ID = t1.[ROOT ID]
ORDER BY COALESCE(t2.NAME, t1.NAME), t1.level
If the parent's name is not available, then this is a case of the current row being the parent itself. Hence the row's NAME field is used instead so as to sort.
Finally, records that belong to the same parent are sorted by level.
This should give you what I think you're looking for:
SELECT
id,
parent_id,
root_id,
name,
level
FROM
Your_Table T1
INNER JOIN Your_Table T2 ON T2.id = T1.root_id
ORDER BY
T2.name,
T1.level

How to get hierarchy from multiple values on SQL Server?

I have table RD with only one column:
-----
rd_id
-----
3
2
6
7
8
I have table DL with two columns, there is hierarchy on stored on this table:
----------------------
dl_id dl_parent
----------------------
1 2
2 Null
3 Null
4 6
6 7
7 8
8 Null
Now the problem is how to get the hierarchy out from DL table using the member of RD table.
The result will be:
--------------
rd_id dl_id
--------------
3 3
2 2
6 6
6 7
6 8
7 7
8 8
8 8
I've been toiling with this problem from Friday and still can't get crack of it.
I know that I can use Common Table Expression to traverse the recursive from one value (like example create one function with input 6 and produce 6,7,8).
but I don't know how to use multiple value (rd_id).
Have some ideas?
This produces the correct results. Data setup:
declare #RD table (rd_id int not null)
insert into #RD(rd_id) values
(3),
(2),
(6),
(7),
(8)
declare #DL table (dl_id int not null,dl_parent int null)
insert into #DL(dl_id,dl_parent) values
(1,2),
(2,Null),
(3,Null),
(4,6),
(6,7),
(7,8),
(8,Null)
And the query:
;with AllValues as (
select rd_id,rd_id as dl_id from #RD
union all
select rd_id,dl_parent
from AllValues av
inner join
#DL dl
on
av.dl_id = dl.dl_id
where
dl.dl_parent is not null
)
select * from AllValues
Result:
rd_id dl_id
----------- -----------
3 3
2 2
6 6
7 7
8 8
7 8
6 7
6 8
Explanation:
In the anchor of the CTE, we simply select rd_id from the #RD table twice - since your sample implies that every input row should produce an output row with the same value in both columns.
We then join to the #DL table for any matching parent rows we can find, based on the second column. If we find a parent, then we produce a new row, substituting the parent value into the second column. This continues until no new rows are produced.
You have to use on-the-fly views to create a recursive query like this:
with n as (
select dl_id, dl_id as ancestor
from dbo.dl
union all
select np1.dl_id, n.ancestor
from dl as np1 , n
where n.dl_id = np1.dl_parent)
select * from n
where dl_id in (select rd_id from rd)
order by dl_id, ancestor