SQL Server CTE Running balance - sql-server

#table1
idno | amount
-------------
1 | 700
2 | 500
#table2
idno | amount1 | amount2 | amount3 | acctno
------------------------------------------
1 | 100 | 200 | 300 | 001
1 | 100 | 200 | 300 | 002
2 | 100 | 200 | 300 | 001
What I want to happen is to distribute the amount from table2 into table 1's amount1,amount2,amount3 respectively then get the remaining balance and apply to the next row. I've tried using CTE but got stucked on passing the running balance to the next row.
Query:
Declare #table2 TABLE (idno varchar(max), amount1 decimal,amount2
decimal,amount3 decimal,acctno varchar(max))
INSERT INTO #table2 VALUES
('1',100,200,300,'001'),
('1',100,200,300,'002'),
('2',100,200,300,'001')
Declare #table1 TABLE (idno varchar(max), amount decimal)
INSERT INTO #table1 VALUES
('1',700),
('2',500);
WITH due AS (SELECT a.idno,a.amount,b.acctno,b.amount1,b.amount2,b.amount3
from #table1 a left join #table2 b on a.idno = b.idno),
payment AS (SELECT *,case when amount-amount1<0 then amount
else amount1 end as amount1pay
,case when amount-amount1<=0 then 0
when amount-amount1-amount2 <0 then amount-amount1
else amount2 end as amount2pay ,
case when amount-amount1-amount2<=0 then 0
when amount-amount1-amount2-amount3<0
then amount-amount1-amount2 else amount3 end as amount3pay
FROM due),
payment2 AS (SELECT SUM(amount-amount1pay-amount2pay-amount3pay)
OVER ( PARTITION BY idno ORDER BY acctno
ROWS UNBOUNDED PRECEDING ) as balance,* FROM payment)
select * from payment2
Current Result
balance | idno | amount | acctno | amount1 | amount2 | amount3 | amount1pay | amount2pay | amount3pay
---------------------------------------------------------------------------------------------------------
100 | 1 | 200 | 001 | 100 | 200 | 300 | 100 | 200 | 300
200 | 1 | 200 | 002 | 100 | 200 | 300 | 100 | 200 | 300
0 | 2 | 500 | 001 | 100 | 200 | 300 | 100 | 200 | 200
Expected Result
balance | idno | amount | acctno | amount1 | amount2 | amount3 | amount1pay | amount2pay | amount3pay
---------------------------------------------------------------------------------------------------------
100 | 1 | 200 | 001 | 100 | 200 | 300 | 100 | 200 | 300
100 | 1 | 200 | 002 | 100 | 200 | 300 | 100 | 0 | 0
0 | 2 | 500 | 001 | 100 | 200 | 300 | 100 | 200 | 200

This seems very messy, as there's a lot of different things going on here. I don't think I quite understand all of the "rules" for how you are distributing this money but this query does produce the expected results (actually it's slightly different, but I think you have a mistake in your table where you show "200" for amount in the first two rows it should be "700")?
WITH Base AS (
SELECT
t1.idno,
t2.acctno,
t1.amount,
t2.amount1,
t2.amount2,
t2.amount3,
ROW_NUMBER() OVER (ORDER BY t1.idno, t2.acctno) AS row_id
FROM
#table1 t1
INNER JOIN #table2 t2 ON t1.idno = t2.idno),
RunningBalance AS (
SELECT
*,
CASE WHEN amount > amount1 + amount2 + amount3 THEN amount - amount1 - amount2 - amount3 ELSE 0 END AS new_balance
FROM
Base),
NewIdno AS (
SELECT
idno,
MIN(row_id) AS first_row_id
FROM
Base
GROUP BY
idno),
NewBalance AS (
SELECT
n.first_row_id AS row_id,
b.amount
FROM
NewIdno n
INNER JOIN Base b ON b.row_id = n.first_row_id),
Amount1 AS (
SELECT
b.row_id,
rb1.new_balance AS balance,
b.idno,
b.amount,
b.acctno,
b.amount1,
b.amount2,
b.amount3,
CASE WHEN ISNULL(n.amount, rb2.new_balance) >= b.amount1 THEN b.amount1 ELSE b.amount1 - ISNULL(n.amount, rb2.new_balance) END AS pay_amount1,
ISNULL(n.amount, rb2.new_balance) - b.amount1 AS carried_forward_1
FROM
Base b
INNER JOIN RunningBalance rb1 ON rb1.row_id = b.row_id
LEFT JOIN RunningBalance rb2 ON rb2.row_id = b.row_id - 1
LEFT JOIN NewBalance n ON n.row_id = b.row_id),
Amount2 AS (
SELECT
*,
CASE WHEN carried_forward_1 >= amount2 THEN amount2 ELSE carried_forward_1 END AS pay_amount2,
carried_forward_1 - CASE WHEN carried_forward_1 >= amount2 THEN amount2 ELSE carried_forward_1 END AS carried_forward_2
FROM
Amount1),
Amount3 AS (
SELECT
*,
CASE WHEN carried_forward_2 >= amount3 THEN amount3 ELSE carried_forward_2 END AS pay_amount3
FROM
Amount2)
SELECT
balance,
idno,
amount,
acctno,
amount1,
amount2,
amount3,
pay_amount1,
pay_amount2,
pay_amount3
FROM
Amount3;
My results are:
balance idno amount acctno amount1 amount2 amount3 pay_amount1 pay_amount2 pay_amount3
100 1 700 001 100 200 300 100 200 300
100 1 700 002 100 200 300 100 0 0
0 2 500 001 100 200 300 100 200 200
Some of the rules I am using:
you start with the amount from table 1 to be distributed, i.e. £700 for idno #1 and £500 for idno #2;
this is assigned to table 2 by acctnos in numerical order;
where there are multiple acctnos you need to carry forward the balance from the previous one, if there's anything left to pay out;
once you start a new idno you can't carry forward any money left from the previous one.
So how does it work?
Step 1 - Order the data and add an index (row_id)
This is just the base data from the two tables, showing how much we have to distribute and the rows we are distributing it over:
idno acctno amount amount1 amount2 amount3 row_id
1 001 700 100 200 300 1
1 002 700 100 200 300 2
2 001 500 100 200 300 3
Step 2 - Work out the running balance
This works out how much money is left if we distributed the entire amount available across each row:
idno acctno amount amount1 amount2 amount3 row_id new_balance
1 001 700 100 200 300 1 100
1 002 700 100 200 300 2 100
2 001 500 100 200 300 3 0
Step 3 - (Interlude) We need to know which row we will be distributing data over first
This is just the first row_id for each idno:
idno first_row_id
1 1
2 3
Step 4 - Slightly wasteful, as we could have done this in the last step
We just need the total to be distributed over each "first" row:
row_id amount
1 700
3 500
Step 5 - This is where we deal with the running balance properly
For each row the rule is that we start with the "new balance" which only exists for the first row of each amount to be distributed. If this isn't the first row then we use the running balance instead, but we take this from the previous row (rb2.row_id = b.row_id - 1). We will always have one or the other of these:
row_id balance idno amount acctno amount1 amount2 amount3 pay_amount1 carried_forward_1
1 100 1 700 001 100 200 300 100 600
2 100 1 700 002 100 200 300 100 0
3 0 2 500 001 100 200 300 100 400
So the carried forward isn't what is to be carried to the next row, it's what is to be carried to the next amount (amount 2 in this case) to be distributed.
Note that this works for your dataset, but it wouldn't work if there were more than two rows per idno. If you were to have more than two rows per idno then you would simply need to add another stage to handle this scenario.
Step 6 - Carry Forward
For each amount we need to take it off the amount carried forward from the previous calculation, giving us the amount we can allocate to this amount, and the amount to be carried forward to the next calculation (which is redundant for amount 3 as there is no amount 4).

Related

How to use Lag with while to calculate the effectiveness of discount , SQL

I want to calculate the effectiveness of a discount.
I want to update the last column as 'pozitive' if s_quantity increased and negative, if decreased. Neutral, if no change. I've written the code:
DECLARE #count AS INT
SET #count=1
WHILE #count< 316
BEGIN
IF product_id = #count
WHEN s_quantity > (LAG(s_quantity) OVER (ORDER BY product_id ASC, discount ASC))
UPDATE [SampleRetail].[sale].[Analiz] SET hesap_kitap = 'pozitif'
SET #count +=1
IF #count > 316
BREAK
ELSE
CONTINUE
END
Where do I make the mistake? Can you help me?
We can do this with the function LAG() in a SELECT.
(see the dbFiddle link below for the test schema.)
SELECT
period,
sales,
LAG(sales) OVER (ORDER BY period) p_1,
sales - LAG(sales) OVER (ORDER BY period) increase
FROM sales
ORDER BY period;
period | sales | p_1 | increase
-----: | ----: | ---: | -------:
1 | 100 | null | null
2 | 200 | 100 | 100
3 | 300 | 200 | 100
4 | 250 | 300 | -50
5 | 500 | 250 | 250
6 | 500 | 500 | 0
WITH sale as (
SELECT
period,
sales,
LAG(sales) OVER (ORDER BY period) p_1
FROM sales)
SELECT
period,
sales,
sales - p_1 increase,
CASE WHEN sales < p_1 THEN 'negative'
WHEN sales = p_1 THEN 'stable'
ELSE 'positive' END AS "change"
FROM sale;
period | sales | increase | change
-----: | ----: | -------: | :-------
1 | 100 | null | positive
2 | 200 | 100 | positive
3 | 300 | 100 | positive
4 | 250 | -50 | negative
5 | 500 | 250 | positive
6 | 500 | 0 | stable
db<>fiddle here

Window function to count occurrences in last 10 minutes

I can use a traditional subquery approach to count the occurrences in the last ten minutes. For example, this:
drop table if exists [dbo].[readings]
go
create table [dbo].[readings](
[server] [int] NOT NULL,
[sampled] [datetime] NOT NULL
)
go
insert into readings
values
(1,'20170101 08:00'),
(1,'20170101 08:02'),
(1,'20170101 08:05'),
(1,'20170101 08:30'),
(1,'20170101 08:31'),
(1,'20170101 08:37'),
(1,'20170101 08:40'),
(1,'20170101 08:41'),
(1,'20170101 09:07'),
(1,'20170101 09:08'),
(1,'20170101 09:09'),
(1,'20170101 09:11')
go
-- Count in the last 10 minutes - example periods 08:31 to 08:40, 09:12 to 09:21
select server,sampled,(select count(*) from readings r2 where r2.server=r1.server and r2.sampled <= r1.sampled and r2.sampled > dateadd(minute,-10,r1.sampled)) as countinlast10minutes
from readings r1
order by server,sampled
go
How can I use a window function to obtain the same result ? I've tried this:
select server,sampled,
count(case when sampled <= r1.sampled and sampled > dateadd(minute,-10,r1.sampled) then 1 else null end) over (partition by server order by sampled rows between unbounded preceding and current row) as countinlast10minutes
-- count(case when currentrow.sampled <= r1.sampled and currentrow.sampled > dateadd(minute,-10,r1.sampled) then 1 else null end) over (partition by server order by sampled rows between unbounded preceding and current row) as countinlast10minutes
from readings r1
order by server,sampled
But the result is just the running count. Any system variable that refers to the current row pointer ? currentrow.sampled ?
This isn't a very pleasing answer but one possibility is to first create a helper table with all the minutes
CREATE TABLE #DateTimes(datetime datetime primary key);
WITH E1(N) AS
(
SELECT 1 FROM (VALUES(1),(1),(1),(1),(1),
(1),(1),(1),(1),(1)) V(N)
) -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b) -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b) -- 1*10^4 or 10,000 rows
, E8(N) AS (SELECT 1 FROM E4 a, E4 b) -- 1*10^8 or 100,000,000 rows
,R(StartRange, EndRange)
AS (SELECT MIN(sampled),
MAX(sampled)
FROM readings)
,N(N)
AS (SELECT ROW_NUMBER()
OVER (
ORDER BY (SELECT NULL)) AS N
FROM E8)
INSERT INTO #DateTimes
SELECT TOP (SELECT 1 + DATEDIFF(MINUTE, StartRange, EndRange) FROM R) DATEADD(MINUTE, N.N - 1, StartRange)
FROM N,
R;
And then with that in place you could use ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
WITH T1 AS
( SELECT Server,
MIN(sampled) AS StartRange,
MAX(sampled) AS EndRange
FROM readings
GROUP BY Server )
SELECT Server,
sampled,
Cnt
FROM T1
CROSS APPLY
( SELECT r.sampled,
COUNT(r.sampled) OVER (ORDER BY N.datetime ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS Cnt
FROM #DateTimes N
LEFT JOIN readings r
ON r.sampled = N.datetime
AND r.server = T1.server
WHERE N.datetime BETWEEN StartRange AND EndRange ) CA
WHERE CA.sampled IS NOT NULL
ORDER BY sampled
The above assumes that there is at most one sample per minute and that all the times are exact minutes. If this isn't true it would need another table expression pre-aggregating by datetimes rounded to the minute.
As far as I know, there is not a simple exact replacement for your subquery using window functions.
Window functions operate on a set of rows and allow you to work with them based on partitions and order.
What you are trying to do isn't the type of partitioning that we can work with in window functions.
To generate the partitions we would need to be able to use window functions in this instance would just result in overly complicated code.
I would suggest cross apply() as an alternative to your subquery.
I am not sure if you meant to restrict your results to within 9 minutes, but with sampled > dateadd(...) that is what is happening in your original subquery.
Here is what a window function could look like based on partitioning your samples into 10 minute windows, along with a cross apply() version.
select
r.server
, r.sampled
, CrossApply = x.CountRecent
, OriginalSubquery = (
select count(*)
from readings s
where s.server=r.server
and s.sampled <= r.sampled
/* doesn't include 10 minutes ago */
and s.sampled > dateadd(minute,-10,r.sampled)
)
, Slices = count(*) over(
/* partition by server, 10 minute slices, not the same thing*/
partition by server, dateadd(minute,datediff(minute,0,sampled)/10*10,0)
order by sampled
)
from readings r
cross apply (
select CountRecent=count(*)
from readings i
where i.server=r.server
/* changed to >= */
and i.sampled >= dateadd(minute,-10,r.sampled)
and i.sampled <= r.sampled
) as x
order by server,sampled
results: http://rextester.com/BMMF46402
+--------+---------------------+------------+------------------+--------+
| server | sampled | CrossApply | OriginalSubquery | Slices |
+--------+---------------------+------------+------------------+--------+
| 1 | 01.01.2017 08:00:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 08:02:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 08:05:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 08:30:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 08:31:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 08:37:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 08:40:00 | 4 | 3 | 1 |
| 1 | 01.01.2017 08:41:00 | 4 | 3 | 2 |
| 1 | 01.01.2017 09:07:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 09:08:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 09:09:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 09:11:00 | 4 | 4 | 1 |
+--------+---------------------+------------+------------------+--------+
Thanks, Martin and SqlZim, for your answers. I'm going to raise a Connect enhancement request for something like %%currentrow that can be used in window aggregates. I'm thinking this would lead to much more simple and natural sql:
select count(case when sampled <= %%currentrow.sampled and sampled > dateadd(minute,-10,%%currentrow.sampled) then 1 else null end) over (...whatever the window is...)
We can already use expressions like this:
select count(case when sampled <= getdate() and sampled > dateadd(minute,-10,getdate()) then 1 else null end) over (...whatever the window is...)
so thinking would be great if we could reference a column that's in the current row.

Grouping by a same range of multiple values with sum and counts in SQL

I want to group different colons in same range by row. Example:
Amount1 | Amount2
------------------------
20,00 | 30,00
35,00 | 32,00
12,00 | 51,00
101,00 | 100,00
Here result should be;
Range |TotalAmount1 |TotalAmount2 | CountAmount1 | CountAmount2 | RateOfCountAmount1
-----------------------------------------------------------------------------
0-50 | 67,00 | 62,00 | 3 | 1 | %75
50-100 | 0,00 | 151,00 | 0 | 2 | %0
100+ | 101,00 | 0,00 | 1 | 0 | %25
Total | 168,00 | 213,00 | 4 | 3 | %100
Here is the example : http://sqlfiddle.com/#!9/05fd3
you can query like this
;with cte as (
select case when amount1 < 50 then '0-50'
when amount1 between 50.01 and 100 then '50-100'
when amount1 > 100 then '100+' end as rngamt1,
case when amount2 < 50 then '0-50'
when amount2 between 5.01 and 100 then '50-100'
when amount2 > 100 then '100+' end as rngamt2,
* from amounts
), cte2 as (select coalesce(rngamt1, rngamt2) as [Range], isnull(a.TotalAmount1,0) as TotalAmount1, isnull(b.TotalAmount2, 0) as TotalAmount2, isnull( a.TotalCount1 , 0) as TotalCount1, isnull(b.TotalCount2, 0) as Totalcount2 from
(select rngamt1, sum(amount1) TotalAmount1, count(amount1) TotalCount1 from cte c
group by rngamt1) a
full join
(select rngamt2, sum(amount2) TotalAmount2, count(amount2) TotalCount2 from cte c
group by rngamt2) b
on a.rngamt1 = b.rngamt2
)
select *, (TotalCount1 * 100 )/sum(TotalCount1) over () as RateCount1
from cte2
union
select 'Total' as [Range], sum(TotalAmount1) as TotalAmount1, sum(totalAmount2) as TotalAmount2,
sum(TotalCount1) as TotalCount1, sum(Totalcount2) as TotalCount2, (sum(TotalCount1)*100)/Sum(TotalCount1) as RateCount1 from cte2

Sum values from 2 tables and subtract it to each other

I have 2 tables, ord_tbl and pay_tbl. o_tbl with these data.
ord_tbl
invoice | emp_id | prod_id | amount
123 | 101 | 1 | 1000
123 | 101 | 2 | 500
123 | 101 | 3 | 500
124 | 101 | 2 | 300
125 | 102 | 3 | 200
pay_tbl
invoice | new_invoice | amount
123 | 321 | 300
123 | 322 | 200
124 | 323 | 300
125 | 324 | 100
I would like the selection statement to give me this result
invoice | emp_id | orig_amt | balance | status
123 | 101 | 2000 | 1500 | unsettled
The invoice that has 0 balance will not be included anymore. I know that I have to use the join and sub queries here but I don't even know how to start it! For me, as a beginner, this is very complex already. This is what I tried so far...
SELECT
ord_tbl.invoice,
SUM(ord_tbl.amount) As 'origAmt',
SUM(pay_tbl.amount) As 'payAmt',
origAmt - payAmt As 'bal'
FROM
ord_tbl
INNER JOIN pay_tbl
ON ord_tbl.invoice = pay_tbl.invoice
WHERE
ord_tbl.emp_id = #emp_id AND
bal != 0
GROUP BY
ord_tbl.invoice
You need to prepare your data joining on a non unique field will lead to a cross JOIN,that`s why you get 4000
;WITH CTE as
(SELECT ot.invoice,MAX(ot.emp_id) as emp_id,SUM(ot.amount) as origAmt FROM ord_tbl ot GROUP BY ot.invoice),
CTE2 as
( SELECT pt.invoice,SUM(pt.ammount) as payAmt FROM pay_tbl pt GROUP BY pt.invoice)
SELECT CTE.invoice,CTE.emp_id,CTE.origAmt,CTE.origAmt-NULLIF(CTE2.payAmt,0) as bal,'unsettled' as status
FROM
CTE LEFT JOIN CTE2 ON CTE.invoice=CTE2.invoice
AND CTE.emp_id=101 AND CTE.origAmt-NULLIF(CTE2.payAmt,0)>0

The highest value from list-distinct

Can anyone help me with query, I have table
vendorid, agreementid, sales
12001 1004 700
5291 1004 20576
7596 1004 1908
45 103 345
41 103 9087
what is the goal ?
when agreemtneid >1 then show me data when sales is the highest
vendorid agreementid sales
5291 1004 20576
41 103 9087
Any ideas ?
Thx
Well you could try using a CTE and ROW_NUMBER something like
;WITH Vals AS (
SELECT *, ROW_NUMBER() OVER(PARTITION BY AgreementID ORDER BY Sales DESC) RowID
FROM MyTable
WHERE AgreementID > 1
)
SELECT *
FROM Vals
WHERE RowID = 1
This will avoid you returning multiple records with the same sale.
If that was OK you could try something like
SELECT *
FROM MyTable mt INNER JOIN
(
SELECT AgreementID, MAX(Sales) MaxSales
FROM MyTable
WHERE AgreementID > 1
) MaxVals ON mt.AgreementID = MaxVals.AgreementID AND mt.Sales = MaxVals.MaxSales
SELECT TOP 1 WITH TIES *
FROM MyTable
ORDER BY DENSE_RANK() OVER(PARTITION BY agreementid ORDER BY SIGN (SIGN (agreementid - 2) + 1) * sales DESC)
Explanation
We break table MyTable into partitions by agreementid.
For each partition we construct a ranking or its rows.
If agreementid is greater than 1 ranking will be equal to ORDER BY sales DESC.
Otherwise ranking for every single row in partition will be the same: ORDER BY 0 DESC.
See how it looks like:
SELECT *
, SIGN (SIGN (agreementid - 2) + 1) * sales AS x
, DENSE_RANK() OVER(PARTITION BY agreementid ORDER BY SIGN (SIGN (agreementid - 2) + 1) * sales DESC) AS rnk
FROM MyTable
+----------+-------------+-------+-------+-----+
| vendorid | agreementid | sales | x | rnk |
+----------|-------------|-------+-------+-----+
| 0 | 0 | 3 | 0 | 1 |
| -1 | 0 | 7 | 0 | 1 |
| 0 | 1 | 3 | 0 | 1 |
| -1 | 1 | 7 | 0 | 1 |
| 41 | 103 | 9087 | 9087 | 1 |
| 45 | 103 | 345 | 345 | 2 |
| 5291 | 1004 | 20576 | 20576 | 1 |
| 7596 | 1004 | 1908 | 1908 | 2 |
| 12001 | 1004 | 700 | 700 | 3 |
+----------+-------------+-------+-------+-----+
Then using TOP 1 WITH TIES construction we leave only rows where rnk equals 1.
you can try like this.
SELECT TOP 1 sales FROM MyTable WHERE agreemtneid > 1 ORDER BY sales DESC
I really do not know the business logic behind agreement_id > 1. It looks to me you want the max sales (with ties) by agreement id regardless of vendor_id.
First, lets create a simple sample database.
-- Sample table
create table #sales
(
vendor_id int,
agreement_id int,
sales_amt money
);
-- Sample data
insert into #sales values
(12001, 1004, 700),
(5291, 1004, 20576),
(7596, 1004, 1908),
(45, 103, 345),
(41, 103, 9087);
Second, let's solve this problem using a common table expression to get a result set that has each row paired with the max sales by agreement id.
The select statement just applies the business logic to filter the data to get your answer.
-- CTE = max sales for each agreement id
;
with cte_sales as
(
select
vendor_id,
agreement_id,
sales_amt,
max(sales_amt) OVER(PARTITION BY agreement_id) AS max_sales
from
#sales
)
-- Filter by your business logic
select * from cte_sales where sales_amt = max_sales and agreement_id > 1;
The screen shot below shows the exact result you wanted.

Resources