How to minus current and previous value in SQL Server - sql

Have one table, need to minus one column previous and current amount. Table value is below, need to write syntax for Cal-Amount column
Id Amount Cal-Amount
1 100 0
2 200 0
3 400 0
4 500 0
Cal-Amount calculation formula with sample value
Id Amount Cal-Amount
1 100 (0-100)=100
2 200 (100-200)=100
3 400 (200-400)=200
4 500 (400-500)=100
Need SQL syntax to minus column current and previous value

LAG is one option if you are using SQL Server 2012 or later:
SELECT
Id,
Amount,
LAG(Amount, 1, 0) OVER (ORDER BY Id) - Amount AS [Cal-Amount]
FROM yourTable;
If you are using an earlier version of SQL Server, then we can use a self join:
SELECT
Id,
Amount,
COALESCE(t2.Amount, 0) - t1.Amount AS [Cal-Amount]
FROM yourTable t1
LEFT JOIN yourTable t2
ON t1.Id = t2.Id + 1;
But note that the self join option might only work if the Id values are continuous. LAG is probably the most efficient way to do this, and is also robust to non sequential Id values, so long as the order is correct.

Well, Tim beat me to the lag(), so here's the old-school using join:
select t.Id,t.Amount,t.Amount-isnull(t2.Amount,0) AS [Cal-Amount]
from yourtable t
left join yourtable t2 on t.id=t2.id+1

SQL Server 2012 or newer:
Select
ID, Amount, [Cal-Amount] = Amount - LAG(Amount, 1, 0) OVER (ORDER BY Id)
From
table
or
Select
current.ID, Current.Amount, Current.Amount - Isnull(Prior.Amount, 0)
from
table current
left join
table prior on current.id - 1 = prior.id

You can use the LAG function if your SQL Server >= 2012
declare #t table (id int, amount1 int)
insert into #t
values (1, 100), (2, 200), (3, 400), (4, 500)
select
*, amount1 - LAG(amount1, 1, 0) over (order by id) as CalAmount
from
#t

You can also use apply :
select t.*, t.Amount - coalesce(tt.Amount, 0) as CalAmount
from table t outer apply (
select top (1) *
from table t1
where t1.id < t.id
order by t1.id desc
) tt;

Related

SQL mimicking analytic LEAD/LAG function with some restrictions

There is a table named test, with one column named amount (number datatype).
There is no PK for this table, and amounts can be repeated.
The table's DDL is below: (created for testing purposes in Oracle 18c xe)
create table test (amount number(20));
insert into test values (20);
insert into test values (10);
insert into test values (30);
insert into test values (20);
insert into test values (10);
insert into test values (40);
insert into test values (15);
insert into test values (40);
The goal is to mimick the LEAD analytical function results ordered by amount, but no analytic (incl. ranking and window functions) can be used. PSM (incl MYSQL stored features, PL/SQL, T-SQL etc.) or some kind of identity tables can neither be used.
The desired output is shown in lead_rows_analytic_amount column:
select
amount,
lead(amount) over (order by amount) as lead_rows_analytic_amount
from test t1;
actual result:
amount lead_rows_analytic_amount
10 10
10 15
15 20
20 20
20 30
30 40
40 40
40
What are some elegant ways to achieve the result taking into account the restrictions set?
The DB is irrelevant here, if the restrictions apply.
I am attaching a stupidly clumsy and direct solution I came up with, but the goal is to get something more elegant (ignoring the performance).
with initial_rn as (
select
amount,t1.rowid,
( select count (*)
from test t2
where
t1.amount >= t2.amount
) as rn
from test t1
)
,prep_table as (
select t1.*,nvl2(repeating_rn,1,0) as repeating_rn_tag,
nvl(( SELECT max(rn)
FROM initial_rn t2
where t2.rn < t1.rn
),0) AS lag_rn
from initial_rn t1
left join (select rn as repeating_rn
from initial_rn
group by rn
having count(*) > 1) t2 on t1.rn = t2.repeating_rn
)
,final_rn as (
select t1.amount,case when repeating_rn_tag = 0 then rn else lag_rn +
( select count (*)
from prep_table t2
where
t1.rowid >= t2.rowid and t1.repeating_rn_tag = 1 and t2.repeating_rn_tag = 1 and t1.rn = t2.rn
)
end as final_rn
from prep_table t1
)
select t1.*,
lead(amount) over (order by amount) as lead_rows_analytic_amount,
(select min(amount)
from test t2
where t2.amount > t1.amount
) as lead_range_amount,
(SELECT MIN(amount)
FROM final_rn t2
where t2.final_rn > t1.final_rn
) AS lead_amount
from final_rn t1
order by amount
;
In Oracle, you can use:
SELECT CASE WHEN LEVEL = 1 THEN amount ELSE PRIOR amount END AS amount,
CASE WHEN LEVEL = 1 THEN NULL ELSE amount END AS lead_amount
FROM (
SELECT amount,
ROWNUM AS rn
FROM (
SELECT amount
FROM test
ORDER BY amount
)
)
WHERE LEVEL = 2
OR LEVEL = 1 AND CONNECT_BY_ISLEAF = 1
CONNECT BY PRIOR rn + 1 = rn
More generally, you can use:
WITH ordered_amounts (amount) AS (
SELECT amount
FROM test
ORDER BY amount
),
indexed_amounts (amount, idx) AS (
SELECT amount,
ROWNUM -- Or any function that gives sequentially increasing values
FROM ordered_amounts
)
SELECT i.amount,
nxt.amount AS lead_amount
FROM indexed_amounts i
LEFT OUTER JOIN indexed_amounts nxt
ON (i.idx + 1 = nxt.idx)
Which, for the sample data, both output:
AMOUNT
LEAD_AMOUNT
10
10
10
15
15
20
20
20
20
30
30
40
40
40
40
null
db<>fiddle here
Ok so just throwing this out there as something you could do, using JSON functionality (support exists in most RDBMS)
This is SQL server syntax:
with v as (
select *
from OpenJson(
(select Concat('[',String_Agg(amount,',')
within group (order by amount),']')from test)
)
)
select value, (
select value
from v v2
where v2.[key]=v.[key]+1
) as lead_rows_analytic_amount
from v
Example fiddle
To contribute to this wonderful collection of solutions how to avoid window functions, I feel it's worth mention Oracle model clause:
with test as (
select column_value as amount
from table(sys.ku$_vcnt(20,10,30,20,10,40,15,40)) -- or your table, I'm just lazy to create fiddle
)
select amount, lead_amount
from (
select *
from (select amount, 0 as lead_amount from test order by amount)
model
dimension by (rownum as rn)
measures (amount, lead_amount)
rules (amount[any] = amount[cv(rn)], lead_amount[any] = amount[cv(rn) + 1])
)
order by amount
(Not sure if it is helpful for you, compared with window functions.)
If you had a primary key (any table should have):
select a.*, (select min(r.amount)
from #test r
where ((r.id <> a.id and r.amount > a.amount)
OR
(r.id > a.id and r.amount=a.amount)
)
) as NextVal
from #test a
order by a.amount, a.id

Cumulative subtraction across rows

Table 1:
Table 2:
How can I subtract the Committed value (7) from the IncomingQuantity cumulatively across rows? So that the result would look like:
Thanks!
You need a cumulative sum and some arithmetic:
select t.*,
(case when running_iq - incomingquantity >= committed then 0
when running_iq > committed then running_iq - committed
else incomingquantity
end) as from_this_row
from (select t2.*, t1.committed,
sum(incomingquantity) over (order by rowid) as running_iq
from table1 t1 cross join
table2 t2
) t;
you can also make use of the built-in functions such as ROW_NUMBER(), LAST_VALUE(), and LAG() with CASE
here is an example :
DECLARE
#t1 TABLE ( ProductID VARCHAR(50), ICommitted INT)
INSERT INTO #t1 VALUES ('Some product', 7)
DECLARE
#t2 TABLE (RowID INT, DueDate DATE, IncommingQuantity INT)
INSERT INTO #t2 VALUES
(1,'2018-11-19', 5),
(2,'2018-11-20', 4),
(3,'2018-11-20', 4),
(4,'2018-11-20', 3),
(5,'2018-11-22', 12)
SELECT
RowID
, DueDate
, CASE
WHEN RowID = 1
THEN 0
WHEN RowID = LAST_VALUE(RowID) OVER(ORDER BY (SELECT NULL) )
THEN IncommingQuantity
WHEN ROW_NUMBER() OVER(PARTITION BY DueDate ORDER BY RowID) > 1
THEN IncommingQuantity
ELSE ICommitted - LAG(IncommingQuantity) OVER (ORDER BY RowID)
END IncommingQuantity
FROM #t2 t2
CROSS APPLY (SELECT t1.ICommitted FROM #t1 t1) e
I ended up doing this simply with WHILE loop inside a user function. The other solutions I would not work properly in 100% of cases

Accumulating in SQL

I have a query with results like ID, Value. What I want is to get the values in order of their ids and also calculate the accumulated value in another column. take a look at my simplified code:
declare #TempTable Table
(
ID int,
Value int
)
insert into #TempTable values
(1, 10),
(2, -15),
(3, 12),
(4, 18),
(5, 5)
select t1.ID, t1.Value, SUM(t2.Value) AccValue from #TempTable t1
inner join #TempTable t2 on t1.ID >= t2.ID
group by t1.ID, t1.Value
order by t1.ID
Result:
ID Value AccValue
1 10 10
2 -15 -5
3 12 7
4 18 25
5 5 30
What I have come up with, is to use inner join between the result and itself for that purpose. But for huge amount of data, it's clearly a low performance issue.
Is there any other alternative to do that?
In 2012 version, you can use:
SELECT
id,
Value,
AccValue = SUM(Value) OVER (ORDER BY ID
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)
FROM
#TempTable ;
For previous versions of SQL-Server, see my answer in this similar question: Recursive SQL- How can I get this table with a running total?, with a cursor solution.
Even better, follow the link to the great article by #Aaron Bertrand, that has a thorough test of various methods to calculate a running total: Best approaches for running totals – updated for SQL Server 2012
You can use recursion:
;WITH x AS
(
SELECT
[ID],
[Value],
bal=[Value]
FROM Table1
WHERE [ID] = 1
UNION ALL
SELECT
y.[ID],
y.[Value],
x.bal+(y.[Value]) as bal
FROM x INNER JOIN Table1 AS y
ON y.[ID] = x.[ID] + 1
)
SELECT
[ID],
[Value],
AccValue= bal
FROM x
order by ID
OPTION (MAXRECURSION 10000);
SQL FIDDLE
The generic SQL way to do this is with a correlated subquery (at least, I think that is the cleanest way):
select t.*,
(select sum(t2.value)
from #TempTable t2
where t2.ID <= t.ID
) AccValue
from #TempTable t
SQL Server 2012 has a cumulative sum function:
select t.*,
sum(t.value) over (order by t.id) as AccValue
from #TempTable t

SQL stored procedure to add up values and stop once the maximum has been reached

I would like to write a SQL query (SQL Server) that will return rows (in a given order) but only up to a given total. My client has paid me a given amount, and I want to return only those rows that are <= to that amount.
For example, if the client paid me $370, and the data in the table is
id amount
1 100
2 122
3 134
4 23
5 200
then I would like to return only rows 1, 2 and 3
This needs to be efficient, since there will be thousands of rows, so a for loop would not be ideal, I guess. Or is SQL Server efficient enough to optimise a stored proc with for loops?
Thanks in advance. Jim.
A couple of options are.
1) Triangular Join
SELECT *
FROM YourTable Y1
WHERE (SELECT SUM(amount)
FROM YourTable Y2
WHERE Y1.id >= Y2.id ) <= 370
2) Recursive CTE
WITH RecursiveCTE
AS (
SELECT TOP 1 id, amount, CAST(amount AS BIGINT) AS Total
FROM YourTable
ORDER BY id
UNION ALL
SELECT R.id, R.amount, R.Total
FROM (
SELECT T.*,
T.amount + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.id)
FROM YourTable T
JOIN RecursiveCTE R
ON R.id < T.id
) R
WHERE R.rn = 1 AND Total <= 370
)
SELECT id, amount, Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
The 2nd one will likely perform better.
In SQL Server 2012 you will be able to so something like
;WITH CTE AS
(
SELECT id,
amount,
SUM(amount) OVER(ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
AS RunningTotal
FROM YourTable
)
SELECT *
FROM CTE
WHERE RunningTotal <=370
Though there will probably be a more efficient way (to stop the scan as soon as the total is reached)
Straight-forward approach :
SELECT a.id, a.amount
FROM table1 a
INNER JOIN table1 b ON (b.id <=a.id)
GROUP BY a.id, a.amount
HAVING SUM(b.amount) <= 370
Unfortunately, it has N^2 performance issue.
something like this:
select id from
(
select t1.id, t1.amount, sum( t2.amount ) s
from tst t1, tst t2
where t2.id <= t1.id
group by t1.id, t1.amount
)
where s < 370

Referencing a previous row value for an arithmetic calculation in SQL Server 2008 R2

I am working with SQL Server 2008 R2 and new to relational database. I need to run a simple calculation but the calculation involves using a previous row value.
Example:
(Value of X) / ((Value of Y at time t + Value of Y at time t-1) / 2)
Example:
select (x/[(y#time,t + y#time,t-1)/2]) as 'Value'
from datatable
select ((c.ACHQ)/(c.RECTQ(row:n) + c.RETQ(row:n-1))/2) as 'AR'
from co_ifndq c
where c.GVKEY in
(select GVKEY
from spidx_cst
where DATADATE = '2012-03-12'
and INDEXID = '500')
and c.DATAFMT = 'std'
and c.DATADATE > '1990-12-30'
order by c.GVKEY, datadate desc
As I understand you want to make a calculation base on a date difference and not really on a row order, right?
If so, if you have a table like this
CREATE TABLE YourTable(
ACHQ float ,
RECTQ float,
DATE datetime)
INSERT INTO YourTable VALUES (100,10,'20100101')
INSERT INTO YourTable VALUES (200,20,'20110101')
INSERT INTO YourTable VALUES (300,30,'20120101')
INSERT INTO YourTable VALUES (400,40,'20130101')
INSERT INTO YourTable VALUES (500,50,'20140101')
INSERT INTO YourTable VALUES (600,60,'20150101')
you can do something like this
SELECT
((c.ACHQ)/(c.RECTQ + cPreviousYear.RECTQ)/2) as 'AR'
FROM
YourTable c
LEFT JOIN YourTable cPreviousYear
ON YEAR(c.Date) - 1 = YEAR(cPreviousYear.Date)
I simplified the calculation just to show that you can link the table to itself directly to the row with the wanted date difference and then calculate the value. you can even use ON DATEADD(y, -1, c.Date) = cPrevious.Date if you want the real date diference
Sorry if I missed the point.
Assuming x, y and t are all on the same table, try:
;with cte as (
select m.*, row_number() over (order by t) rn from mytable)
select t1.t, t1.x / ((t1.y + t0.y)/2) as [value]
from cte t1
left join cte t0 on t0.rn = t1.rn-1
EDIT: based on the query supplied:
;with cte as (
select c.*, row_number() over (partition by c.GVKEY order by c.DATADATE) rn
from co_ifndq c
where c.GVKEY in
(select GVKEY
from spidx_cst
where DATADATE = '2012-03-12' and INDEXID = '500')
and c.DATAFMT = 'std'
and c.DATADATE > '1990-12-30'
)
select t1.GVKEY, t1.DATADATE, t1.ACHQ / ((t1.RETQ + t0.RETQ)/2) as [value]
from cte t1
left join cte t0 on t1.GVKEY = t0.GVKEY and t0.rn = t1.rn-1
order by t1.GVKEY, t1.datadate desc