Cumulative vs. Tiered Calculation in SQL Query or Inline Table Value Function with Conditional Logic? - sql

I'm working with two tables of data, but different methodologies to derive two distinct desired outputs below.
The first is a cumulative tier calculation and the second is a just a tiered lookup based on a range. I need to be able to return this calculation for a row item transaction based on some JOINS using foreign keys to other dimension tables, namely Accounts and Regions and Tier Type. For example, a single account could have a calculation be either cumulative or tiered based on the region's and/or account's ID.
Link to DB Fiddle
Type ID
Name
1
Cumulative
2
Tiered
Tiered table:
Account ID
Type ID
Region ID
TierNo
Min
Max
Total A
Total B
101
1
2
1
0
10000
.90
.10
101
1
2
2
10001
30000
.60
.40
101
1
2
3
30001
100000
.40
.60
101
1
2
4
100001
500000
.40
.60
101
1
2
5
500001
999999999999
.20
.80
102
1
3
1
0
7800
.80
.20
102
1
3
2
7801
12800
.70
.30
102
1
3
3
12801
34000
.60
.40
102
1
3
4
34001
50000
.50
.50
102
1
3
5
5000 1
999999999999
.50
.50
103
2
1
1
0
10000
.90
.10
103
2
1
2
10001
30000
.60
.40
103
2
1
3
30001
100000
.40
.60
103
2
1
4
100001
500000
.40
.60
103
2
1
5
500001
999999999999
.20
.80
Current table sample:
Trans ID
Account ID
Type ID
Region ID
GrossAmt
Total A %
Total A $
Net Amt
100001
101
1
2
42650
100002
102
1
3
42650
100003
103
2
1
42650
Desired output:
Trans ID
Account ID
Type ID
Region ID
GrossAmt
Total A %
Total A $
Net Amt
100001
101
1
2
42650
0.611
26059.99
16589.99
100002
102
1
3
42650
0.628
26784.98
15864.99
100003
103
2
1
42650
0.40
17060.00
25590.00
I've been able to make some edits to a previous post to get the accounts, but can't seem to figure out the logic for the tiered lookup value in TransID 100003.
Ideally, I'd prefer to create this logic in a table value function (or two) and then incorporate it into a View that I'll use for reporting in a web form using C#.
SELECT
c.*,
[Total A %] = t.Total / c.GrossAmt,
[Total A $] = t.Total,
[Net Amt] = c.GrossAmt - t.Total
FROM #temp c
INNER JOIN Accounts a ON a.[Account ID] = c.[Account ID]
CROSS APPLY (
SELECT
Total = SUM((v.ActualMax - t.[Min]) * t.[Total A %])
FROM [dbo].[Tiered Table] t
CROSS APPLY (VALUES(
CASE WHEN c.GrossAmt < t.[Max] THEN c.GrossAmt ELSE t.[Max] END
)) v(ActualMax)
WHERE c.GrossAmt > t.[Min] AND t.[Account ID] = c.[Account ID]
) t;
Any ideas or guidance would be extremely helpful and appreciated.

It seems to be a simple matter of AND OR logic. You need to exclude rows which are TypeID = 2 and also have their maximum below the level of GrossAmt.
Then you just conditionally aggregate either the total amount (for tiered rows only, there will be only one row) or just the amount for that tier (for cumulative tiers).
SELECT
c.*,
[Total A %] = t.Total / c.GrossAmt,
[Total A $] = t.Total,
[Net Amt] = c.GrossAmt - t.Total
FROM CurrentData c
INNER JOIN Accounts a ON a.[AccountID] = c.[AccountID]
CROSS APPLY (
SELECT
Total = SUM(CASE WHEN t.TypeID = 2 THEN v.GrossAmt ELSE (v.ActualMax - t.[Min]) END * t.[Total A])
FROM [dbo].[Tiers] t
CROSS APPLY (VALUES(
CASE WHEN c.GrossAmt < t.[Max] THEN c.GrossAmt ELSE t.[Max] END,
c.GrossAmt
)) v(ActualMax, GrossAmt)
WHERE t.[AccountID] = c.[AccountID]
AND t.TypeID = c.TypeID
AND t.RegionID = c.RegionID
AND c.GrossAmt > t.[Min]
AND (t.TypeID = 1 OR c.GrossAmt <= t.Max)
) t;
db<>fiddle
The second CROSS APPLY is only necessary because of aggregating outer values. You don't need this if you place it in a function, as shown in your previous question.
Note that you should use half-open intervals here. In other words, either Min or Max should be exclusive. Otherwise there may be values that can "fall through the cracks".

Related

Need to group records based on matching reversal in sql

I have a tricky scenario to aggregate the data.
Data in my source table is as follows.
CustomerId Transaction Type Transaction Amount
1 Payment 100
1 ReversePayment -100
1 payment 100
1 ReversePayment -100
1 Payment 100
1 Payment 100
Requirement is as follows:
If the payment as a assoociated Reversepayment with matched amount, sum these two records.
If the payment does not have an associated Reverse payment, consider it as orphan(dont sum it).
I want output to be like this.
CustomerId Transaction Type Transaction Amount
1 Payment,ReversePayment 0
1 payment,ReversePayment 0
1 payment 100
1 Payment 100
In this scenario,
First record which is payment has an associated reverse payment (2nd record), Hence the sum becomes 0
Third record which is payment has an associated reverse payment (4th record), then the sum becomes 0
Fifth and sixth does not have associated reversals. dont sum these records.
Second Example:
Data in the source as follows:
CustomerId Transaction Type Transaction Amount
1 Payment 100
1 ReversePayment -100
1 payment 300
1 ReversePayment -300
1 Payment 400
1 Payment 500
Expected Output
CustomerId Transaction Type Transaction Amount
1 Payment,ReversePayment 0
1 payment,ReversePayment 0
1 payment 400
1 Payment 500
Second example requirement:
-As first and second records (payment and its associated reverse payment got
matched) ,sum these two records, output is 0.
- As third and fourth records (payment and its associated reverse payment got
matched), sum these two records, output is 0.
- Fifth and sixth does not have associated reversals. don't sum these records.
I got solutions in group, but data is not always guaranteed to have orphan records as 'payments'. Some times they are 'Payments' and some times they are 'ReversePayments'. Can some help me get ouptut like the below (using rank or rownumber functions ) so that i can group by using RRR column.
CustomerId Transaction Type Transaction Amount RRR
1 Payment 100 1
1 ReversePayment -100 1
1 payment 100 2
1 ReversePayment -100 2
1 Payment 100 3
1 Payment 100 4
CustomerId Transaction Type Transaction Amount RRR
1 Payment 100 1
1 ReversePayment -100 1
1 payment 300 2
1 ReversePayment -300 2
1 Payment 400 3
1 Payment 500 4
You can enumerate the different types and then aggregate:
select customerid,
listagg(ttype, ',') within group (order by ttype) as types,
sum(amount) as amount
from (select t.*,
row_number() over (partition by customerid, ttype, amount order by customerid) as seqnum
from t
) t
group by customerid, seqnum;
Edited to include your second scenario:
Using rownum to enforce inherent ordering (i.e. transactions happened in the order you've listed ), since your example is missing a transaction id or transaction time
SQL> select * from trans_data2;
CUSTOMER_ID TRANSACTION_TY TRANSACTION_AMOUNT
----------- -------------- ------------------
1 Payment 100
1 ReversePayment -100
1 payment 300
1 ReversePayment -300
1 Payment 400
1 Payment 500
6 rows selected.
SQL> select customer_id,
2 case
3 when upper(next_transaction) = 'REVERSEPAYMENT' then transaction_type||','||next_transaction
4 else transaction_type
5 end transaction_type,
6 case
7 when upper(next_transaction) = 'REVERSEPAYMENT' then transaction_amount + next_transaction_amount
8 else transaction_amount
9 end transaction_amount
10 from (
11 select customer_id, transaction_type, transaction_amount,
12 lead (transaction_type) over ( partition by customer_id order by transaction_id ) next_transaction,
13 nvl(lead (transaction_amount) over ( partition by customer_id order by transaction_id),0) next_transaction_amount
14 from ( select rownum transaction_id, t.* from trans_data2 t )
15 ) where upper(transaction_type) = 'PAYMENT'
16 ;
CUSTOMER_ID TRANSACTION_TYPE TRANSACTION_AMOUNT
----------- ----------------------------- ------------------
1 Payment,ReversePayment 0
1 payment,ReversePayment 0
1 Payment 400
1 Payment 500

Computing rolling average and standard deviation by dates

I have the below table where I will need to compute the rolling average and standard deviation based on the dates. I have listed below the tables and expected results. I am trying to compute the rolling average for an id based on date. rollAvgA is computed based on metricA. For example, for the first occurrence of id for a particular date the result should return zero as it does not have any preceding values. Please let me know how this can be accomplished?
Current Table :
Date id metricA
8/1/2019 100 2
8/2/2019 100 3
8/3/2019 100 2
8/1/2019 101 2
8/2/2019 101 3
8/3/2019 101 2
8/4/2019 101 2
Expected Table :
Date id metricA rollAvgA
8/1/2019 100 2 0
8/2/2019 100 3 2.5
8/3/2019 100 2 2.3
8/1/2019 101 2 0
8/2/2019 101 3 2.5
8/3/2019 101 2 2.3
8/4/2019 101 2 2.25
You seem to want a cumulative average. This is basically:
select t.*,
avg(metricA * 1.0) over (partition by id order by date) as rollingavg
from t;
The only caveat is that the first value is an average of one value. To handle this, use a case expression:
select t.*,
(case when row_number() over (partition by id order by date) > 1
then avg(metricA * 1.0) over (partition by id order by date)
else 0
end) as rollingavg
from t;

Dividing a sum value into multiple rows due to field length constraint

I am migrating financial data from a very large table (100 million+ of rows) by summarizing the amount and insert them into summary table. I ran into problem when the summary amount (3 billions) is larger than what the field in the summary table can hold (can only hold up to 999 millions.) Changing the field size is not an option as it requires a change process.
The only option I have is to divide the amount (the one that breach the size limit) into smaller ones so it can be inserted into the table.
I came across this SQL - I need to divide a total value into multiple rows in another table which is similar except the number of rows I need to insert is dynamic.
For simplicity, this is how the source table might look like
account_table
acct_num | amt
-------------------------------
101 125.00
101 550.00
101 650.00
101 375.00
101 475.00
102 15.00
103 325.00
103 875.00
104 200.00
104 275.00
The summary records are as follows
select acct_num, sum(amt)
from account_table
group by acct_num
Account Summary
acct_num | amt
-------------------------------
101 2175.00
102 15.00
103 1200.00
104 475.00
Assuming the maximum value in the destination table is 1000.00, the expected output will be
summary_table
acct_num | amt
-------------------------------
101 1000.00
101 1000.00
101 175.00
102 15.00
103 1000.00
103 200.00
104 475.00
How do I create a query to get the expected result? Thanks in advance.
You need a numbers table. If you have a handful of values, you can define it manually. Otherwise, you might have one on hand or use a similar logic:
with n as (
select (rownum - 1) as n
from account_table
where rownum <= 10
),
a as (
select acct_num, sum(amt) as amt
from account_table
group by acct_num
)
select acct_num,
(case when (n.n + 1) * 1000 < amt then 1000
else amt - n.n * 1000
end) as amt
from a join
n
on n.n * 1000 < amt ;
A variation along these lines might give some ideas (using the 1,000 of your sample data):
WITH summary AS (
SELECT acct_num
,TRUNC(SUM(amt) / 1000) AS times
,MOD(SUM(amt), 1000) AS remainder
FROM account_table
GROUP BY acct_num
), x(acct_num, times, remainder) AS (
SELECT acct_num, times, remainder
FROM summary
UNION ALL
SELECT s.acct_num, x.times - 1, s.remainder
FROM summary s
,x
WHERE s.acct_num = x.acct_num
AND x.times > 0
)
SELECT acct_num
,CASE WHEN times = 0 THEN remainder ELSE 1000 END AS amt
FROM x
ORDER BY acct_num, amt DESC
The idea is to first build a summary table with div and modulo:
ACCT_NUM TIMES REMAINDER
101 2 175
102 0 15
103 1 200
104 0 475
Then perform a hierarchical query on the summary table based on the number of "times" (i.e. rows) you want, with an extra for the remainder.
ACCT_NUM AMT
101 1000
101 1000
101 175
102 15
103 1000
103 200
104 475

Opening Stock, Closing Stock By Date sql Query

I have two table with primary key and foreign key (MaterialId)
Material Table (Multiple Material)
MaterialId MaterialName OpeningStock
1 Pen 100
2 Pencil 50
Material Stock (Multiple Material Entry)
MaterialId PurchaseQty SalesQty Date
1 500 0 2016-12-15
1 0 0 2016-12-16
1 300 0 2016-12-17
1 0 400 2016-12-18
1 0 0 2016-12-19
1 0 0 2016-12-20
1 0 400 2016-12-21
1 200 100 2016-12-22
Now When I Pass #FromDate and #Todate
I want to output like below:
Date MaterialName OpeningStock PurchaseQty SalesQty ClosingStock
2016-12-15 Pen 100 500 0 600
2016-12-16 Pen 600 0 0 600
2016-12-17 Pen 600 300 0 900
2016-12-18 Pen 900 0 400 500
2016-12-19 Pen 500 0 0 500
2016-12-20 Pen 500 0 0 500
2016-12-21 Pen 500 0 400 100
2016-12-22 Pen 100 200 100 200
Note :
1. If Something is wrong on database tables so, please guide me how to handle this situation.
2. And Also find Current Date Stock From Two Tables
You are looking for a rolling sum of the various quantity values. One way to do this is using correlated subqueries:
SELECT
t1.Date,
mt.MaterialName,
(SELECT OpeningStock FROM [Material Table] WHERE MaterialId = t1.MaterialId) +
COALESCE((SELECT SUM(t2.PurchaseQty - t2.SalesQty) FROM [Material Stock] t2
WHERE t2.Date < t1.Date AND t1.MaterialId = t2.MaterialId), 0) AS OpeningStock,
t1.PurchaseQty,
t1.SalesQty,
(SELECT OpeningStock FROM [Material Table] WHERE MaterialId = t1.MaterialId) +
COALESCE((SELECT SUM(t2.PurchaseQty - t2.SalesQty) FROM [Material Stock] t2
WHERE t2.Date <= t1.Date AND t1.MaterialId = t2.MaterialId), 0) AS ClosingStock
FROM [Material Stock] t1
INNER JOIN [Material Table] mt
ON t1.MaterialId = mt.MaterialId
ORDER BY
mt.MaterialName,
t1.Date;
Note that it is bad table design to be storing the opening stock values in a separate table from the material stock table. This means the above query would return no pencil records. A better approach would be to insert a seed record into material stock, for each material, with the amount being the initial stock.
Output:
Demo here:
Rextester
Simply do as below :
SELECT S.DATE, M.MaterialName, M.OpeningStock, S.PurchaseQty, S.SalesQty, SUM((M.OpeningStock+S.PurchaseQty)-S.SalesQty)ClosingStock FROM #TABLE
(
SELECT * FROM MaterialTABLE
) M
INNER JOIN Material S ON S.MaterialId = M.MaterialId where s.date between #FromDate and #Todate

SQL Group by range and total column

With the following function and stored procedure i get a resultset with 2 columns.
What i need additional is a third column total for all open invoices inner score range
It would great for any idea.
ALTER FUNCTION dbo.OpenOrders
(
#MandantId int
)
RETURNS TABLE
AS
RETURN
SELECT SUM(Invoices.Amount - ISNULL(Payment.Amount, 0)) AS Op
FROM Invoices INNER JOIN
Orders ON Invoices.OrderId = Orders.OrderId LEFT OUTER JOIN
Payment ON Invoices.InvoiceId = Payment.InvoiceId
WHERE (Orders.MandantId = #MandantId)
GROUP BY Invoice.InvoiceId, Invoices.Amount
HAVING (SUM(Invoices.Amount - ISNULL(Payment.Amount, 0)) <> 0)
ALTER PROCEDURE dbo.GetOpRanges
#MandantId int
AS
BEGIN
SELECT * INTO #tmp_ranges
FROM
//wrong in first post -> OPDebitorByMandant(#MandantId)
OpenOrders(#MandantId)
SELECT op AS [score range], COUNT(*) AS [number of occurences]
FROM
(SELECT CASE
WHEN op BETWEEN 0 AND 50 THEN ' 0- 50'
WHEN op BETWEEN 50 AND 100 THEN ' 50-100'
WHEN op BETWEEN 100 AND 500 THEN '100-500'
ELSE '500 and >' END AS op
FROM [#tmp_ranges]) AS t
GROUP BY op
RETURN
Result:
score range number of occurences range
------------------+-------------
0- 50 23
50-100 4
100-500 4
500 and > 21
What i need additional is a third column total for all open invoices inner score range.
Target result:
score range number of occurences Total
-----------+--------------------+------
0- 50 23 1.150
50-100 4 400
100-500 4 2.000
500 and > 21 22.000
Tables:
Invoices
InvoiceId CustomerId OrderId DateOfInvoice Amount
----------+----------+-------+-------------+------
1 1 20 20160301 1000.00
2 2 22 20160501 2000.00
3 1 102 20160601 3000.00
...
Orders
OrderId MandantId CustomerId DateOfOrder Amount
-------+---------+----------+-----------+-----------
20 1 1 20160101 1000.00
22 1 2 20160101 2000.00
102 1 1 20160101 3000.00
...
Payment
PaymentId MandantId CustomerId InvoiceId OrderId DateOfPayment Amount
---------+---------+----------+---------+-------+-------------+-------------
1 1 1 1 20 20160310 1000.00
2 1 2 2 22 20160505 2000.00
3 1 1 3 102 20160610 3000.00
...
hope it's helpfull and thanks again in advance for any solution