Subtracting value from parent table with SUM(value from child table) - sql

I have 2 tables, tblBasicInfo and tblPayment.
Relationship is 1 to many, where tblBasicInfo is on the 1 side, and tblPayment is on the many side.
Relationship is optional and that is the problem.
I need to subtract value of certain field from parent table with sum of certain fields from child table that match certain criteria.
If there are no records in child table that fulfill the criteria then this should be represented with zero ( data from parent table - 0 ).
I apologize if this is not crystal clear, English is not my native and I am not experienced enough to know how to properly describe the problem.
It would be best to demonstrate what I mean with a small example:
We shall start from table schema:
tblBasicInfo: #ID, TotalPrice (double)
tblPayment: #P_ID, $ID, Amount (double), IsPaid (bool)
Here is the content for parent table tblBasicInfo:
ID | TotalPrice
1 | 100
2 | 150
3 | 200
4 | 250
Here is the content for child table tblPayment:
P_ID | ID | IsPaid | Amount
1 | 1 | true | 50
2 | 1 | false | 25
3 | 2 | false | 100
4 | 2 | false | 25
5 | 3 | true | 200
This is what I have accomplished on my own:
SELECT tblBasicInfo.ID,
( tblBasicInfo.TotalPrice - sum(tblPayment.Amount) ) AS [Difference]
FROM tblBasicInfo, tblPayment
WHERE ( tblBasicInfo.ID = tblPayment.ID )
GROUP BY tblBasicInfo.TotalPrice, tblPayment.IsPaid
HAVING ( tblPayment.IsPaid = TRUE ) --this is the criteria I talked above
ORDER BY tblBasicInfo.ID;
This is what I get from the above query:
ID | Difference
1 | 50
3 | 0
.
.
.
I need to get the following result:
ID | Difference
1 | 50
2 | 150 -- does not meet the criteria ( IsPayed = false )
3 | 0
4 | 250 -- no records in child table
.
.
.
I apologize for imperfect title of the question, but I really did not know how to describe this problem.

I tried this on SQL Server, but you can achieve same in other RDMS you can achieve this in probably more than one way here I presented two solutions I found that first solution performs better than second
SELECT ti.id,MAX(totalprice) - ISNULL(SUM(CASE WHEN is_payed = ((0)) THEN 0 ELSE amount END),0) amount
FROM tblbasicinfo ti LEFT OUTER JOIN tblpayment tp ON ti.id = tp.p_id
GROUP BY ti.id
--OR
SELECT id,totalprice-ISNULL((SELECT SUM(amount)
FROM tblpayment tp
WHERE ti.id = tp.p_id AND is_payed = ((1))
GROUP BY id),0) AS reconsile
FROM tblbasicinfo ti
CREATE TABLE tblBasicInfo (id INT IDENTITY(1,1),totalprice MONEY)
CREATE TABLE tblPayment (id INT IDENTITY(1,1), P_ID INT ,is_payed BIT,amount MONEY)
INSERT INTO tblbasicinfo
VALUES(100),(150),(200),(250)
INSERT INTO tblpayment(p_id,is_payed,amount)
VALUES(1,((1)),50),(1,((0)),25),(2,((0)),100),(2,((0)),25),(3,((1)),200)

try this
select a.Id,(a.TotalPrice-payment.paid) as Difference from tblBasicInfo a
left join
(
select sum(Amount) as paid,Id
from
tblPayment
group by Id
where IsPaid =1)payment
on a.Id=payment.Id

(minor correction - IsPaid rather than IsPayed)
This isn't tested or anything it is just to point you in the right direction hopefully.
You want to use a left join and then check to see if amount is null in your calculation of difference
SELECT
bi.ID,
( bi.TotalPrice - sum(IIF(p.Amount is null,0,p.Amount)) ) AS [Difference]
FROM tblBasicInfo bi,
left join tblPayment p
on p.id = bi.id
and p.IsPaid = 1
GROUP BY bi.ID, bi.TotalPrice
ORDER BY bi.ID;

Related

How do I transform the specific row value into column headers in hive [duplicate]

I tried to search posts, but I only found solutions for SQL Server/Access. I need a solution in MySQL (5.X).
I have a table (called history) with 3 columns: hostid, itemname, itemvalue.
If I do a select (select * from history), it will return
+--------+----------+-----------+
| hostid | itemname | itemvalue |
+--------+----------+-----------+
| 1 | A | 10 |
+--------+----------+-----------+
| 1 | B | 3 |
+--------+----------+-----------+
| 2 | A | 9 |
+--------+----------+-----------+
| 2 | C | 40 |
+--------+----------+-----------+
How do I query the database to return something like
+--------+------+-----+-----+
| hostid | A | B | C |
+--------+------+-----+-----+
| 1 | 10 | 3 | 0 |
+--------+------+-----+-----+
| 2 | 9 | 0 | 40 |
+--------+------+-----+-----+
I'm going to add a somewhat longer and more detailed explanation of the steps to take to solve this problem. I apologize if it's too long.
I'll start out with the base you've given and use it to define a couple of terms that I'll use for the rest of this post. This will be the base table:
select * from history;
+--------+----------+-----------+
| hostid | itemname | itemvalue |
+--------+----------+-----------+
| 1 | A | 10 |
| 1 | B | 3 |
| 2 | A | 9 |
| 2 | C | 40 |
+--------+----------+-----------+
This will be our goal, the pretty pivot table:
select * from history_itemvalue_pivot;
+--------+------+------+------+
| hostid | A | B | C |
+--------+------+------+------+
| 1 | 10 | 3 | 0 |
| 2 | 9 | 0 | 40 |
+--------+------+------+------+
Values in the history.hostid column will become y-values in the pivot table. Values in the history.itemname column will become x-values (for obvious reasons).
When I have to solve the problem of creating a pivot table, I tackle it using a three-step process (with an optional fourth step):
select the columns of interest, i.e. y-values and x-values
extend the base table with extra columns -- one for each x-value
group and aggregate the extended table -- one group for each y-value
(optional) prettify the aggregated table
Let's apply these steps to your problem and see what we get:
Step 1: select columns of interest. In the desired result, hostid provides the y-values and itemname provides the x-values.
Step 2: extend the base table with extra columns. We typically need one column per x-value. Recall that our x-value column is itemname:
create view history_extended as (
select
history.*,
case when itemname = "A" then itemvalue end as A,
case when itemname = "B" then itemvalue end as B,
case when itemname = "C" then itemvalue end as C
from history
);
select * from history_extended;
+--------+----------+-----------+------+------+------+
| hostid | itemname | itemvalue | A | B | C |
+--------+----------+-----------+------+------+------+
| 1 | A | 10 | 10 | NULL | NULL |
| 1 | B | 3 | NULL | 3 | NULL |
| 2 | A | 9 | 9 | NULL | NULL |
| 2 | C | 40 | NULL | NULL | 40 |
+--------+----------+-----------+------+------+------+
Note that we didn't change the number of rows -- we just added extra columns. Also note the pattern of NULLs -- a row with itemname = "A" has a non-null value for new column A, and null values for the other new columns.
Step 3: group and aggregate the extended table. We need to group by hostid, since it provides the y-values:
create view history_itemvalue_pivot as (
select
hostid,
sum(A) as A,
sum(B) as B,
sum(C) as C
from history_extended
group by hostid
);
select * from history_itemvalue_pivot;
+--------+------+------+------+
| hostid | A | B | C |
+--------+------+------+------+
| 1 | 10 | 3 | NULL |
| 2 | 9 | NULL | 40 |
+--------+------+------+------+
(Note that we now have one row per y-value.) Okay, we're almost there! We just need to get rid of those ugly NULLs.
Step 4: prettify. We're just going to replace any null values with zeroes so the result set is nicer to look at:
create view history_itemvalue_pivot_pretty as (
select
hostid,
coalesce(A, 0) as A,
coalesce(B, 0) as B,
coalesce(C, 0) as C
from history_itemvalue_pivot
);
select * from history_itemvalue_pivot_pretty;
+--------+------+------+------+
| hostid | A | B | C |
+--------+------+------+------+
| 1 | 10 | 3 | 0 |
| 2 | 9 | 0 | 40 |
+--------+------+------+------+
And we're done -- we've built a nice, pretty pivot table using MySQL.
Considerations when applying this procedure:
what value to use in the extra columns. I used itemvalue in this example
what "neutral" value to use in the extra columns. I used NULL, but it could also be 0 or "", depending on your exact situation
what aggregate function to use when grouping. I used sum, but count and max are also often used (max is often used when building one-row "objects" that had been spread across many rows)
using multiple columns for y-values. This solution isn't limited to using a single column for the y-values -- just plug the extra columns into the group by clause (and don't forget to select them)
Known limitations:
this solution doesn't allow n columns in the pivot table -- each pivot column needs to be manually added when extending the base table. So for 5 or 10 x-values, this solution is nice. For 100, not so nice. There are some solutions with stored procedures generating a query, but they're ugly and difficult to get right. I currently don't know of a good way to solve this problem when the pivot table needs to have lots of columns.
SELECT
hostid,
sum( if( itemname = 'A', itemvalue, 0 ) ) AS A,
sum( if( itemname = 'B', itemvalue, 0 ) ) AS B,
sum( if( itemname = 'C', itemvalue, 0 ) ) AS C
FROM
bob
GROUP BY
hostid;
Another option,especially useful if you have many items you need to pivot is to let mysql build the query for you:
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'ifnull(SUM(case when itemname = ''',
itemname,
''' then itemvalue end),0) AS `',
itemname, '`'
)
) INTO #sql
FROM
history;
SET #sql = CONCAT('SELECT hostid, ', #sql, '
FROM history
GROUP BY hostid');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
FIDDLE
Added some extra values to see it working
GROUP_CONCAT has a default value of 1000 so if you have a really big query change this parameter before running it
SET SESSION group_concat_max_len = 1000000;
Test:
DROP TABLE IF EXISTS history;
CREATE TABLE history
(hostid INT,
itemname VARCHAR(5),
itemvalue INT);
INSERT INTO history VALUES(1,'A',10),(1,'B',3),(2,'A',9),
(2,'C',40),(2,'D',5),
(3,'A',14),(3,'B',67),(3,'D',8);
hostid A B C D
1 10 3 0 0
2 9 0 40 5
3 14 67 0 8
Taking advantage of Matt Fenwick's idea that helped me to solve the problem (a lot of thanks), let's reduce it to only one query:
select
history.*,
coalesce(sum(case when itemname = "A" then itemvalue end), 0) as A,
coalesce(sum(case when itemname = "B" then itemvalue end), 0) as B,
coalesce(sum(case when itemname = "C" then itemvalue end), 0) as C
from history
group by hostid
I edit Agung Sagita's answer from subquery to join.
I'm not sure about how much difference between this 2 way, but just for another reference.
SELECT hostid, T2.VALUE AS A, T3.VALUE AS B, T4.VALUE AS C
FROM TableTest AS T1
LEFT JOIN TableTest T2 ON T2.hostid=T1.hostid AND T2.ITEMNAME='A'
LEFT JOIN TableTest T3 ON T3.hostid=T1.hostid AND T3.ITEMNAME='B'
LEFT JOIN TableTest T4 ON T4.hostid=T1.hostid AND T4.ITEMNAME='C'
use subquery
SELECT hostid,
(SELECT VALUE FROM TableTest WHERE ITEMNAME='A' AND hostid = t1.hostid) AS A,
(SELECT VALUE FROM TableTest WHERE ITEMNAME='B' AND hostid = t1.hostid) AS B,
(SELECT VALUE FROM TableTest WHERE ITEMNAME='C' AND hostid = t1.hostid) AS C
FROM TableTest AS T1
GROUP BY hostid
but it will be a problem if sub query resulting more than a row, use further aggregate function in the subquery
If you could use MariaDB there is a very very easy solution.
Since MariaDB-10.02 there has been added a new storage engine called CONNECT that can help us to convert the results of another query or table into a pivot table, just like what you want:
You can have a look at the docs.
First of all install the connect storage engine.
Now the pivot column of our table is itemname and the data for each item is located in itemvalue column, so we can have the result pivot table using this query:
create table pivot_table
engine=connect table_type=pivot tabname=history
option_list='PivotCol=itemname,FncCol=itemvalue';
Now we can select what we want from the pivot_table:
select * from pivot_table
More details here
My solution :
select h.hostid, sum(ifnull(h.A,0)) as A, sum(ifnull(h.B,0)) as B, sum(ifnull(h.C,0)) as C from (
select
hostid,
case when itemName = 'A' then itemvalue end as A,
case when itemName = 'B' then itemvalue end as B,
case when itemName = 'C' then itemvalue end as C
from history
) h group by hostid
It produces the expected results in the submitted case.
I make that into Group By hostId then it will show only first row with values,
like:
A B C
1 10
2 3
I figure out one way to make my reports converting rows to columns almost dynamic using simple querys. You can see and test it online here.
The number of columns of query is fixed but the values are dynamic and based on values of rows. You can build it So, I use one query to build the table header and another one to see the values:
SELECT distinct concat('<th>',itemname,'</th>') as column_name_table_header FROM history order by 1;
SELECT
hostid
,(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue else '' end) as col1
,(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue else '' end) as col2
,(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue else '' end) as col3
,(case when itemname = (select distinct itemname from history a order by 1 limit 3,1) then itemvalue else '' end) as col4
FROM history order by 1;
You can summarize it, too:
SELECT
hostid
,sum(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue end) as A
,sum(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue end) as B
,sum(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue end) as C
FROM history group by hostid order by 1;
+--------+------+------+------+
| hostid | A | B | C |
+--------+------+------+------+
| 1 | 10 | 3 | NULL |
| 2 | 9 | NULL | 40 |
+--------+------+------+------+
Results of RexTester:
http://rextester.com/ZSWKS28923
For one real example of use, this report bellow show in columns the hours of departures arrivals of boat/bus with a visual schedule. You will see one additional column not used at the last col without confuse the visualization:
** ticketing system to of sell ticket online and presential
This isn't the exact answer you are looking for but it was a solution that i needed on my project and hope this helps someone. This will list 1 to n row items separated by commas. Group_Concat makes this possible in MySQL.
select
cemetery.cemetery_id as "Cemetery_ID",
GROUP_CONCAT(distinct(names.name)) as "Cemetery_Name",
cemetery.latitude as Latitude,
cemetery.longitude as Longitude,
c.Contact_Info,
d.Direction_Type,
d.Directions
from cemetery
left join cemetery_names on cemetery.cemetery_id = cemetery_names.cemetery_id
left join names on cemetery_names.name_id = names.name_id
left join cemetery_contact on cemetery.cemetery_id = cemetery_contact.cemetery_id
left join
(
select
cemetery_contact.cemetery_id as cID,
group_concat(contacts.name, char(32), phone.number) as Contact_Info
from cemetery_contact
left join contacts on cemetery_contact.contact_id = contacts.contact_id
left join phone on cemetery_contact.contact_id = phone.contact_id
group by cID
)
as c on c.cID = cemetery.cemetery_id
left join
(
select
cemetery_id as dID,
group_concat(direction_type.direction_type) as Direction_Type,
group_concat(directions.value , char(13), char(9)) as Directions
from directions
left join direction_type on directions.type = direction_type.direction_type_id
group by dID
)
as d on d.dID = cemetery.cemetery_id
group by Cemetery_ID
This cemetery has two common names so the names are listed in different rows connected by a single id but two name ids and the query produces something like this
CemeteryID Cemetery_Name Latitude
1 Appleton,Sulpher Springs 35.4276242832293
You can use a couple of LEFT JOINs. Kindly use this code
SELECT t.hostid,
COALESCE(t1.itemvalue, 0) A,
COALESCE(t2.itemvalue, 0) B,
COALESCE(t3.itemvalue, 0) C
FROM history t
LEFT JOIN history t1
ON t1.hostid = t.hostid
AND t1.itemname = 'A'
LEFT JOIN history t2
ON t2.hostid = t.hostid
AND t2.itemname = 'B'
LEFT JOIN history t3
ON t3.hostid = t.hostid
AND t3.itemname = 'C'
GROUP BY t.hostid
I'm sorry to say this and maybe I'm not solving your problem exactly but PostgreSQL is 10 years older than MySQL and is extremely advanced compared to MySQL and there's many ways to achieve this easily. Install PostgreSQL and execute this query
CREATE EXTENSION tablefunc;
then voila! And here's extensive documentation: PostgreSQL: Documentation: 9.1: tablefunc or this query
CREATE EXTENSION hstore;
then again voila! PostgreSQL: Documentation: 9.0: hstore

Could anyone help me with an SQL statement?

Given is the following table:
aID | bID | idx
1 | 1 | 0
2 | 2 | 0
2 | 3 | 1
3 | 1 | 0
4 | 2 | 0
4 | 3 | 1
4 | 4 | 2
Is it possible to get the following outcome only using DELETE and UPDATE statements and not CURSORS?
aID | bID | idx
1 | 1 | 0
2 | 2 | 0
2 | 3 | 1
4 | 4 | 0
Each aID shall have a list of unique bIDs. The idx column just describes the order of the list.
I want to delete all multiple bIDs and only keep the bIDs with the lowest aID. And then update the idx for the remaining rows if needed.
Thank you in advance.
This seems to do the job:
declare #t table (aID int, bID int, idx int)
insert into #t(aID,bID,idx) values
(1,1 ,0 ),
(2,2 ,0 ),
(2,3 ,1 ),
(3,1 ,0 ),
(4,2 ,0 ),
(4,3 ,1 ),
(4,4 ,2 )
delete
t1
from #t t1
where exists (select * from #t t2 where t2.bID = t1.bID and t2.aID < t1.aID)
;With Numbered as (
select *,ROW_NUMBER() OVER (PARTITION BY aID ORDER by bID) -1 as newIdx
from #t
)
update Numbered set idx = newIdx
select * from #t
Once you've spelt out what you're requirements are in words, the logic of the queries flows quite naturally from that. Delete rows where theres another row with the same bID and a lower aID value.
Then idx is just the row numbers based on separate series for each aID. Of course, this does raise the question of why we're storing them at all when they can be so easily calculated1.
Result:
aID bID idx
----------- ----------- -----------
1 1 0
2 2 0
2 3 1
4 4 0
(Note that I've used SQL Server to test this - you didn't apply any product specific tags to the question - The table variable syntax is non-standard and the delete ... from is a bit questionable, but the rest is standard SQL)
1The general rule of thumb is to only store base data, not derived data. By separately storing derived data, you introduce the opportunity for the calculated values to be out-of-sync with the base data.
Second rule of thumb is if you do have to store derived data, ideally have the database engine take responsibility for maintaining it rather than yourself. But usually this is only if the calculation cost is prohibitively high and you're heavily biased towards readers rather than writers.
use co-related sub-query
select t.* from your_table t
where aID = (select min(aID) from your_table t1
where t1.bID=t.bID
)

Comparing different columns in SQL for each row

after some transformation I have a result from a cross join (from table a and b) where I want to do some analysis on. The table for this looks like this:
+-----+------+------+------+------+-----+------+------+------+------+
| id | 10_1 | 10_2 | 11_1 | 11_2 | id | 10_1 | 10_2 | 11_1 | 11_2 |
+-----+------+------+------+------+-----+------+------+------+------+
| 111 | 1 | 0 | 1 | 0 | 222 | 1 | 0 | 1 | 0 |
| 111 | 1 | 0 | 1 | 0 | 333 | 0 | 0 | 0 | 0 |
| 111 | 1 | 0 | 1 | 0 | 444 | 1 | 0 | 1 | 1 |
| 112 | 0 | 1 | 1 | 0 | 222 | 1 | 0 | 1 | 0 |
+-----+------+------+------+------+-----+------+------+------+------+
The ids in the first column are different from the ids in the sixth column.
In a row are always two different IDs that are matched with each other. The other columns always have either 0 or 1 as a value.
I am now trying to find out how many values(meaning both have "1" in 10_1, 10_2 etc) two IDs have on average in common, but I don't really know how to do so.
I was trying something like this as a start:
SELECT SUM(CASE WHEN a.10_1 = 1 AND b.10_1 = 1 then 1 end)
But this would obviously only count how often two ids have 10_1 in common. I could make something like this for example for different columns:
SELECT SUM(CASE WHEN (a.10_1 = 1 AND b.10_1 = 1)
OR (a.10_2 = 1 AND b.10_1 = 1) OR [...] then 1 end)
To count in general how often two IDs have one thing in common, but this would of course also count if they have two or more things in common. Plus, I would also like to know how often two IDS have two things, three things etc in common.
One "problem" in my case is also that I have like ~30 columns I want to look at, so I can hardly write down for each case every possible combination.
Does anyone know how I can approach my problem in a better way?
Thanks in advance.
Edit:
A possible result could look like this:
+-----------+---------+
| in_common | count |
+-----------+---------+
| 0 | 100 |
| 1 | 500 |
| 2 | 1500 |
| 3 | 5000 |
| 4 | 3000 |
+-----------+---------+
With the codes as column names, you're going to have to write some code that explicitly references each column name. To keep that to a minimum, you could write those references in a single union statement that normalizes the data, such as:
select id, '10_1' where "10_1" = 1
union
select id, '10_2' where "10_2" = 1
union
select id, '11_1' where "11_1" = 1
union
select id, '11_2' where "11_2" = 1;
This needs to be modified to include whatever additional columns you need to link up different IDs. For the purpose of this illustration, I assume the following data model
create table p (
id integer not null primary key,
sex character(1) not null,
age integer not null
);
create table t1 (
id integer not null,
code character varying(4) not null,
constraint pk_t1 primary key (id, code)
);
Though your data evidently does not currently resemble this structure, normalizing your data into a form like this would allow you to apply the following solution to summarize your data in the desired form.
select
in_common,
count(*) as count
from (
select
count(*) as in_common
from (
select
a.id as a_id, a.code,
b.id as b_id, b.code
from
(select p.*, t1.code
from p left join t1 on p.id=t1.id
) as a
inner join (select p.*, t1.code
from p left join t1 on p.id=t1.id
) as b on b.sex <> a.sex and b.age between a.age-10 and a.age+10
where
a.id < b.id
and a.code = b.code
) as c
group by
a_id, b_id
) as summ
group by
in_common;
The proposed solution requires first to take one step back from the cross-join table, as the identical column names are super annoying. Instead, we take the ids from the two tables and put them in a temporary table. The following query gets the result wanted in the question. It assumes table_a and table_b from the question are the same and called tbl, but this assumption is not needed and tbl can be replaced by table_a and table_b in the two sub-SELECT queries. It looks complicated and uses the JSON trick to flatten the columns, but it works here:
WITH idtable AS (
SELECT a.id as id_1, b.id as id_2 FROM
-- put cross join of table a and table b here
)
SELECT in_common,
count(*)
FROM
(SELECT idtable.*,
sum(CASE
WHEN meltedR.value::text=meltedL.value::text THEN 1
ELSE 0
END) AS in_common
FROM idtable
JOIN
(SELECT tbl.id,
b.*
FROM tbl, -- change here to table_a
json_each(row_to_json(tbl)) b -- and here too
WHERE KEY<>'id' ) meltedL ON (idtable.id_1 = meltedL.id)
JOIN
(SELECT tbl.id,
b.*
FROM tbl, -- change here to table_b
json_each(row_to_json(tbl)) b -- and here too
WHERE KEY<>'id' ) meltedR ON (idtable.id_2 = meltedR.id
AND meltedL.key = meltedR.key)
GROUP BY idtable.id_1,
idtable.id_2) tt
GROUP BY in_common ORDER BY in_common;
The output here looks like this:
in_common | count
-----------+-------
2 | 2
3 | 1
4 | 1
(3 rows)

Tsql looping father-son relationship between tables

I have a table like this:
table item
(
id int,
quantity float,
father int, -- refer to item itself in case of subitem
)
I need to sum al quantity plus sons quantity like this way:
select i.id, max(i.quantity)+sum(ft.quantity) as quantity
from item i
left join item ft on ft.id=i.id
group by i.id
My trouble is because relationship between father-son is recursive so I would like to sum also his grandfather quantity and so on... and i don't know the maximum deepness, than I can not join many times.
What can i do?
Thank you.
You have to use a recursive CTE. Somthing like this:
;WITH FathersSonsTree
AS
(
SELECT Id, quantity, 0 AS Level
FROM Items WHERE fatherid IS NULL
UNION ALL
SELECT c.id, c.quantity, p.level+1
FROM FathersSonsTree p
INNER JOIN items c ON c.fatherid = p.id
), ItemsWithMaxQuantities
AS
(
SELECT *,
ROW_NUMBER() OVER(PARTITION BY level
ORDER BY quantity DESC) rownum
FROM FathersSonsTree
)
SELECT
ID,
(SELECT MAX(Quantity)
FROM FathersSonsTree t3
WHERE t3.level = t1.level
) +
ISNULL((SELECT SUM(t2.Quantity)
FROM FathersSonsTree t2
WHERE t1.level - t2.level = 1), 0)
FROM FathersSonsTree t1
ORDER BY ID;
SQL Fiddle Demo
This will give you something like:
| ID | QUANTITY |
-----------------
| 1 | 10 |
| 2 | 20 |
| 3 | 20 |
| 4 | 20 |
| 5 | 32 |
| 6 | 32 |
| 7 | 32 |
| 8 | 32 |
You might try building a recursive CTE (common table expression) as described in this article on SQLAuthority:
http://blog.sqlauthority.com/2012/04/24/sql-server-introduction-to-hierarchical-query-using-a-recursive-cte-a-primer/
The author, Pinal Dave, discusses using a recursive CTE on an employees table that has a self referencing foreign key for ManagerID to return a list of employees with a count of how many levels are between them and the top of the hierarchy where the employee has no manager (ManagerID = NULL). That's not exactly what you're wanting but it might get you started.
I did a little experimentation and ended up with something very similar to Mahmoud Gamal's solution but with a slight difference to include the not just the parent, grandparents, great-grandparents, etc. quantity but also the child quantity.
Here's the test table I used:
CREATE TABLE Items(ID int IDENTITY
CONSTRAINT PK_Items PRIMARY KEY,
Quantity int NOT NULL,
ParentID int NULL
CONSTRAINT FK_Item_Parents REFERENCES Items(ID));
And the data:
ID Quantity ParentID
------------------------------------------------------------
1 10 {NULL}
2 10 1
3 10 2
4 10 3
5 10 2
Here's my recursive query:
WITH cteRecursiveItems
AS (SELECT Id,
quantity,
0
AS Level
FROM Items
WHERE ParentID IS NULL
UNION ALL
SELECT i.id,
i.quantity,
cri.level + 1
FROM
cteRecursiveItems cri
INNER JOIN items i ON i.ParentID = cri.id)
SELECT ID,
Quantity + (
SELECT MAX(Quantity)
FROM cteRecursiveItems cri3
WHERE cri3.level = cri1.level) + (
SELECT SUM(cri2.Quantity)
FROM cteRecursiveItems cri2
WHERE cri1.level - cri2.level = 1) as Total
FROM cteRecursiveItems cri1
ORDER BY ID;
And here's the results I get from running it against the test table:
ID Total
----------------------------------------
1 {NULL}
2 30
3 30
4 40
5 30
It still needs a little tweaking because the first and 2nd row are off by 10. Row 1 should have a total of 10 and row 2 should have a total of 20. I'm making a note to try and fix that when I get home. Can't spend too much of my employer's time on this right now. :) The other rows have the value I was expecting.

Left Join SQL based on criteria that may be missing

I've got several tables that I want to merge for one big query. The criteria for the search are based on two tables: section_types and sections. I want to pull all section_types and their associated sections where the sections match certain criteria, PLUS any section_types that are active but don't have any sections associated. It seems like a basic LEFT JOIN, but I think because some of my filter criteria are based on sections, I can't get section_types that have no associated sections
`section_types`
id | name | active
---+------+-------
1 | a | 1
2 | b | 0
3 | c | 1
`sections`
type | issue | location
-----+-------+----------
1 | 0611 | 1
2 | 0611 | 1
1 | 0511 | 1
Say I want to pull all sections for issue 0611 at location 1, plus any empty section types. Like so:
(edited. see below)
But I'm only getting section_types that have corresponding sections. So in this query, section_types row 3 would not show up. What am I doing wrong?
EDIT:
I'm getting all the section_types now, but not all the sections I need. I guess LEFT JOIN will do that. There can be many sections for each section_type, or none. My query is at this point now:
SELECT * FROM `section_types` st
RIGHT JOIN `sections` s
ON s.type=st.id
AND s.issue='0611'
AND s.location=1
WHERE st.active OR s.issue IS NOT NULL
ORDER BY st.id
which gets me:
id | name | active | issue | location
---+------+--------+-------+---------
1 | a | 1 | 0611 | 1
2 | b | 0 | 0611 | 1
3 | c | 1 | |
but I still need that second type-1 section
EDIT
I deleted this, but based on the conversation, I think it accomplishes what you're looking for.
ORIGINAL
Feels like a hack... but I think it works.
Declare #tmp TABLE(
id int,
name varchar(50),
active int,
type int,
issue int,
location int
)
Insert Into #tmp
SELECT * FROM section_types st
LEFT JOIN sections s
ON st.id=s.type
AND s.issue='0611'
AND s.location=1
WHERE st.active = 1 OR s.issue IS NOT NULL
ORDER BY st.id
Select * FROM #tmp
UNION
Select
*, NULL, NULL, NULL
From
section_types
WHERE
id NOT IN ( SELECT id FROM #tmp)
AND active = 0
Is this what you need?
All section_types and ALL their related sections where at least one section has issue '0611' and location 1. Plus all the rest section_types that are active:
SELECT *
FROM section_types st
JOIN sections s
ON s.type = st.id
WHERE EXISTS
( SELECT *
FROM sections s2
WHERE s2.type = st.id
AND s2.issue = '0611'
AND s2.location = 1
)
UNION ALL
SELECT *, NULL, NULL, NULL
FROM section_types st
WHERE st.active
AND NOT EXISTS
( SELECT *
FROM sections s2
WHERE s2.type = st.id
AND s2.issue = '0611'
AND s2.location = 1
)
ORDER BY id
You just have your tables reversed. LEFT OUTER JOIN requires the left table to have a row for the ON condition. Use a RIGHT OUTER JOIN or swap the tables.