declare #Temp table
(
Grp int,
Bal float,
[Value] float
)
declare #Amt float =1000;
Insert into #Temp(Grp,[Value])
Values(1,10),(1,5),(1,15)
,(2,20),(2,5),(2,15)
,(3,50),(3,50)
select Grp,#Amt as Amount,Value,Bal from #Temp
Required Output:
Grp Amount Value Bal
1 1000 10 1000
1 1000 5 1000
1 1000 15 1000
2 1000 20 1030 ---(10+5+15)
2 1000 5 1030
2 1000 15 1030
3 1000 50 1070 ---- (20+5+15)
3 1000 50 1070
Balance calculated based on running total of 'Value' of Group1 added to Group2 and running total of group 2 added to balance of Group3 and soon
I know how to calculate the running total but I can't as sums are added to next partition.
Please help to get required result efficiently. I am using SQL Server 2017
One method is outer apply:
select t.*, t.amount + coalesce(t2.value, 0)
from #temp t outer apply
(select sum(t2.value) as value
from #temp t2
where t2.grp < t.grp
) t2;
It is possibly more efficient to use aggregation and a running sum:
select t.*,
(1000 + tt.running_value)
from #temp t join
(select t.grp, sum(value) as value,
sum(sum(value)) over (order by grp) - sum(value) as running_value
from #temp t
group by grp
) tt
on t.grp = tt.grp;
Unfortunately, SQL Server doesn't fully support range window frames, so I don't think there is a convenient way to do this only with window functions. But the group by will probably have much better performance.
Related
I am trying to determine how to group records together based the cumulative total of the Qty column so that the group size doesn't exceed 50. The desired group is given in the group column with sample data below.
Is there a way to accomplish this in SQL (specifically SQL Server 2012)?
Thank you for any assistance.
ID Qty Group
1 10 1
2 20 1
3 30 2 <- 60 greater than 50 so new group
4 40 3
5 2 3
6 3 3
7 10 4
8 25 4
9 15 4
10 5 5
You can use CTE to achieve the goal.
If one of the item exceeds Qty 50, a group still assign for it
DECLARE #Data TABLE (ID int identity(1,1) primary key, Qty int)
INSERT #Data VALUES (10), (20), (30), (40), (2), (3), (10), (25), (15), (5)
;WITH cte AS
(
SELECT ID, Qty, 1 AS [Group], Qty AS RunningTotal FROM #Data WHERE ID = 1
UNION ALL
SELECT data.ID, data.Qty,
-- The group limits to 50 Qty
CASE WHEN cte.RunningTotal + data.Qty > 50 THEN cte.[Group] + 1 ELSE cte.[Group] END,
-- Reset the running total for each new group
data.Qty + CASE WHEN cte.RunningTotal + data.Qty > 50 THEN 0 ELSE cte.RunningTotal END
FROM #Data data INNER JOIN cte ON data.ID = cte.ID + 1
)
SELECT ID, Qty, [Group] FROM cte
The following query gives you most of what you want. One more self-join of the result would compute the group sizes:
select a.ID, G, sum(b.Qty) as Total
from (
select max(ID) as ID, G
from (
select a.ID, sum(b.Qty) / 50 as G
from T as a join T as b
where a.ID >= b.ID
group by a.ID
) as A
group by G
) as a join T as b
where a.ID >= b.ID
group by a.ID
ID G Total
---------- ---------- ----------
2 0 30
3 1 60
8 2 140
10 3 160
The two important tricks:
Use a self-join with an inequality to get running totals
Use integer division to calculate group numbers.
I discuss this and other techniques on my canonical SQL page.
You need to create a stored procedure for this.
If you have Group column in your database then you have to take care about it while inserting a new record by fetching the max Group value and its sum of Qty column otherwise if you want Group column as computed in select statement then you have to code stored procedure accordingly.
I have 4 columns- Code, Amount, Start, End. I would like to take the between amounts in the start and end columns and change them into one column with all results. Any suggestions on how to achieve this? Thanks.
Current Results:
Code Amount Start End
1 5000 2015 2016
2 5000 2014 2016
3 20000 2012 2016
Desired Results:
Code Amount StartEnd
1 5000 2015
1 5000 2016
2 5000 2014
2 5000 2015
2 5000 2016
3 20000 2012
3 20000 2013
3 20000 2014
3 20000 2015
3 20000 2016
You can use a recursive cte to generate all the numbers between minimum start and maximum end and join on the generated numbers.
with cte as (select min(start) col,max(end) mx from tablename
union all
select col+1,mx from cte where col < mx)
select t.code,t.amount,c.col
from cte c
join tablename t on c.col between t.start and t.end
or more simply
with cte as (select id,amount,start startend,end from tablename
union all
select id,amount,start+1,end from cte where start<end)
select id,amount,startend
from cte
order by 1,3
You can query like this
SELECT
c.code,
c.amount,
f.yr
FROM #code c
CROSS APPLY fn_yearslist(c.startyr, c.endyr) f
function you cancreate like this
CREATE FUNCTION fn_yearslist (#startyear int, #endyear int)
RETURNS #t TABLE (
yr int
)
AS
BEGIN
WHILE (#startyear <= #endyear)
BEGIN
INSERT INTO #t (yr)
VALUES (#startyear)
SET #startyear += 1
END
RETURN
END
Another option is a UDF. I use this TVF to generate dynamic ranges
Declare #YourTable table (Code int, Amount int, Start int , [End] int)
Insert into #YourTable values
(1,5000 ,2015,2016),
(2,5000 ,2014,2016),
(3,20000,2012,2016)
Select A.Code
,A.Amount
,StartEnd = cast(B.RetVal as int)
From #YourTable A
Cross Apply (Select * from [dbo].[udf-Range-Number](A.Start,A.[End],1)) B
Returns
Code Amount StartEnd
1 5000 2015
1 5000 2016
2 5000 2014
2 5000 2015
2 5000 2016
3 20000 2012
3 20000 2013
3 20000 2014
3 20000 2015
3 20000 2016
The Function
CREATE FUNCTION [dbo].[udf-Range-Number] (#R1 money,#R2 money,#Incr money)
Returns Table
Return (
with cte0(M) As (Select cast((#R2-#R1)/#Incr as int)),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a,cte1 b,cte1 c,cte1 d,cte1 e,cte1 f,cte1 g,cte1 h )
Select RetSeq=1,RetVal=#R1 Union All Select N+1,(N*#Incr)+#R1
From cte2
)
/*
Max 100 million observations --
Syntax:
Select * from [dbo].[udf-Range-Number](0,4,0.25)
*/
I am not sure if it works on SQL 2008, but here's a CTE:
;with sel_cte as (
select Code, Amount, start StartEnd
from #tblTest t
union all
select t.code, t.amount, c.StartEnd + 1 StartEnd
from sel_cte c
inner join #tblTest t on t.code = c.code
where c.StartEnd + 1 <= [end]
)
select *
from sel_cte
order by Code, StartEnd
Note: replace #tblTest with the actual table name.
If the requirements allow you to ONLY have successive numbers (like 2014, 2015, then 2016, etc) then the approach above (cte) would work fine. However, if not, you could create a another temp table (say numbers having 1 column) with numbers in successive sequence of what all you want to be the result output, like.
Number
2014
2015
2016
2018 <-- Missing 2017 and jumping on 2018
2019
And then use a right join to get results in the progressive series with a query similar to the one below.
select Code, StartEnd, Amount from numbers
right join inputs on number between Start and End
I had a table like
ID UserID rupees time
1 1 200 2014-01-05
---------------------------------
2 1 500 2014-04-06
----------------------------------
3 2 10 2014-05-05
----------------------------------
4 2 20 2014-05-06
----------------------------------
I want the output lie
ID UserID Rupees time CumulativeSum
1 1 200 2014-01-05 200
-------------------------------------------------
2 1 500 2014-04-06 700
-------------------------------------------------
3 2 10 2014-05-06 10
-------------------------------------------------
4 2 20 2014-05-06 30
---------------------------------------------------
How can i get this table as purput
Please try using CTE:
;With T as(
select
*,
ROW_NUMBER() over(partition by UserId order by [time]) RN
from tbl
)
select
UserID,
rupees,
[time],
(select SUM(rupees)
from T b
where b.UserID=a.UserID and b.RN<=a.RN) CumulativeSum
from T a
For records with column value time increasing, try the below query:
select
UserID,
rupees,
[time],
(select SUM(rupees)
from tbl b
where b.UserID=a.UserID and b.[time]<=a.[time]) CumulativeSum
from tbl a
For SQL Server 2012 or later, you can use SUM() with an OVER clause that specifies a ROW clause:
declare #t table (ID int,UserID int,rupees int,[time] date)
insert into #t(ID,UserID,rupees,[time]) values
(1,1,200,'20140105'),
(2,1,500,'20140406'),
(3,2, 10,'20140505'),
(4,2, 20,'20140506')
select
*,
SUM(rupees) OVER (
PARTITION BY UserID
ORDER BY id /* or time? */
ROWS BETWEEN
UNBOUNDED PRECEDING AND
CURRENT ROW)
as total
from #t
Result:
ID UserID rupees time total
----------- ----------- ----------- ---------- -----------
1 1 200 2014-01-05 200
2 1 500 2014-04-06 700
3 2 10 2014-05-05 10
4 2 20 2014-05-06 30
DECLARE #t table (UserID INT,rupees INT,DateKey Date )
INSERT INTO #t VALUES
(1,200,'2014-01-05'),
(2,300,'2014-01-06'),
(2,800,'2014-03-06')
select UserID,
rupees,
DateKey,
(SELECT SUM(rupees)from #t t
where t.rupees <= tt.rupees) from #t tt
GROUP BY UserID,rupees,DateKey
Hope this too helps you.
DECLARE #tab TABLE (id INT,userId INT,rupees INT,[time] Date)
INSERT INTO #tab VALUES
(1,1,200 ,'2014-01-05'),
(2,1,500 ,'2014-04-06'),
(3,2,10 ,'2014-05-05'),
(4,2,20 ,'2014-05-06')
SELECT LU.id,LU.userId,LU.rupees,LU.time,SUM(b.rupees) CumulativeSum
FROM (SELECT *,ROW_NUMBER() OVER (PARTITION BY userId ORDER BY [time]) R FROM #tab) B
JOIN (SELECT *,ROW_NUMBER() OVER (PARTITION BY userId ORDER BY [time]) R FROM #tab) LU
ON B.userId = LU.userId AND B.R <= LU.R
GROUP BY LU.id,LU.userId,LU.rupees,LU.time
Result
I am assuming that you are not using SQL Server 2012, which provides the cumulative sum function. The other answers use some form of the row_number() function, but these seems totally unnecessary. I usually approach cumulative sums using correlated subqueries:
select ID, UserID, rupees, [time],
(select sum(rupees)
from table t2
where t2.UserId = t.UserId and
t2.ID <= t.ID
) as CumulativeSum
from table t;
This requires having a column that uniquely identifies each row, and that seems to be the purpose of id. For performance, I would want to have an index on table(UserId, ID, rupees).
select *, SUM(rupees) OVER (
PARTITION BY UserID
ORDER BY id) as CumSum from #tbl
Need To select Data From One Table After Minus With One Value
this is the question i already asked and this solution for one value input to table and result. but i need this with more input values for different categories and each categories output
for eg(based of previous question)
Table 1
SNo Amount categories
1 100 type1
2 500 type1
3 400 type1
4 100 type1
5 100 type2
6 200 type2
7 300 type2
8 500 type3
9 100 type3
and
values for type1 - 800
values for type2 - 200
values for type3 - 100
and the output need is
for type-1
800 - 100 (Record1) = 700
700 - 500 (record2) = 200
200 - 400 (record3) = -200
The table records starts from record 3 with Balance Values Balance 200
Table-Output
SNo Amount
1 200
2 100
that means if minus 800 in first table the first 2 records will be removed and in third record 200 is Balance
same operation for remain types also and how to do it?
SQLFiddle demo
with T1 as
(
select t.*,
SUM(Amount) OVER (PARTITION BY [Type] ORDER BY [SNo])
-
CASE WHEN Type='Type1' then 800
WHEN Type='Type2' then 200
WHEN Type='Type3' then 100
END as Total
from t
)select Sno,Type,
CASE WHEN Amount>Total then Total
Else Amount
end as Amount
from T1 where Total>0
order by Sno
UPD: If types are not fixed then you should create a table for them, for example:
CREATE TABLE types
([Type] varchar(5), [Value] int);
insert into types
values
('type1',800),
('type2',200),
('type3',100);
and use the following query:
with T1 as
(
select t.*,
SUM(Amount) OVER (PARTITION BY t.[Type] ORDER BY [SNo])
-
ISNULL(types.Value,0) as Total
from t
left join types on (t.type=types.type)
)select Sno,Type,
CASE WHEN Amount>Total then Total
Else Amount
end as Amount
from T1 where Total>0
order by Sno
SQLFiddle demo
UPDATE: For MSSQL 2005 just replace SUM(Amount) OVER (PARTITION BY t.[Type] ORDER BY [SNo]) with (select SUM(Amount) from t as t1
where t1.Type=t.Type
and t1.SNo<=t.SNo)
with T1 as
(
select t.*,
(select SUM(Amount) from t as t1
where t1.Type=t.Type
and t1.SNo<=t.SNo)
-
ISNULL(types.Value,0) as Total
from t
left join types on (t.type=types.type)
)select Sno,Type,
CASE WHEN Amount>Total then Total
Else Amount
end as Amount
from T1 where Total>0
order by Sno
SQLFiddle demo
For even rows formula for median is (104.5 + 108)/2 for table below and For odd rows it is 108 for table below
Total Total
100 100
101 101
104.5 104.5
108 108
108.3 108.3
112 112
114
Code below works in SQL Server 2008 but not in SQL Server 2000 as it does not understand row_number() and over.
How can we change the lower code to make it work on SQL Server 2000?
select avg(Total) median from
(select Total,
rnasc = row_number() over(order by Total),
rndesc = row_number() over(order by Total desc)
from [Table]
) b
where rnasc between rndesc - 1 and rndesc + 1
If you only want a median, you may use this simple query.
SELECT
(
(SELECT MAX(Total) FROM
(SELECT TOP 50 PERCENT Total FROM [Table] ORDER BY Total) AS BottomHalf)
+
(SELECT MIN(Total) FROM
(SELECT TOP 50 PERCENT Total FROM [Table] ORDER BY Total DESC) AS TopHalf)
) / 2.0 AS Median
Source: Function to Calculate Median in Sql Server
SELECT Median = AVG(Total) FROM
(
SELECT Total FROM (
SELECT TOP 1 Total = Total * 1.0 FROM
(
SELECT TOP 50 PERCENT Total
FROM dbo.[Table] ORDER BY Total
) AS sub_a
ORDER BY 1 DESC
) AS sub_1
UNION ALL
SELECT Total FROM (
SELECT TOP 1 Total = Total * 1.0 FROM
(
SELECT TOP 50 PERCENT Total
FROM dbo.[Table] ORDER BY Total DESC
) AS sub_b
ORDER BY 1
) AS sub_2
) AS median;