Tsql looping father-son relationship between tables - sql

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.

Related

How can I get last 2 records from another table as columns

I have a table called products with this schema:
CREATE TABLE products (
id INT PRIMARY KEY,
sku TEXT NOT NULL,
fee REAL
);
And another table with fee change log with this schema:
CREATE TABLE fee_change(
id SERIAL PRIMARY KEY,
sku_id INT NOT NULL,
old_fee REAL NOT NULL,
new_fee REAL NOT NULL,
FOREIGN KEY (sku_id) REFERENCES products(id)
);
Is there anyway to get last 2 fee changes for each sku in one sql and not 2 rows for each sku, I want to have 2 new columns with old_fee_1, new _fee_1, old_fee_2, new_fee_2:
Desired result:
id | sku | old_fee_1 | new_fee_1 | old_fee_2 | new_fee_2
1 | ASC | 4 | 2.5 | 3 | 4
2 | CF2 | 4 | 1 | 3 | 4
3 | RTG | 0.5 | 1 | 2 | 0.5
4 | VHN5 | null | null | null | null
dbfiddle
As starting point I took your query from the fiddle you linked:
SELECT *
FROM products AS p
LEFT JOIN LATERAL (
SELECT *
FROM fee_change
WHERE sku_id = p.id
ORDER BY id DESC
LIMIT 2
) AS oo
ON true
demo: db<>fiddle
You can use the FILTER clause (alternatively it works with a CASE WHEN construct as well) to pivot your joined table. To get the pivot value, you can add a row count (using the row_number() window function):
SELECT
p.id, p.sku, p.fee,
MAX(old_fee) FILTER (WHERE row_number = 1) AS old_fee_1, -- 2
MAX(new_fee) FILTER (WHERE row_number = 1) AS new_fee_1,
MAX(old_fee) FILTER (WHERE row_number = 2) AS old_fee_2,
MAX(new_fee) FILTER (WHERE row_number = 2) AS new_fee_2
FROM products AS p
LEFT JOIN LATERAL (
SELECT
*,
row_number() OVER (PARTITION BY sku_id) -- 1
FROM fee_change
WHERE sku_id = p.id
ORDER BY id DESC
LIMIT 2
) AS oo ON true
GROUP BY p.id, p.sku, p.fee -- 2
Create pivot value
Do the filtered aggregation to create the pivoted table.
Something like this should do the trick :
SELECT p.id,
p.sku,
old.old_fee_1,
old.new_fee_1,
new.old_fee_2,
new.new_fee_2
FROM products p
LEFT JOIN (SELECT fee.sku_id id, fee.old_fee old_fee_1, fee.new_fee new_fee_1
FROM fee_change ORDER BY fee.id DESC LIMIT 1 OFFSET 1) old ON old.id = p.id
LEFT JOIN (SELECT fee.sku_id id, fee.old_fee old_fee_2, fee.new_fee new_fee_2
FROM fee_change ORDER BY fee.id DESC LIMIT 1 OFFSET 0) new ON new.id = p.id

Select first rows where condition [duplicate]

Here's what I'm trying to do. Let's say I have this table t:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
2 | 18 | 2012-05-19 | y
3 | 18 | 2012-08-09 | z
4 | 19 | 2009-06-01 | a
5 | 19 | 2011-04-03 | b
6 | 19 | 2011-10-25 | c
7 | 19 | 2012-08-09 | d
For each id, I want to select the row containing the minimum record_date. So I'd get:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
The only solutions I've seen to this problem assume that all record_date entries are distinct, but that is not this case in my data. Using a subquery and an inner join with two conditions would give me duplicate rows for some ids, which I don't want:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
5 | 19 | 2011-04-03 | b
4 | 19 | 2009-06-01 | a
How about something like:
SELECT mt.*
FROM MyTable mt INNER JOIN
(
SELECT id, MIN(record_date) AS MinDate
FROM MyTable
GROUP BY id
) t ON mt.id = t.id AND mt.record_date = t.MinDate
This gets the minimum date per ID, and then gets the values based on those values. The only time you would have duplicates is if there are duplicate minimum record_dates for the same ID.
I could get to your expected result just by doing this in mysql:
SELECT id, min(record_date), other_cols
FROM mytable
GROUP BY id
Does this work for you?
To get the cheapest product in each category, you use the MIN() function in a correlated subquery as follows:
SELECT categoryid,
productid,
productName,
unitprice
FROM products a WHERE unitprice = (
SELECT MIN(unitprice)
FROM products b
WHERE b.categoryid = a.categoryid)
The outer query scans all rows in the products table and returns the products that have unit prices match with the lowest price in each category returned by the correlated subquery.
I would like to add to some of the other answers here, if you don't need the first item but say the second number for example you can use rownumber in a subquery and base your result set off of that.
SELECT * FROM
(
SELECT
ROW_NUM() OVER (PARTITION BY Id ORDER BY record_date, other_cols) as rownum,
*
FROM products P
) INNER
WHERE rownum = 2
This also allows you to order off multiple columns in the subquery which may help if two record_dates have identical values. You can also partition off of multiple columns if needed by delimiting them with a comma
This does it simply:
select t2.id,t2.record_date,t2.other_cols
from (select ROW_NUMBER() over(partition by id order by record_date)as rownum,id,record_date,other_cols from MyTable)t2
where t2.rownum = 1
If record_date has no duplicates within a group:
think of it as of filtering. Simpliy get (WHERE) one (MIN(record_date)) row from the current group:
SELECT * FROM t t1 WHERE record_date = (
select MIN(record_date)
from t t2 where t2.group_id = t1.group_id)
If there could be 2+ min record_date within a group:
filter out non-min rows (see above)
then (AND) pick only one from the 2+ min record_date rows, within the given group_id. E.g. pick the one with the min unique key:
AND key_id = (select MIN(key_id)
from t t3 where t3.record_date = t1.record_date
and t3.group_id = t1.group_id)
so
key_id | group_id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
8 | 19 | 2009-06-01 | e
will select key_ids: #1 and #4
SELECT p.* FROM tbl p
INNER JOIN(
SELECT t.id, MIN(record_date) AS MinDate
FROM tbl t
GROUP BY t.id
) t ON p.id = t.id AND p.record_date = t.MinDate
GROUP BY p.id
This code eliminates duplicate record_date in case there are same ids with same record_date.
If you want duplicates, remove the last line GROUP BY p.id.
This a old question, but this can useful for someone
In my case i can't using a sub query because i have a big query and i need using min() on my result, if i use sub query the db need reexecute my big query. i'm using Mysql
select t.*
from (select m.*, #g := 0
from MyTable m --here i have a big query
order by id, record_date) t
where (1 = case when #g = 0 or #g <> id then 1 else 0 end )
and (#g := id) IS NOT NULL
Basically I ordered the result and then put a variable in order to get only the first record in each group.
The below query takes the first date for each work order (in a table of showing all status changes):
SELECT
WORKORDERNUM,
MIN(DATE)
FROM
WORKORDERS
WHERE
DATE >= to_date('2015-01-01','YYYY-MM-DD')
GROUP BY
WORKORDERNUM
select
department,
min_salary,
(select s1.last_name from staff s1 where s1.salary=s3.min_salary ) lastname
from
(select department, min (salary) min_salary from staff s2 group by s2.department) s3

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

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;

Need to fetch Total Rows Count and Rows count based on Group by in single Query

I have table REQUEST
REQUESTID` | ProductID
----------------------------
1 | 1
2 | 1
3 | 4
4 | 4
5 | 4 `
Now i need output as within single query
ProductID | Count | Total
----------------------------------------------------
1 | 2 | 5
4 | 3 | 5
Basically, i need to calculate Product Percentage amongs total request how many percentage people prefer particular product. And i need this to be done in single query
Code What I have tried ::
Alter Proc SP_Get_Product_History_Count
AS
Declare #Tot bigint
Select #Tot = COUNT(RequestID) from Request
Select
Pro.ProductName,
COUNT(Req.RequestID) /#Tot as Count
From
Request As Req
inner join
Product As Pro
on
Req.ProductID = Pro.ProductID
Group by Pro.ProductName
;WITH x AS
(
SELECT ProductID, total = COUNT(*) OVER()
FROM dbo.REQUEST
)
SELECT
ProductID,
COUNT(*),
MAX(total),
1.0*COUNT(*)/MAX(total)
FROM x
GROUP BY ProductID;
SQLFiddle demo
select productid,
count(requestid) as [Count],
(select count(requestid) from [Request]) as Total
from [Request]
group by productid

Group by minimum value in one field while selecting distinct rows

Here's what I'm trying to do. Let's say I have this table t:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
2 | 18 | 2012-05-19 | y
3 | 18 | 2012-08-09 | z
4 | 19 | 2009-06-01 | a
5 | 19 | 2011-04-03 | b
6 | 19 | 2011-10-25 | c
7 | 19 | 2012-08-09 | d
For each id, I want to select the row containing the minimum record_date. So I'd get:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
The only solutions I've seen to this problem assume that all record_date entries are distinct, but that is not this case in my data. Using a subquery and an inner join with two conditions would give me duplicate rows for some ids, which I don't want:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
5 | 19 | 2011-04-03 | b
4 | 19 | 2009-06-01 | a
How about something like:
SELECT mt.*
FROM MyTable mt INNER JOIN
(
SELECT id, MIN(record_date) AS MinDate
FROM MyTable
GROUP BY id
) t ON mt.id = t.id AND mt.record_date = t.MinDate
This gets the minimum date per ID, and then gets the values based on those values. The only time you would have duplicates is if there are duplicate minimum record_dates for the same ID.
I could get to your expected result just by doing this in mysql:
SELECT id, min(record_date), other_cols
FROM mytable
GROUP BY id
Does this work for you?
To get the cheapest product in each category, you use the MIN() function in a correlated subquery as follows:
SELECT categoryid,
productid,
productName,
unitprice
FROM products a WHERE unitprice = (
SELECT MIN(unitprice)
FROM products b
WHERE b.categoryid = a.categoryid)
The outer query scans all rows in the products table and returns the products that have unit prices match with the lowest price in each category returned by the correlated subquery.
I would like to add to some of the other answers here, if you don't need the first item but say the second number for example you can use rownumber in a subquery and base your result set off of that.
SELECT * FROM
(
SELECT
ROW_NUM() OVER (PARTITION BY Id ORDER BY record_date, other_cols) as rownum,
*
FROM products P
) INNER
WHERE rownum = 2
This also allows you to order off multiple columns in the subquery which may help if two record_dates have identical values. You can also partition off of multiple columns if needed by delimiting them with a comma
This does it simply:
select t2.id,t2.record_date,t2.other_cols
from (select ROW_NUMBER() over(partition by id order by record_date)as rownum,id,record_date,other_cols from MyTable)t2
where t2.rownum = 1
If record_date has no duplicates within a group:
think of it as of filtering. Simpliy get (WHERE) one (MIN(record_date)) row from the current group:
SELECT * FROM t t1 WHERE record_date = (
select MIN(record_date)
from t t2 where t2.group_id = t1.group_id)
If there could be 2+ min record_date within a group:
filter out non-min rows (see above)
then (AND) pick only one from the 2+ min record_date rows, within the given group_id. E.g. pick the one with the min unique key:
AND key_id = (select MIN(key_id)
from t t3 where t3.record_date = t1.record_date
and t3.group_id = t1.group_id)
so
key_id | group_id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
8 | 19 | 2009-06-01 | e
will select key_ids: #1 and #4
SELECT p.* FROM tbl p
INNER JOIN(
SELECT t.id, MIN(record_date) AS MinDate
FROM tbl t
GROUP BY t.id
) t ON p.id = t.id AND p.record_date = t.MinDate
GROUP BY p.id
This code eliminates duplicate record_date in case there are same ids with same record_date.
If you want duplicates, remove the last line GROUP BY p.id.
This a old question, but this can useful for someone
In my case i can't using a sub query because i have a big query and i need using min() on my result, if i use sub query the db need reexecute my big query. i'm using Mysql
select t.*
from (select m.*, #g := 0
from MyTable m --here i have a big query
order by id, record_date) t
where (1 = case when #g = 0 or #g <> id then 1 else 0 end )
and (#g := id) IS NOT NULL
Basically I ordered the result and then put a variable in order to get only the first record in each group.
The below query takes the first date for each work order (in a table of showing all status changes):
SELECT
WORKORDERNUM,
MIN(DATE)
FROM
WORKORDERS
WHERE
DATE >= to_date('2015-01-01','YYYY-MM-DD')
GROUP BY
WORKORDERNUM
select
department,
min_salary,
(select s1.last_name from staff s1 where s1.salary=s3.min_salary ) lastname
from
(select department, min (salary) min_salary from staff s2 group by s2.department) s3