Multiply quantities for all parent child relationships - sql

I have a table kind of this.
======================================
ID | Description|Quantity| Parentid|
=====================================
1 | Main | NULL | NULL |
2 | Sub | 20 | 1 |
3 | Sub2 | 21 | 1 |
4 | A1 | 32 | 2 |
5 | B1 | 51 | 3 |
6 | B2 | 43 | 3 |
7 | C1 | 34 | 4 |
9 | D1 | 22 | 5 |
10 | D2 | 90 | 5 |
11 | E1 | 21 | 7 |
12 | F1 | 2 | 11 |
13 | F2 | 42 | 11 |
14 | G1 | 12 | 13 |
-------------------------------------
I want total quantity of G1.. parent of G1 is F2. parent of F2 is E1 . parent of E1 is C1. parent of C1 is A1. parent of A1 is Sub. Parent of Sub is Main. so the total quantity of G1 is (12*42*21*34*32*20=230307840).
How to get that answer with sql query?

WITH TotalQuantity AS
(
SELECT Quantity, ParentID
FROM MyTable
WHERE Description = 'G1'
UNION ALL
SELECT TQ.Quantity * COALESCE(T.Quantity,1), T.ParentID
FROM TotalQuantity TQ
INNER JOIN MyTable T ON T.ID = TQ.ParentID
)
SELECT * FROM TotalQuantity
WHERE ParentID IS NULL

This will give the increasing totals for each generation.
WITH Hierarchy(ChildId, Description, Quantity, Generation, ParentId)
AS
(
SELECT Id, Description, Quantity, 0 as Generation, ParentId
FROM Table1 AS FirtGeneration
WHERE ParentId IS NULL
UNION ALL
SELECT NextGeneration.Id, NextGeneration.Description,
ISNULL(NextGeneration.Quantity, 1) * ISNULL(Parent.Quantity, 1),
Parent.Generation + 1, Parent.ChildId
FROM Table1 AS NextGeneration
INNER JOIN Hierarchy AS Parent ON NextGeneration.ParentId = Parent.ChildId
)
SELECT *
FROM Hierarchy
For G1 simply
select quantity from Hierarchy where description = 'G1' -- result = 230307840
SQL Fiddle

Related

Avoiding double counting values when aggregating tables used in joins

I have two tables that have a parent/child relationship, where one record in the parent table may correspond to N records in the child. Both tables have an amount column that I want to aggregate in one query, so I can see the total amount for the parent and the children.
When I join the tables together, the parent amounts will be counted multiple times for each child resulting in the parent aggregate values to be incorrect.
Here is a simplified version of the problem that has the bad results and my desired results.
drop table if exists parent;
CREATE TABLE parent (
id numeric,
amount numeric,
person text
);
drop table if exists child;
CREATE TABLE child (
id numeric,
parentId numeric,
amount numeric,
person text
);
insert into parent (id, amount, person) values
(1, 5, 'P1'),
(2, 15, 'P1'),
(3, 5, 'P2'),
(4, 20, 'P2');
insert into child (id, parentId, amount) values
(1, 1, 3),
(2, 1, 5),
(3, 2, 10),
(4, 3, 6),
(5, 4, 12),
(5, 4, 8);
-- Parent is double counted for each child joined onto
select
p.person,
p.id,
sum(p.amount) as parent_sum,
sum(c.amount) as child_sum
from
parent p
left outer join child as c on
c.parentId = p.id
group by rollup (p.person, p.id)
order by (p.person, p.id)
/*
Output:
| person | id | parent_sum | child_sum |
|--------|--------|------------|-----------|
| P1 | 1 | 10 | 8 |
| P1 | 2 | 15 | 10 |
| P1 | (null) | 25 | 18 |
| P2 | 3 | 5 | 6 |
| P2 | 4 | 40 | 20 |
| P2 | (null) | 45 | 26 |
| (null) | (null) | 70 | 44 |
Desired output:
| person | id | parent_sum | child_sum |
|--------|--------|------------|-----------|
| P1 | 1 | 5 | 8 |
| P1 | 2 | 15 | 10 |
| P1 | (null) | 20 | 18 |
| P2 | 3 | 5 | 6 |
| P2 | 4 | 20 | 20 |
| P2 | (null) | 25 | 26 |
| (null) | (null) | 45 | 44 |
*/
Here is an sql fiddle showing this: http://sqlfiddle.com/#!17/c4af6/2
I think I might be able to get this to work using a window function but I am looking for a better solution that is as performant as possible. Any ideas on this?
You can sum first up and then join, the results
SQL Fiddle
Query 1:
-- Parent is double counted for each child joined onto
select
p.person,
p.id,
SUM(parent_sum) as parent_sum,
SUM(child_sum) as child_sum
from
(SELECT p.person,
p.id,
sum(p.amount) as parent_sum FROM parent p
GROUP BY p.person,
p.id) p
left outer join (SELECT
c.parentId,
sum(c.amount) as child_sum FROM child c
GROUP BY c.parentId) c on
c.parentId = p.id
group by rollup (p.person, p.id)
order by (p.person, p.id)
Results:
| person | id | parent_sum | child_sum |
|--------|--------|------------|-----------|
| P1 | 1 | 5 | 8 |
| P1 | 2 | 15 | 10 |
| P1 | (null) | 20 | 18 |
| P2 | 3 | 5 | 6 |
| P2 | 4 | 20 | 20 |
| P2 | (null) | 25 | 26 |
| (null) | (null) | 45 | 44 |
Using rollup will get you the extra rows but I think you have to do the summary a bit manually like this:
select p.person, p.id,
coalesce(
case when grouping(p.id) = 0 then min(p.amount) end,
case when grouping(p.id) = 1 then sum(case when grouping(p.id) = 0 then min(p.amount) end) over (partition by p.person) end,
case when grouping(p.person) = 1 then sum(case when grouping(p.id) = 0 then min(p.amount) end) over () end,
0
) as parent_sum,
sum(c.amount) as child_sum
from parent p left outer join child as c on c.parentId = p.id
group by rollup(p.person, p.id)
order by p.person, p.id;
You should be able to extend the pattern to deeper levels if necessary.
https://dbfiddle.uk/CxD1n2T1

Create column based on multiple possible tables using SQL

We want to add the column doc_val to the Main table such that the value will be the val of the table determined by the doc_type at the row determined by doc_id.
For example, the first value of val will be 57 because in the first row of Main the doc_type is 'doc_a' and the doc_id is 1 and the val of id 1 in table A is 57.
Table Main
id | doc_type | doc_id
----------------------
1 | 'doc_a' | 1
2 | 'doc_a' | 2
3 | 'doc_b' | 3
4 | 'doc_c' | 1
5 | 'doc_c' | 3
Table A Table B Table C
id | val id | val id | val
-------- -------- --------
1 | 57 1 | 33 1 | 55
2 | 63 2 | 26 2 | 22
3 | 72 3 | 19 3 | 54
The table we want is below.
id | doc_type | doc_id | doc_val
--------------------------------
1 | 'doc_a' | 1 | 57
2 | 'doc_a' | 2 | 63
3 | 'doc_b' | 3 | 19
4 | 'doc_c' | 1 | 55
5 | 'doc_c' | 3 | 54
How can we do this with an SQL query?
You can use left join:
select m.*, coalesce(a.val, b.val, c.val) as val
from main m left join
a
on m.doc_id = a.id and m.doc_type = 'doc_a' left join
b
on m.doc_id = b.id and m.doc_type = 'doc_b' left join
c
on m.doc_id = c.id and m.doc_type = 'doc_c';
An alternative method (which is probably less performant) uses union all:
select m.*, abc.val
from main left join
((select id, 'doc_a' as doc_type, val from a
) union all
(select id, 'doc_b' as doc_type, val from b
) union all
(select id, 'doc_c' as doc_type, val from c
)
) abc
on m.id = abc.id and m.doc_type = abc.doc_type;

PostgreSQL can't make Self Join

I have a table:
| acctg_cath_id | parent | description |
| 1 | 20 | Bills |
| 9 | 20 | Invoices |
| 20 | | Expenses |
| 88 | 30 |
| 89 | 30 |
| 30 | |
And I want to create a self join in order to group my items under a parent.
Have tried this, but it doesn't work:
SELECT
accounting.categories.acctg_cath_id,
accounting.categories.parent
FROM accounting.categories a1, accounting.categories a2
WHERE a1.acctg_cath_id=a2.parent
I get error: invalid reference to FROM-clause entry for table "categories"
When I try:
a.accounting.categories.acctg_cath_id
b.accounting.categories.acctg_cath_id
I get error: cross-database references are not implemented: a.accounting.categories.acctg_cath_id
Desired output:
Expenses (Parent 20)
Bills (Child 1)
Invoices (Child 9)
What am I doing wrong here?
It seems you merely want to sort the rows:
select *
from accounting.categorie
order by coalesce(parent, acctg_cath_id), parent nulls first, acctg_cath_id;
Result:
+---------------+--------+-------------+
| acctg_cath_id | parent | description |
+---------------+--------+-------------+
| 20 | | Expenses |
| 1 | 20 | Bills |
| 9 | 20 | Invoices |
| 30 | | |
| 88 | 30 | |
| 89 | 30 | |
+---------------+--------+-------------+
Your syntax is performing a cross join:
FROM accounting.categories a1, accounting.categories a2
Try the following:
SELECT
a2.acctg_cath_id,
a2.parent
FROM accounting.categories a1
JOIN accounting.categories a2 ON (a1.acctg_cath_id = a2.parent)
;
Examine the DBFiddle.
You don't need grouping, only self join:
select
c.acctg_cath_id parentid, c.description parent,
cc.acctg_cath_id childid, cc.description child
from (
select distinct parent
from categories
) p inner join categories c
on p.parent = c.acctg_cath_id
inner join categories cc on cc.parent = p.parent
where p.parent = 20
You can remove the WHERE clause if you want all the parents with all their children.
See the demo.
Results:
> parentid | parent | childid | child
> -------: | :------- | ------: | :-------
> 20 | Expences | 1 | Bills
> 20 | Expences | 9 | Invoices
You don't need a self-join. You don't need aggregation. You just need a group by clause:
SELECT ac.*
FROM accounting.categories ac
ORDER BY COALESCE(ac.parent, ac.acctg_cath_id),
(CASE WHEN ac.parent IS NULL THEN 1 ELSE 2 END),
ac.acctg_cath_id;

Update table with ordered values

i need to update a table ordering by price and reassigning the ordered price.
The price and values are grouped by idcategory. Here is an example:
| ID | idcategory | price | value |
| 1 | 1 | 10 | 3 |
| 2 | 1 | 12 | 30 |
| 3 | 1 | 43 | 9 |
| 4 | 1 | 32 | 23 |
| 5 | 2 | 38 | 13 |
| 6 | 2 | 8 | 26 |
| 7 | 2 | 3 | 34 |
| 8 | 2 | 10 | 12 |
. .. .. .. .. .. .. ... ... .. .. .. ..
I need to reorder the table grouping by idcategory reassigning the ordered value to the ordered price like this:
| ID | idcategory | price | value |
| 1 | 1 | 10 | 3 |
| 2 | 1 | 12 | 9 |
| 3 | 1 | 32 | 23 |
| 4 | 1 | 43 | 30 |
| 5 | 2 | 3 | 12 |
| 6 | 2 | 8 | 13 |
| 7 | 2 | 10 | 26 |
| 8 | 2 | 38 | 34 |
.. .. .. .. .. .. .. .. .. ... ..
database is a postgres 9.2.
any idea will be appreciated.
Thanks you and Happy new Year !!!
this is the updated working solution based on GarethD suggestion:
WITH OrderedValues AS
( SELECT Value,
Price,
idcategory,
ROW_NUMBER() OVER(PARTITION BY idcategory ORDER BY Value) AS ValueNum,
ROW_NUMBER() OVER(PARTITION BY idcategory ORDER BY Price) AS PriceNum
FROM T
), OrderedIDs AS
( SELECT ID,
idcategory,
ROW_NUMBER() OVER(PARTITION BY idcategory ORDER BY ID) AS RowNum
FROM T
), NewValues AS
( SELECT i.ID,
v.Value,
p.Price
FROM OrderedIDs i
INNER JOIN OrderedValues v
ON i.RowNum = v.ValueNum
AND i.idcategory = v.idcategory
INNER JOIN OrderedValues p
ON i.RowNum = p.PriceNum
AND i.idcategory = p.idcategory
)
UPDATE T
SET Price = v.Price,
Value = v.Value
FROM NewValues v
WHERE v.ID = T.ID;
SELECT *
FROM T;
You first need to rank your both your IDs (OrderedIDs), and your Price/Value combination (OrderedValues). Then you can matched the corresponding ranks (NewValues), and update your table accordingly:
WITH OrderedValues AS
( SELECT Value,
Price,
idcategory,
ROW_NUMBER() OVER(PARTITION BY idcategory ORDER BY Value, Price) AS RowNum
FROM T
), OrderedIDs AS
( SELECT ID,
idcategory,
ROW_NUMBER() OVER(PARTITION BY idcategory ORDER BY ID) AS RowNum
FROM T
), NewValues AS
( SELECT i.ID,
v.Value,
v.Price
FROM OrderedIDs i
INNER JOIN OrderedValues v
ON i.RowNum = v.RowNum
AND i.idcategory = v.idcategory
)
UPDATE T
SET Price = v.Price,
Value = v.Value
FROM NewValues v
WHERE v.ID = T.ID;
Example on SQL Fiddle

Sub-sub-selects and grouping: Get name column from the row containing the max value of a group

I have two tables: States, and Items.
States:
+----+------+-------+----------+
| id | name | state | priority |
+----+------+-------+----------+
| 1 | AA | 10 | 1 |
| 2 | AB | 10 | 2 |
| 3 | AC | 10 | 3 |
| 4 | BA | 20 | 1 |
| 5 | BB | 20 | 5 |
| 6 | BC | 20 | 10 |
| 7 | BD | 20 | 50 |
+----+------+-------+----------+
Items:
+----+--------+-------+
| id | item | state |
+----+--------+-------+
| 1 | Blue | 10 |
| 2 | Red | 20 |
| 3 | Green | 20 |
| 4 | Yellow | 10 |
| 5 | Brown | 10 |
+----+--------+-------+
The priority column is not used in the Items table, but complicates getting the data I need, as shown below.
What I want is a list of the rows in the Items table, replacing the state.id value in each row with the name of the highest priority state.
Results would look like this:
+----+--------+-------+
| id | item | state |
+----+--------+-------+
| 1 | Blue | AC |
| 2 | Red | BD |
| 3 | Green | BD |
| 4 | Yellow | AC |
| 5 | Brown | AC |
+----+--------+-------+
Here's the tiny monster I've come up with. Is this the best way, or can I be more efficient / less verbose? (Sub-sub-selects make my palms itch. :-P )
SELECT *
FROM
Items AS itm
INNER JOIN (SELECT sta.name, sta.state
FROM (SELECT state, MAX(priority) [highest]
FROM States
GROUP BY state) AS pri
INNER JOIN States AS sta
ON sta.state = pri.state
AND sta.priority = pri.highest) AS nam
ON item.state = name.state
Update: I'm using MS-SQL 2005 and MS-SQL 2008R2
You did not post your version of SQL-Server. Assuming you are on 2005 or later you can use the ROW_NUMBER() function together with a cross apply like this:
CREATE TABLE dbo.States(id INT, name NVARCHAR(25), state INT, priority INT);
INSERT INTO dbo.States
VALUES
( 1 ,'AA', 10 , 1 ),
( 2 ,'AB', 10 , 2 ),
( 3 ,'AC', 10 , 3 ),
( 4 ,'BA', 20 , 1 ),
( 5 ,'BB', 20 , 5 ),
( 6 ,'BC', 20 , 10 ),
( 7 ,'BD', 20 , 50 );
CREATE TABLE dbo.Items( id INT ,item NVARCHAR(25), state INT );
INSERT INTO dbo.Items
VALUES
( 1 ,'Blue', 10 ),
( 2 ,'Red', 20 ),
( 3 ,'Green', 20 ),
( 4 ,'Yellow', 10 ),
( 5 ,'Brown', 10 );
SELECT i.id,
i.item,
s.name,
s.priority
FROM dbo.Items i
CROSS APPLY (
SELECT *,ROW_NUMBER()OVER(ORDER BY priority DESC) rn FROM dbo.States si WHERE si.state = i.state
)s
WHERE s.rn = 1;
The cross apply works like a join but allows to reference columns on the left side in the right side as you can see in the where clause. The ROW_NUMBER() function numbers all rows in the states table that match the current state value in reverse priority order so that the row with the highest priority always gets the number 1. The final where clause is filtering out just those rows.
EDIT:
I just started a blog series about joins: A Join A Day
The Cross Apply will be topic of day 8 (12/8/2012).