Conditionally move values between columns - sql-server

I have the following record set output:
ID Name Pay_Type Paid_Amnt Interest_Amnt
1 John Smith Benefit 1075 0
1 John Smith Interest 1.23 0
2 Tom Ryder Benefit 1123 0
3 Mark Thompson Benefit 1211 0
3 Mark Thompson Interest 1.34 0
What I'd like is for values with the Pay_Type = Interest to be placed in the Interest column.
Desired output:
ID Name Pay_Type Pay_Type 2 Paid_Amnt Interest_Amnt
1 John Smith Benefit Interest 1075 1.23
2 Tom Ryder Benefit NULL 1123 0
3 Mark Thompson Benefit Interest 1211 1.34
I tried something like the following:
Select row_number()over(partition by id, case when pay_type = 'Interest' then interest_amnt = paid_amnt
when pay_type = 'Interest' then paid_amnt = 0 end) as new_interest
Does anyone know how to get the desired results?
Thank you
declare #t table(id int, pay_type varchar(25), name varchar(100), paid_amnt float, interest_amnt float)
insert into #t values(1, 'Benefit', 'John Smith', 1075, 0),
(1, 'Interest', 'John Smith',1.23, 0),
(2, 'Benefit', 'Tom Ryder', 1123, 0),
(3, 'Benefit', 'Mark Thompson', 1211, 0),
(4, 'Interest', 'Mark Thompson', 1.34, 0)
select * from #t

Just in case you can have more than 2 records per person, I believe this will give you what you want, it utilizes a couple of subqueries and group by,
subquery x groups your records so you get the interest sums and benefits sums in a row per user,
subquery y uses CASE expressions to place the summed amounts into their proper columns or zero in case of it being Benefit/Interest and adds the pay type columns of pay_type1 and pay_type2 with values of Benefit and Interest respectively,
outer query groups everything together into 1 row per user, and sums their interest and benefit columns respectively:
SELECT y.[id] AS [ID], y.[name] AS [Name],
y.[pay_type1] AS [Pay_Type], y.[Pay_Type2], SUM(y.[Paid_Amnt]) AS [Paid_Amnt],
SUM(y.[Interest_Amnt]) AS [Interest_Amnt]
FROM
(
SELECT id, name, 'Benefit' AS [pay_type1], 'Interest' AS [pay_type2],
CASE WHEN pay_type = 'Benefit' THEN x.Amount ELSE 0 END AS [Paid_Amnt],
CASE WHEN pay_type = 'Interest' THEN x.Amount ELSE 0 END AS [Interest_Amnt]
FROM
(
SELECT id, pay_type, name, SUM(paid_amnt) AS [Amount]
FROM table as t
GROUP BY id, pay_type, name
) AS x
) AS y
GROUP BY y.[id], y.[name], y.[pay_type1], y.[pay_type2]

Related

Creating SQL Server (T-SQL) view that flattens nulls

I'm having a lot of difficulty trying to create a view that flattens the data without nulls. I've supplied the code that creates two basic tables and my view code so you can see what I've tried so far. Please note that the two tables do not have a matching primary or foreign key column, so the summary in the view is created by just joining on City. I can't use XML because my team of data analysts all have intermediate skills and won't be able to understand it. I considered using a recursive CTE, but I can't get it right. The result produces 6 lines but I want 3 lines.
Thanks for any ideas about a better way to achieve this.
CREATE TABLE A (
OrdID int,
Cat varchar(255),
Qty int,
City varchar(255),
Ctry varchar(255)
);
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (1, 'TV', 5,'London', 'England');
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (2, 'Laptop', 3,'London', 'England');
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (3, 'Laptop', 4, 'Berlin', 'Germany');
CREATE TABLE Cust (
CustID int,
CustType varchar(255),
City varchar(255),
NumItems int,
);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (1, 'New', 'London', 2);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (2, 'Returning','London', 5);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (3, 'Returning','Berlin', 2);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (4, 'New','Berlin', 8);
alter view My_View
as
With CTE_FlattenNulls
as
(
Select
S.Cat
, S.Qty
, S.City
, S.Ctry
, case when C.CustType like 'New' then sum(C.NumItems) end as NewC
, case when C.CustType like 'Returning' then sum(C.NumItems) end as RetC
from A as S
left join Cust as C
on S.City = C.City
group by
S.Cat
, S.Qty
, S.City
, S.Ctry
, C.CustType
)
select
Cat
,Qty
,City
,Ctry
,NewC
,RetC
,SUM(IsNull(NewC, 0) + IsNull(RetC, 0)) as TotC
from CTE_FlattenNulls
group by
Cat
,Qty
,City
,Ctry
,NewC
,RetC
go
Just adding the output that I wanted:
Cat
Qty
City
Cntry
NewC
RetCust
TotC
Laptop
4
Berlin
Germany
8
2
10
Laptop
3
London
England
2
5
7
TV
5
London
England
2
5
7
You were very close.
See comments in code for explanation.
With CTE_FlattenNulls
as
(
Select S.Cat, S.Qty, S.City, S.Ctry,
-- To do conditional summation case expression needs to be inside the SUM function
sum( case when C.CustType like 'New' then C.NumItems else 0 end ) as NewC,
sum( case when C.CustType like 'Returning' then C.NumItems else 0 end ) as RetC
from A as S
left join Cust as C
on S.City = C.City
group by
S.Cat
, S.Qty
, S.City
, S.Ctry
-- then you do not need to group by this column and therefore you do not get extra rows
--, C.CustType
)
select Cat, Qty, City, Ctry, NewC, RetC,
-- As per your example, you would no longer need GROUP BY,
-- therefore SUM function should be removed
SUM(IsNull(NewC, 0) + IsNull(RetC, 0)) as TotC
from CTE_FlattenNulls
-- As per your example, you would no longer need GROUP BY
group by Cat, Qty, City, Ctry
-- ,NewC -- Definitely not needed anymore
-- ,RetC -- Definitely not needed anymore
Everything else stays the same
To get to your result, why can you not just do a simple group by with conditional sum ?
It has no need for a CTE
See this example, also in this DBFiddle
select A.Cat,
A.Qty,
A.City,
min(A.Ctry) as Country,
sum(case when C.CustType = 'New' then C.NumItems else 0 end) as NewC,
sum(case when C.CustType = 'Returning' then C.NumItems else 0 end) as RetCust,
sum(C.NumItems) as TotC
from A
join Cust C on A.City = C.City
group by A.Cat,
A.Qty,
A.City
order by A.Cat, A.City
it returns this
Cat
Qty
City
Country
NewC
RetCust
TotC
Laptop
4
Berlin
Germany
8
2
10
Laptop
3
London
England
2
5
7
TV
5
London
England
2
5
7

How to select changed columns

The Problem
I'm trying to detect and react to changes in a table where each update is being recorded as a new row with some values being the same as the original, some changed (the ones I want to detect) and some NULL values (not considered changed).
For example, given the following table MyData, and assuming the OrderNumber is the common value,
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
2 456 Base Inc. AL337 7 20.15 2020-02-03
3 123 NULL WG301b 5 19.57 2020-01-02
If I execute the query for OrderNumber = 123 I would like the following data returned:
Column OldValue NewValue
ID 1 3
PartNumber WG301 WG301b
Qty 4 5
Price 15.02 19.57
Or possibly a single row result with only the changes filled, like this (however, I would strongly prefer the former format):
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
3 NULL NULL WG301b 5 19.57 NULL
My Solution
I have not had a chance to test this, but I was considering writing the query with the following approach (pseudo-code):
select
NewOrNull(last.ID, prev.ID) as ID,
NewOrNull(last.OrderNumber, prev.OrderNumber) as OrderNumber
NewOrNull(last.CustomerName, prev.CustomerName) as CustomerName,
...
from last row with OrderNumber = 123
join previous row where OrderNumber = 123
Where the function NewOrNull(lastVal, prevVal) returns NULL if the values are equal or lastVal value is NULL, otherwise the lastVal.
Why I'm Looking for an Answer
I'm afraid that the ugly join, the number of calls to the function, and the procedural approach may make this approach not scalable. Before I start down the rabbit hole, I was wondering...
The Question
...are there any other approaches I should try, or any best practices to solving this specific type of problem?
I came up with a solution for the second (less preferred) format:
The Data
Using the following data:
INSERT INTO MyData
([ID], [OrderNumber], [CustomerName], [PartNumber], [Qty], [Price], [OrderDate])
VALUES
(1, 123, 'Acme Corp.', 'WG301', '4', '15.02', '2020-01-02'),
(2, 456, 'Base Inc.', 'AL337', '7', '20.15', '2020-02-03'),
(3, 123, NULL, 'WG301b', '5', '19.57', '2020-01-02'),
(4, 123, 'ACME Corp.', 'WG301b', NULL, NULL, '2020-01-02'),
(6, 456, 'Base Inc.', NULL, '7', '20.15', '2020-02-05');
The Function
This function returns the updated value if it has changed, otherwise NULL:
CREATE FUNCTION dbo.NewOrNull
(
#newValue sql_variant,
#oldValue sql_variant
)
RETURNS sql_variant
AS
BEGIN
DECLARE #ret sql_variant
SELECT #ret = CASE
WHEN #newValue IS NULL THEN NULL
WHEN #oldValue IS NULL THEN #newValue
WHEN #newValue = #oldValue THEN NULL
ELSE #newValue
END
RETURN #ret
END;
The Query
This query returns the history of changes for the given order number:
select dbo.NewOrNull(new.ID, old.ID) as ID,
dbo.NewOrNull(new.OrderNumber, old.OrderNumber) as OrderNumber,
dbo.NewOrNull(new.CustomerName, old.CustomerName) as CustomerName,
dbo.NewOrNull(new.PartNumber, old.PartNumber) as PartNumber,
dbo.NewOrNull(new.Qty, old.Qty) as Qty,
dbo.NewOrNull(new.Price, old.Price) as Price,
dbo.NewOrNull(new.OrderDate, old.OrderDate) as OrderDate
from MyData new
left join MyData old
on old.ID = (
select top 1 ID
from MyData pre
where pre.OrderNumber = new.OrderNumber
and pre.ID < new.ID
order by pre.ID desc
)
where new.OrderNumber = 123
The Result
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
3 (null) (null) WG301b 5 19.57 (null)
4 (null) ACME Corp. (null) (null) (null) (null)
The Fiddle
Here's the SQL Fiddle that shows the whole thing in action.
http://sqlfiddle.com/#!18/b720f/5/0

Filling a complete line of rows with last_value() oracle

First of all I have this table.
CREATE TABLE tabla_1
(table_num NUMBER, amount NUMBER, first_name VARCHAR2(100), l_name VARCHAR2(100));
INSERT INTO tabla_1 (table_num, amount, first_name,l_name) VALUES (1,1,'Luis', 'Rosas');
INSERT INTO tabla_1 (table_num, amount, first_name,l_name) VALUES (1,1,'Carlos','Borolas');
INSERT INTO tabla_1 (table_num, amount, first_name,l_name) VALUES (1,2,'Elena','Zeta');
INSERT INTO tabla_1 (table_num, amount, first_name,l_name) VALUES (1,null,null,null);
INSERT INTO tabla_1 (table_num, amount, first_name,l_name) VALUES (2,null,null,null);
commit;
query result:
TABLE_NUM AMOUNT FIRST_NAME L_NAME
---------- ---------- ---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
1 1 Luis Rosas
1 1 Carlos Borolas
1 2 Elena Zeta
1 -1 'N/A' 'N/A'
2 -1 'N/A' 'N/A'
Expected result:
TABLE_NUM AMOUNT FIRST_NAME L_NAME
---------- ---------- ----------------------------------------------------------------------------- ---------------------------------------------------------------
1 1 Luis Rosas
1 1 Carlos Borolas
1 2 Elena Zeta
1 2 Elena Zeta
2 2 Elena Zeta
sorry for the Spanish table, English it's not my native language.
The question is:
Note: In the last rows I have 'N/A' this is because this values was already null inserted. (I can't insert null values on a table, at least I have to insert something -1 on null numbers and 'n/a' on null varchar2 ), This is already done with code.
2.- How I can replace this """null"""" values with the previous last not """""null"""" values known by the user?
3.- which is better last_value or LAG, and how I can use it? Is it possible to use lag or last value in a complete row?
NVL + LAST_VALUE might be one option:
SQL> select table_num,
2 nvl(amount, last_value(amount ignore nulls) over (order by table_num)) amount,
3 nvl(first_name, last_value(first_name ignore nulls) over (order by table_num)) first_name
4 from tabla_1;
TABLE_NUM AMOUNT FIRST_NAME
---------- ------ --------------------
1 1 Carlos
1 1 Luis
1 2 Elena
1 2 Elena
2 2 Elena
SQL>
If there aren't NULLs (as sample data suggest) but -1 and N/A, then use CASE:
select table_num,
case when amount = -1 then last_value(amount ignore nulls) over (order by table_num)
else amount
end amount,
--
case when first_name = 'N/A' then last_value(first_name ignore nulls) over (order by table_num)
else first_name
end first_name
from tabla_1

Distribute multiple payments to invoice lines

I'm having a problem allocating payments to invoice lines.
Data looks like this:
Invoice lines table (sales):
lineId invoiceId value
1 1 100
2 1 -50
3 1 40
4 2 500
Payments table (payments):
paymentId invoiceId amount
1 1 50
2 1 40
3 2 300
Now, I want to know for each invoice line the payment details. The payments shall be allocated first to the smallest values (i.e. line 2, -50)
The output should look like this:
lineId invoiceId value paymentId valuePaid valueUnpaid
2 1 -50 1 -50 0
3 1 40 1 40 0
1 1 100 1 60 40
1 1 100 2 40 0
4 2 500 3 300 200
The problem is solved in the post below, but the solution does not work if you have negative invoice values or if you have to split an invoice line in two payments.
https://dba.stackexchange.com/questions/58474/how-can-i-use-running-total-aggregates-in-a-query-to-output-financial-accumulati/219925?noredirect=1#comment431486_219925
This is what I've done so far based on the article above:
drop table dbo.#sales
drop table dbo.#payments
CREATE TABLE dbo.#sales
( lineId int primary key, -- unique line id
invoiceId int not null , -- InvoiceId foreign key
itemValue money not null ) -- value of invoice line.
CREATE TABLE dbo.#payments
( paymentId int primary key, -- Unique payment id
InvoiceId int not null, -- InvoiceId foreign key
PayAmount money not null
)
-- Example invoice, id #1, with 3 lines, total ammount = 90; id #2, with one line, value 500
INSERT dbo.#sales VALUES
(1, 1, 100),
(2, 1, -50),
(3, 1, 40),
(4, 2, 500) ;
-- Two payments paid towards invoice id#1, 50+40 = 90
-- One payment paid towards invoice id#2, 300
INSERT dbo.#Payments
VALUES (1, 1, 50),
(2, 1, 40),
(3, 2, 300);
-- Expected output should be as follows, for reporting purposes.
/* lineId, invoiceId, value, paymentId, valuePaid, valueUnpaid
2, 1, -50, 1, -50, 0
3, 1, 40, 1, 40, 0
1, 1, 100, 1, 60, 40
1, 1, 100, 2, 40, 0
4, 2, 500, 3, 300, 200 */
WITH inv AS
( SELECT lineId, invoiceId,
itemValue,
SumItemValue = SUM(itemValue) OVER
(PARTITION BY InvoiceId
ORDER BY ItemValue Asc
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)
FROM dbo.#Sales
)
, pay AS
( SELECT
PaymentId, InvoiceId, PayAmount as PayAmt,
SumPayAmt = SUM(PayAmount) OVER
(PARTITION BY InvoiceId
ORDER BY PaymentId
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)
FROM dbo.#payments
)
SELECT
inv.lineId,
inv.InvoiceId,
inv.itemValue,
pay.PaymentId,
PaymentAllocated =
CASE WHEN SumPayAmt <= SumItemValue - itemValue
OR SumItemValue <= SumPayAmt - PayAmt
THEN 0
ELSE
CASE WHEN SumPayAmt <= SumItemValue THEN SumPayAmt
ELSE SumItemValue END
- CASE WHEN SumPayAmt-PayAmt <= SumItemValue-itemValue
THEN SumItemValue-itemValue
ELSE SumPayAmt-PayAmt END
END
FROM inv JOIN pay
ON inv.InvoiceId = pay.InvoiceId
ORDER BY
inv.InvoiceId,
pay.PaymentId;
The current output is:
lineId InvoiceId itemValue PaymentId PaymentAllocated
2 1 -50.00 1 0.00
3 1 40.00 1 0.00
1 1 100.00 1 50.00
2 1 -50.00 2 0.00
3 1 40.00 2 0.00
1 1 100.00 2 40.00
4 2 500.00 3 300.00
Any direction will be appreciated. Thank you.
More info on the allocation rules:
Allocating first payment to the smallest sale (i.e. -50) was just a
convention to insure all sales lines get payments. If I’d allocate
arbitrary or with another rule, and line 1 (value 100) would get the
first payment, I’d use all the payment for this line and the rest of
the invoice would remain unallocated.
As I said, it’s just an convention. If someone else comes with a
different rule that works, it’s ok. Actually, the structure is
simplified compared with the production tables: payments also have a
payment date, type, … and a correct distribution should tell us what
invoice lines were paid at each payment time.
Payments are restricted by the logic of the system to be smaller then
the sum of the invoice lines. Well, it might be a case when payments
are greater: the total invoice is negative (ie: -100). In this case
we can insert in the payments table amounts in the range of -100: 0
and Total Payments are restricted to -100
In the end I found quite a simple and natural sollution - to allocate payments based on the percentage of each payment in the total value of the invoice.
drop table dbo.#sales
drop table dbo.#payments
CREATE TABLE dbo.#sales
( lineId int primary key, -- unique line id
invoiceId int not null , -- InvoiceId foreign key
itemValue money not null ) -- value of invoice line.
CREATE TABLE dbo.#payments
( paymentId int primary key, -- Unique payment id
InvoiceId int not null, -- InvoiceId foreign key
PayAmount money not null
)
-- Example invoice, id #1, with 3 lines, total ammount = 90; id #2, with one line, value 500
INSERT dbo.#sales VALUES
(1, 1, 100),
(2, 1, -50),
(3, 1, 40),
(4, 2, 500) ;
-- Two payments paid towards invoice id#1, 50+40 = 90
-- One payment paid towards invoice id#2, 300
INSERT dbo.#Payments
VALUES (1, 1, 50),
(2, 1, 40),
(3, 2, 300);
SELECT
s.lineId,
s.InvoiceId,
s.itemValue,
p.PayAmount,
p.PaymentId,
round(p.PayAmount / ts.SumItemValue,3) as PaymentPercent,
s.ItemValue * round(p.PayAmount / ts.SumItemValue,3) as AllocatedPayment
FROM dbo.#sales s
LEFT JOIN dbo.#payments p
ON s.InvoiceId = p.InvoiceId
LEFT JOIN (SELECT invoiceId, sum(itemValue) as SumItemValue FROM dbo.#sales GROUP BY invoiceId) ts
ON s.invoiceId = ts.invoiceId
ORDER BY
s.InvoiceId,
p.PaymentId;
And the resunt looks like this:
lineId InvoiceId itemValue PayAmount PaymentId PaymentPercent AllocatedPayment
1 1 100.00 50.00 1 0.556 55.60
2 1 -50.00 50.00 1 0.556 -27.80
3 1 40.00 50.00 1 0.556 22.24
3 1 40.00 40.00 2 0.444 17.76
2 1 -50.00 40.00 2 0.444 -22.20
1 1 100.00 40.00 2 0.444 44.40
4 2 500.00 300.00 3 0.60 300.00

Sum of coloumn based on two condition in where cluase

I have table structure
ACNo Deal PRODTYPE INT AMT
100 123 OS 23
100 123 EX 7
I have to group by Deal,AC NO and have to report other column as well, but for Int amount have to do sum of for both prod type.
How I can implement it?
Result
AC No Deal Total INT
100 123 30
Try this
SELECT AccNo, DEAL, SUM([INT AMT]) AS Total Amount
FROM Table
GROUP BY AccNo, DEAL
something like this ?
select ACNo,
Deal,
sum([INT AMT]) as [Total INT]
from unknowntablename
group by ACNo, Deal
You can test this like this :
declare #table table (ACNo int, Deal int, PRODTYPE varchar(2), [INT AMT] int)
insert into #table values (100, 123, 'OS', 23)
insert into #table values (100, 123, 'EX', 7)
select ACNo,
Deal,
sum([INT AMT]) as [Total INT]
from #table
group by ACNo, Deal
the outcome will be :
ACNo Deal Total INT
100 123 30

Resources