How to calculate the sum of values in a tree using SQL - sql

I need to sum points on each level earned by a tree of users. Level 1 is the sum of users' points of the users 1 level below the user. Level 2 is the Level 1 points of the users 2 levels below the user, etc...
The calculation happens once a month on a non production server, no worries about performance.
What would the SQL look like to do it?
If you're confused, don't worry, I am as well!
User table:
ID ParentID Points
1 0 230
2 1 150
3 0 80
4 1 110
5 4 54
6 4 342
Tree:
0
|---\
1 3
| \
2 4---
\ \
5 6
Output should be:
ID Points Level1 Level2
1 230 150+110 150+110+54+342
2 150
3 80
4 110 54+342
5 54
6 342
SQL Server Syntax and functions preferably...

If you were using Oracle DBMS that would be pretty straightforward since Oracle supports tree queries with the CONNECT BY/STARTS WITH syntax. For SQL Server I think you might find Common Table Expressions useful

Trees don't work well with SQL. If you have very (very very) few write accesses, you could change the tree implementation to use nested sets, that would make this query incredibly easy.
Example (if I'm not mistaken):
SELECT SUM(points)
FROM users
where left > x and right < y
However, any changes on the tree require touching a massive amount of rows. It's probably better to just do the recursion in you client.

I would say: create a stored procedure, probably has the best performance.
Or if you have a maximum number of levels, you could create subqueries, but they will have a very poort performance.
(Or you could get MS SQL Server 2008 and get the new hierarchy functions... ;) )

SQL in general, like others said, does not handle well such relations. Typically, a surrogate 'relations' table is needed (id, parent_id, unique key on (id, parent_id)), where:
every time you add a record in 'table', you:
INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_id]);
INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_parent_id]);
INSERT INTO relations (id, parent_id)
SELECT [current_id], parent_id
FROM relations
WHERE id = [current_parent_id];
have logic to avoid cycles
make sure that updates, deletions on 'relations' are handled with stored procedures
Given that table, you want:
SELECT rel.parent_id, SUM(tbl.points)
FROM table tbl INNER JOIN relations rel ON tbl.id=rel.id
WHERE rel.parent_id <> 0
GROUP BY rel.parent_id;

Ok, this gives you the results you are looking for, but there are no guarantees that I didn't miss something. Consider it a starting point. I used SQL 2005 to do this, SQL 2000 does not support CTE's
WITH Parent (id, GrandParentId, parentId, Points, Level1Points, Level2Points)
AS
(
-- Find root
SELECT id,
0 AS GrandParentId,
ParentId,
Points,
0 AS Level1Points,
0 AS Level2Points
FROM tblPoints ptr
WHERE ptr.ParentId = 0
UNION ALL (
-- Level2 Points
SELECT pa.GrandParentId AS Id,
NULL AS GrandParentId,
NULL AS ParentId,
0 AS Points,
0 AS Level1Points,
pa.Points AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.GrandParentId = pt.Id
UNION ALL
-- Level1 Points
SELECT pt.ParentId AS Id,
NULL AS GrandParentId,
NULL AS ParentId,
0 AS Points,
pt.Points AS Level1Points,
0 AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL
UNION ALL
-- Points
SELECT pt.id,
pa.ParentId AS GrandParentId,
pt.ParentId,
pt.Points,
0 AS Level1Points,
0 AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL )
)
SELECT id,
SUM(Points) AS Points,
SUM(Level1Points) AS Level1Points,
CASE WHEN SUM(Level2Points) > 0 THEN SUM(Level1Points) + SUM(Level2Points) ELSE 0 END AS Level2Points
FROM Parent
GROUP BY id
ORDER by id

If you are working with trees stored in a relational database, I'd suggest looking at "nested set" or "modified preorder tree traversal". The SQL will be as simple as that:
SELECT id,
SUM(value) AS value
FROM table
WHERE left>left\_value\_of\_your\_node
AND right<$right\_value\_of\_your\_node;
... and do this for every node you are interested in.
Maybe this will help you:
http://www.dbazine.com/oracle/or-articles/tropashko4 or use google.

You have a couple of options:
Use a cursor and a recursive user-defined function call (it's quite slow)
Create a cache table, update it on INSERT using a trigger (it's the fastest solution but could be problematic if you have lots of updates to the main table)
Do a client-side recursive calculation (preferable if you don't have too many records)

You can write a simple recursive function to do the job. My MSSQL is a little bit rusty, but it would look like this:
CREATE FUNCTION CALC
(
#node integer,
)
returns
(
#total integer
)
as
begin
select #total = (select node_value from yourtable where node_id = #node);
declare #children table (value integer);
insert into #children
select calc(node_id) from yourtable where parent_id = #node;
#current = #current + select sum(value) from #children;
return
end

The following table:
Id ParentId
1 NULL
11 1
12 1
110 11
111 11
112 11
120 12
121 12
122 12
123 12
124 12
And the following Amount table:
Id Val
110 500
111 50
112 5
120 3000
121 30000
122 300000
Only the leaves (last level) Id's have a value defined.
The SQL query to get the data looks like:
;WITH Data (Id, Val) AS
(
select t.Id, SUM(v.val) as Val from dbo.TestTable t
join dbo.Amount v on t.Id = v.Id
group by t.Id
)
select cd.Id, ISNULL(SUM(cd.Val), 0) as Amount FROM
(
-- level 3
select t.Id, d.val from TestTable t
left join Data d on d.id = t.Id
UNION
-- level 2
select t.parentId as Id, sum(y.Val) from TestTable t
left join Data y on y.id = t.Id
where t.parentId is not null
group by t.parentId
UNION
-- level 1
select t.parentId as Id, sum(y.Val) from TestTable t
join TestTable c on c.parentId = t.Id
left join Data y on y.id = c.Id
where t.parentId is not null
group by t.parentId
) AS cd
group by id
this results in the output:
Id Amount
1 333555
11 555
12 333000
110 500
111 50
112 5
120 3000
121 30000
122 300000
123 0
124 0
I hope this helps.

Related

SQL: Looping over query result

Regarding company tree in PostgreSQL
I am using the following to get the subsidiaries of the company with id = 11.
SELECT * FROM "OwnershipTable"
WHERE "Parent_ID" = 11;
giving me the following output
Company_ID
Company_Name
Parent_ID
Parent_Name
111
Holdco 1
11
Topco
112
Holdco 2
11
Topco
113
Holdco 3
11
Topco
114
Holdco 4
11
Topco
However, I would like to investigate if any of the Holdco-companies has any subsidiaries. My question is therefore: Is it possible insert the column "Company_ID" as "Parent_ID" in the query using some sort of loop?
Yes. This is called a recursive CTE:
with recursive cte as (
select company_id as parent_company_id, company_id as child_id
from OwnershipTable ot
where parent_id = 11
union all
select cte.parent_company_id, ot.company_id
from cte join
OwnershipTable ot
on ot.parent_id = cte.child_id
)
select *
from cte;
If you want additional information about the companies, you can join it in or include it in the recursive CTE definitions.
This should work:
SELECT * FROM "OwnershipTable" ot
WHERE EXISTS(SELECT 1 FROM "OwnershipTable" where Parent_ID = ot.Company_ID)

Recursive many-to-many CTE

I am not a DB expert was trying to write cte for below scenario for sql server 2012 but could not resolve my problem. Appreciate if some one help me to figure this out.
I am having a many to many table called Jockcard2Item for for the tables Jobcard and items.
Jobcard may have multiple items and items can have many Jobcards,
in my case I want to find out all the Jobcards for given item id like bellow.
jobcard1 having item 1, item 2.
jobcard2 having item 2, item 3
jobards3 having item 3
jobcard4 having item 4
I want to get all jobcards associated to given item, if there other
items which is refering the same jobcard (like jobcard2 referring
item2) want to include that job cards too.
result should return jobcard1,jobcard3,jobcard3,
I was trying with below query which is running infinite.
DECLARE #itemId int
SELECT #itemId = 12
;WITH temp as(
SELECT jobCard_ID, item_id FROM Jobcard2Item
WHERE item_id = #EstimateID
UNION ALL
SELECT bi.jobCard_ID ,bi.item_id FROM Jobcard2Item
JOIN temp x ON bi.jobCard_ID= x.jobCard_ID where x.item_id not IN (bi.item_id )
)
select * from temp option (maxrecursion 0)
sample date as bellow
id jobcard_ID, item_ID
1 512 12
2 512 13
3 513 13
4 513 14
5 514 14
6 515 15
7 516 16
when I pass the 12 as item_ID it should return the result - 512,513,514,
This query should help:
select distinct A.jobcard_id from Jockcard2Item A join Jockcard2Item B
on A.jobcard_id = B.jobcard_id or A.item_id = B.item_id
where A.item_id <> B.item_id or A.jobcard_id <> B.jobcard_id
It doesn't require CTE (I tried, but I think it's impossible).

SQL Server 2008 - need help on a antithetical query

I want to find out meter reading for given transaction day. In some cases there won’t be any meter reading and would like to see a meter reading for previous day.
Sample data set follows. I am using SQL Server 2008
declare #meter table (UnitID int, reading_Date date,reading int)
declare #Transactions table (Transactions_ID int,UnitID int,Transactions_date date)
insert into #meter (UnitID,reading_Date,reading ) values
(1,'1/1/2014',1000),
(1,'2/1/2014',1010),
(1,'3/1/2014',1020),
(2,'1/1/2014',1001),
(3,'1/1/2014',1002);
insert into #Transactions(Transactions_ID,UnitID,Transactions_date) values
(1,1,'1/1/2014'),
(2,1,'2/1/2014'),
(3,1,'3/1/2014'),
(4,1,'4/1/2014'),
(5,2,'1/1/2014'),
(6,2,'3/1/2014'),
(7,3,'4/1/2014');
select * from #meter;
select * from #Transactions;
I expect to get following output
Transactions
Transactions_ID UnitID Transactions_date reading
1 1 1/1/2014 1000
2 1 2/1/2014 1010
3 1 3/1/2014 1020
4 1 4/1/2014 1020
5 2 1/1/2014 1001
6 2 3/1/2014 1001
7 3 4/1/2014 1002
Your SQL Query to get your desired out put will as following:
SELECT Transactions_ID, T.UnitID, Transactions_date
, (CASE WHEN ISNULL(M.reading,'') = '' THEN
(
SELECT MAX(Reading) FROM #meter AS A
JOIN #Transactions AS B ON A.UnitID=B.UnitID AND A.UnitID=T.UnitID
)
ELSE M.reading END) AS Reading
FROM #meter AS M
RIGHT OUTER JOIN #Transactions AS T ON T.UnitID=M.UnitID
AND T.Transactions_date=M.reading_Date
I can think of two ways to approach this - neither of them are ideal.
The first (and slightly better) way would be to create a SQL Function that took the Transactions_date as a parameter and returned the reading for Max(Reading_date) where reading_date <= transactions_date. You could then use this function in a select statement against the Transactions table.
The other approach would be to use a cursor to iterate through the transactions table and use the same logic as above where you return the reading for Max(Reading_date) where reading_date <= transactions_date.
Try the below query:
Please find the result of the same in SQLFiddle
select a.Transactions_ID, a.UnitID, a.Transactions_date,
case when b.reading IS NULL then c.rd else b.reading end as reading
from
Transactions a
left outer join
meter b
on a.UnitID = b.UnitID
and a.Transactions_date = b.reading_Date
inner join
(
select UnitID,max(reading) as rd
from meter
group by UnitID
) as C
on a.UnitID = c.UnitID

sql select query self join or loop through to fetch records [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question appears to be off-topic because it lacks sufficient information to diagnose the problem. Describe your problem in more detail or include a minimal example in the question itself.
Closed 8 years ago.
Improve this question
I have this kind of scenario in sql server I have table named Room and here is the data of it and I want output something like this as shown in this picture I have tried to show my table named room and then on top of it I have placed tag input which have RoomId,ConnectingRoomID and many more other columns now what I want is a sql select query that can return me the scenario I have placed with tag name output..
These values are self created I have thousand of rooms and in room table and thousand of connecting room with it hope my question is clear enough thanks.
I think you can use this:
with x as (
select *, sum(case connectingroomid when 0 then 1 else 0 end) over(order by roomid) as grp
from rooms
)
select x.roomid, (select min(x2.roomid) as min_roomid from x x2 where x2.grp = x.grp) as connectingroomid
from x
This is a recursive query: For all rooms go to the connecting room till you find the one that has no more connecting room (i.e. connecting room id is 0).
with rooms (roomid, connectingroomid) as
(
select
roomid,
case when connectingroomid = 0 then
roomid
else
connectingroomid
end as connectingroomid
from room
where connectingroomid = 0
union all
select room.roomid, rooms.connectingroomid
from room
inner join rooms on room.connectingroomid = rooms.roomid
)
select * from rooms
order by connectingroomid, roomid;
Here is the SQL fiddle: http://www.sqlfiddle.com/#!3/46ed0/1.
EDIT: Here is the explanation. Rather than doing this in the comments I am doing it here for better readability.
The WITH clause is used to create a recursion here. You see I named it rooms and inside rooms I select from rooms itself. Here is how to read it: Start with the part before UNION ALL. Then recursively do the part after UNION ALL. So, before UNION ALL I only select the records where connectingroomid is zero. In your example you show every room with its connectingroomid except for those with connectingroomid for which you show the room with itself. I use CASE here to do the same. But now that I am explaining this, I notice that connectingroomid is always zero because of the WHERE clause. So the statement can be simplified thus:
with rooms (roomid, connectingroomid) as
(
select
roomid,
roomid as connectingroomid
from room where connectingroomid = 0
union all
select room.roomid, rooms.connectingroomid
from room
inner join rooms on room.connectingroomid = rooms.roomid
)
select * from rooms
order by connectingroomid, roomid;
The SQL fiddle: http://www.sqlfiddle.com/#!3/46ed0/2.
With the part before the UNION ALL I found the two rooms without connecting room. Now the part after UNION ALL is executed for the two rooms found. It selects the rooms which connecting room was just found. And then it selects the rooms which connecting room was just found. And so on till the join returns no more rooms.
Hope this helps understanding the query. You can look for "recursive cte" on the Internet to find more examples and explanations on the topic.
select RoomID,
(Case when RoomID<=157 then 154
else 158 end) ConnectingRoomID
from Input
First of all, your output is not correct: Room 154 should also connect to room 0 :-)
What you are after is the transitive closure of the relation defined by the table Room. It is impossible to get this with "vanilla" SQL. There are however, a few extensions to SQL to make recursive queries possible.
For example, If I have a relation "graph":
src | target
-----+--------
1 | 2
2 | 3
3 | 4
5 | 6
6 | 7
I can define a new table "closure" with the same fields:
WITH RECURSIVE closure (src, target) AS
(SELECT src, target FROM
graph
UNION
SELECT graph.src, closure.target FROM graph, closure WHERE
graph.target = closure.src)
SELECT * FROM closure
Note that "closure" occurs in its own definition (that is why this is a recursive query) It uses the original graph as a "seed" and grows by adding tuples with increasing distance (inspecting itself to do so).
The result (it clearly shows how the relation has grown):
src | target
-----+--------
1 | 2
2 | 3
3 | 4
5 | 6
6 | 7
1 | 3
2 | 4
5 | 7
1 | 4
If you are only interested in pairs that cannot be extended further, as in your original example, you could add an extra field "distance" to the closure table and use a GROUP BY clause to keep only the maximal pairs.
Disclaimer: I'm not on Windows, and used postgres for this. MS SQL should work very much the same way.
try below sql:
Assumming #input is your input table
Note: I added an ID column in the #input table
declare #input table
(
id int identity,
RoomId int,
ConnectingRoomId int
)
insert into #input
select 154,0 union all
select 155,154 union all
select 156,155 union all
select 157,156 union all
select 158, 0 union all
select 159, 158 union all
select 160, 159
**UPDATED: remove the union **
SQL:
select
d.id,
d.roomId
,max(d.connectingRoomId) as ConnectingRoomId
from
(
select
bb.id,
bb.RoomId
,b.RoomId as connectingRoomId
from #input b
right join
(
select
a.id,
a.RoomId,a.ConnectingRoomId
from #input a
) bb on (b.id < bb.Id) or b.Id = bb.Id
where b.ConnectingRoomId = 0
) d
group by d.id, d.RoomId
/*
Result (OUTPUT TABLE)
id roomId ConnectingRoomId
----------- ----------- ----------------
1 154 154
2 155 154
3 156 154
4 157 154
5 158 158
6 159 158
7 160 158
*/

How to get second parent with recursive query in Common Table

I am using SQL Server 2008. I have a table like this:
UnitId ParentId UnitName
---------------------------
1 0 FirstUnit
2 1 SecondUnit One
3 1 SecondUnit Two
4 3 B
5 2 C
6 4 D
7 6 E
8 5 F
I want to get second parent of the record. For example:
If I choose unit id that equal to 8, It will bring unit id is equal to 2 to me. It needs to be SecondUnit One. or If I choose unit id that equal to 7, It will bring unit id is equal to 3 to me. It needs to be SecondUnit Two.
How can I write a SQL query this way?
It took me a while, but here it is :)
with tmp as (
select unitId, parentId, unitName, 0 as iteration
from t
where unitId = 7
union all
select parent.unitId, parent.parentId, parent.unitName, child.iteration + 1
from tmp child
join t parent on child.parentId = parent.unitId
where parent.parentId != 0
)
select top 1 unitId, parentId, unitName from tmp
order by iteration desc
Here is also a fiddle to play with.
SELECT t.*, tParent1.UnitId [FirstParent], tParent2.UnitId [SecondParent]
FROM Table t
LEFT JOIN Table tParent1 ON t.ParentId = tParent1.UnitId
LEFT JOIN Table tParent2 ON tParent1.ParentId = tParent2.UnitId
WHERE t.UnitId = <Unit ID search here>
AND NOT tParent2.UnitId IS NULL
Edit: And leave out second part of the WHERE clause if you want results returned even if they don't have a second parent.