How to get data on a single row - tsql

I have table called RUGS with the data below. How do I write a TSQl query to get the data as shown in Output. I am not familiar with unPIVOT
`cono ARtype days Revenue PPD
140 MCD 5 1000 500
140 MRA 6 2000 600
140 MRA 7 3000 700
141 MCD 1 5000 100
141 MRA 2 6000 200
141 MRA 3 7000 300`
Result
140 MCD 5 1000 500 MRA 6 2000 600 MRA 7 3000 700
141 MCD 1 5000 100 MRA 2 6000 200 MRA 3 7000 300

Given that every cono will have exactly 3 records (as stated in the comments), a cte with row_number can be used with case statements.
If any have less than three records, you will see blanks and zeroes in the results. Any with more than three will not have all records represented.
Here is an example with #RUGS as a table variable:
declare #RUGS table (cono int, ARType char(3), [days] int, Revenue int, PPD int)
insert into #RUGS VALUES
(140,'MCD',5,1000,500)
,(140,'MRA',6,2000,600)
,(140,'MRA',7,3000,700)
,(141,'MCD',1,5000,100)
,(141,'MRA',2,6000,200)
,(141,'MRA',3,7000,300);
with cte as
(
select row_number() over(partition by cono order by (select 1)) as rn, * from #RUGS
)
select cono,
max(case when rn = 1 then ARType else '' end) as ARType1,
max(case when rn = 1 then days else '' end) as days1,
max(case when rn = 1 then Revenue else '' end) as Revenue1,
max(case when rn = 1 then PPD else '' end) as PPD1,
max(case when rn = 2 then ARType else '' end) as ARType2,
max(case when rn = 2 then days else '' end) as days2,
max(case when rn = 2 then Revenue else '' end) as Revenue2,
max(case when rn = 2 then PPD else '' end) as PPD2,
max(case when rn = 3 then ARType else '' end) as ARType3,
max(case when rn = 3 then days else '' end) as days3,
max(case when rn = 3 then Revenue else '' end) as Revenue3,
max(case when rn = 3 then PPD else '' end) as PPD3
from cte group by cono

Related

Why is there a difference in my SQL output

This is puzzling me no end, and I know it might be tough without the data but thought it might be a longshot to post here.
Here goes, the first code I received was this
USE [Radiotherapy]
GO
if exists (
select * from tempdb.dbo.sysobjects o
where o.xtype in ('U')
and o.id = object_id(N'tempdb..#MySampleTemp')
)
DROP TABLE #MySampleTemp;
if exists (
select * from tempdb.dbo.sysobjects o
where o.xtype in ('U')
and o.id = object_id(N'tempdb..#MyPivotTemp')
)
DROP TABLE #MyPivotTemp;
SELECT [AttendanceNumber]
,CASE WHEN AgeAtExamDate BETWEEN 0 AND 5 THEN '0-5'
WHEN AgeAtExamDate BETWEEN 6 AND 18 THEN '6-18'
WHEN AgeAtExamDate BETWEEN 19 AND 150 THEN '19+'
ELSE 'Error' END AS AgeRange
,[LocalPatientIdentifier]
,[ExaminationDate]
,[ExamExaminationCode] INTO #MySampleTemp
FROM [dbo].[tblRadiologyData]
WHERE AttendanceSiteCode IN('CNM','RNM')
--AND AttendanceStatus NOT IN ( 'Appt', 'Booked In', 'Cancelled', 'Pending' )
--AND AttendancePatientGroup = 'Out Patient'
--AND AttendancePatientCategory IN ( 'EU', 'Military', 'N.H.S.' )
--AND AttendanceSourceName <> 'PACs Support'
AND [ExaminationDate] >= '1 OCTOBER 2015' --
ORDER BY [AttendanceNumber], CASE WHEN AgeAtExamDate BETWEEN 0 AND 5 THEN '0-5'
WHEN AgeAtExamDate BETWEEN 6 AND 18 THEN '6-18'
WHEN AgeAtExamDate BETWEEN 19 AND 150 THEN '19+'
ELSE 'Error' END, [LocalPatientIdentifier], [ExaminationDate], ExamExaminationCode
SELECT [AttendanceNumber],AgeRange,[LocalPatientIdentifier],[ExaminationDate], 1 AS ExamCount,
[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14] INTO #MyPivotTemp
FROM
(
SELECT *,
row_number() OVER(PARTITION BY [AttendanceNumber]
ORDER BY [AttendanceNumber], [LocalPatientIdentifier]) rn
FROM #MySampleTemp
) AS st
pivot
(
MAX(ExamExaminationCode)
FOR rn in ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14])
) AS pivottable
SELECT
[1] AS Exam01,
[2] AS Exam02,
[3] AS Exam03,
[4] AS Exam04,
[5] AS Exam05,
[6] AS Exam06,
[7] AS Exam07,
[8] AS Exam08,
[9] AS Exam09,
[10] AS Exam10,
[11] AS Exam11,
[12] AS Exam12,
[13] AS Exam13,
[14] AS Exam14,
COUNT(ExamCount) AS [No. Attendances]
FROM #MyPivotTemp
GROUP BY [1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14]
ORDER BY [1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14]
What I tried to do was replicate this in query as follows:
USE [Radiotherapy]
;With CTE AS (SELECT s.attendanceNumber,
MAX(CASE WHEN s.rnk = 1 THEN s.ExamExaminationCode END) as examCode1,
MAX(CASE WHEN s.rnk = 2 THEN s.ExamExaminationCode END) as examCode2,
MAX(CASE WHEN s.rnk = 3 THEN s.ExamExaminationCode END) as examCode3,
MAX(CASE WHEN s.rnk = 4 THEN s.ExamExaminationCode END) as examCode4,
MAX(CASE WHEN s.rnk = 5 THEN s.ExamExaminationCode END) as examCode5,
MAX(CASE WHEN s.rnk = 6 THEN s.ExamExaminationCode END) as examCode6,
MAX(CASE WHEN s.rnk = 7 THEN s.ExamExaminationCode END) as examCode7,
MAX(CASE WHEN s.rnk = 8 THEN s.ExamExaminationCode END) as examCode8,
MAX(CASE WHEN s.rnk = 9 THEN s.ExamExaminationCode END) as examCode9,
MAX(CASE WHEN s.rnk = 10 THEN s.ExamExaminationCode END) as examCode10,
MAX(CASE WHEN s.rnk = 11 THEN s.ExamExaminationCode END) as examCode11,
MAX(CASE WHEN s.rnk = 12 THEN s.ExamExaminationCode END) as examCode12,
MAX(CASE WHEN s.rnk = 13 THEN s.ExamExaminationCode END) as examCode13,
MAX(CASE WHEN s.rnk = 14 THEN s.ExamExaminationCode END) as examCode14
FROM (
SELECT [AttendanceNumber]
,[ExaminationDate]
,[ExamExaminationCode]
,ROW_NUMBER() OVER(PARTITION BY [AttendanceNumber]
ORDER BY [RadiologyID]) as rnk --Ordered by date ASC
FROM [Radiotherapy].[dbo].[tblRadiologyData] rd
where rd.ExaminationDate >= '01 october 2015'
and rd.AttendanceSiteCode IN('CNM','RNM') ) s
GROUP BY s.attendanceNumber)
Select CTE.examCode1,
CTE.examCode2,
CTE.examCode3,
CTE.examCode4,
CTE.examCode5,
CTE.examCode6,
CTE.examCode7,
CTE.examCode8,
CTE.examCode9,
CTE.examCode10,
CTE.examCode11,
CTE.examCode12,
CTE.examCode13,
CTE.examCode14,
COUNT(CTE.AttendanceNumber) as [No of occurances]
from CTE
GROUP by CTE.examCode1,
CTE.examCode2,
CTE.examCode3,
CTE.examCode4,
CTE.examCode5,
CTE.examCode6,
CTE.examCode7,
CTE.examCode8,
CTE.examCode9,
CTE.examCode10,
CTE.examCode11,
CTE.examCode12,
CTE.examCode13,
CTE.examCode14
ORDER BY CTE.examCode1
Great I thought until my code returned more results which puzzled me. Having broken it down I found the offending code from the original query:
ORDER BY [AttendanceNumber], CASE WHEN AgeAtExamDate BETWEEN 0 AND 5 THEN '0-5'
WHEN AgeAtExamDate BETWEEN 6 AND 18 THEN '6-18'
WHEN AgeAtExamDate BETWEEN 19 AND 150 THEN '19+'
ELSE 'Error' END, [LocalPatientIdentifier], [ExaminationDate], ExamExaminationCode
When I removed this from the first query the results matched, but my question is how and why is an ORDER BY affecting the output. I assumed this was just showing how the results are ordered? Understanding why code is doing what it's doing is something I really need to get my head around. Any advise more than welcome.
Probably it have something to do with this piece of code:
SELECT *,
row_number() OVER(PARTITION BY [AttendanceNumber]
ORDER BY [AttendanceNumber], [LocalPatientIdentifier]) rn
FROM #MySampleTemp
Obviously your ordering is not breaking a tie(it isn't deterministic) and it produces rank number differently when you have ordered data in temp table and when not ordered. Try to add some PK to your temp table so the above code will look like:
SELECT *,
row_number() OVER(PARTITION BY [AttendanceNumber]
ORDER BY [AttendanceNumber], [LocalPatientIdentifier], [SomePK]) rn
FROM #MySampleTemp
Then removing ordering from first select statement will not have an effect on the result set.

Building totals based on condition in another row

Really struggling to understand the best way of doing this...
I have a table of data
**StudentID AssessmentCode ResultGroup Result**
46933 12ENG IBLevel HL
46933 12ENG Mark 6
46933 12ECO IBLevel HL
46933 12ECO Mark 5
46933 12GEO IBLevel SL
46933 12GEO Mark 6
46933 12LAN IBLevel HL
46933 12LAN Mark 4
46933 12PED IBLevel SL
46933 12PED Mark 5
46933 12SCI IBLevel SL
46933 12SCI Mark 3
67767 12FRE IBLevel HL
67767 12FRE Mark 4
67767 12MAT IBLevel SL
67767 12MAT Mark 5
and so on...
Unfortunately the result column holds 2 different bits of info. The level a student does (HL = High Level; SL = Standard Level) and then the result for that subject at that level. Note that for each student, 2 rows are generated per subject a student does, one row has the IBLevel and the next row has the result for that level.
How can I rearrange the data to get something like
StudentID HLResult SLResult TotalResult CountofHL CountofSL
46933 15 14 29 3 3
67767 13 10 23 4 2
So each student has one row of data with totals for HL, SL, both together and then a count of the number of HL and SL subjects. As I said, really not sure of the best way of going about this. In the end, I would also like to extend this to get some columns which, based on the results give some warning messages eg the TotalResult has to be great than 24 so I would love a column that simply returns whether this has been achieved or not...
StudentID HLResult SLResult TotalResult CountofHL CountofSL MoreThan24
46933 15 14 29 3 3 True
67767 13 10 23 4 2 False
Any help would be greatly appreciated...
Your table seems to have some serious normalization issues. You have to perform an INNER JOIN on AssessmentCode first:
SELECT t1.StudentID, t1.AssessmentCode,
t1.Result AS Level, CAST(t2.Result AS INT) AS Mark
FROM (
SELECT StudentID, AssessmentCode, Result
FROM mytable
WHERE ResultGroup = 'IBLevel' ) AS t1
INNER JOIN (
SELECT StudentID, AssessmentCode, Result
FROM mytable
WHERE ResultGroup = 'Mark' ) AS t2
ON t1.StudentID = t2.StudentID AND t1.AssessmentCode = t2.AssessmentCode
to get one row per AssessmentCode.
Output:
StudentID AssessmentCode Level Mark
-----------------------------------------
46933 12ENG HL 6
46933 12ECO HL 5
46933 12GEO SL 6
46933 12LAN HL 4
46933 12PED SL 5
46933 12SCI SL 3
67767 12FRE HL 4
67767 12MAT SL 5
You can now wrap the above query in a CTE and perform conditional aggregation to get required result:
;WITH CTE AS (
... above query here
)
SELECT StudentID,
SUM(CASE WHEN Level = 'HL' THEN Mark ELSE 0 END) AS HLResult,
SUM(CASE WHEN Level = 'SL' THEN Mark ELSE 0 END) AS SLResult,
SUM(Mark) AS TotalResult,
COUNT(CASE WHEN Level = 'HL' THEN 1 END) AS CountofHL,
COUNT(CASE WHEN Level = 'SL' THEN 1 END) AS CountofSL,
CASE WHEN SUM(Mark) > 24 THEN 'True'
ELSE 'False'
END AS MoreThan24
FROM CTE
GROUP BY StudentID
Output:
StudentID HLResult SLResult TotalResult CountofHL CountofSL MoreThan24
46933 15 14 29 3 3 True
67767 4 5 9 1 1 False
Demo here
My Version of Your Table
DECLARE #yourTable TABLE
(
StudentID INT,
AssessmentCode CHAR(5),
ResultGroup VARCHAR(10),
Result VARCHAR(5)
)
INSERT INTO #yourTable
VALUES (46933,'12ENG','IBLevel','HL'),
(46933,'12ENG','Mark','6'),
(46933,'12ECO','IBLevel','HL'),
(46933,'12ECO','Mark','5'),
(46933,'12GEO','IBLevel','SL'),
(46933,'12GEO','Mark','6'),
(46933,'12LAN','IBLevel','HL'),
(46933,'12LAN','Mark','4'),
(46933,'12PED','IBLevel','SL'),
(46933,'12PED','Mark','5'),
(46933,'12SCI','IBLevel','SL'),
(46933,'12SCI','Mark','3'),
(67767,'12FRE','IBLevel','HL'),
(67767,'12FRE','Mark','4'),
(67767,'12MAT','IBLevel','SL'),
(67767,'12MAT','Mark','5');
Actual Query
WITH CTE_ResultValue
AS
(
SELECT StudentID,
AssessmentCode,
ResultGroup,
CAST(Result AS INT) AS ResultValue
FROM #yourTable
WHERE ISNUMERIC(Result) = 1
),
CTE_IBLevel
AS
(
SELECT StudentID,
AssessmentCode,
Result AS IBLevel
FROM #yourTable
WHERE ISNUMERIC(Result) = 0
),
CTE_Normalized
AS
(
SELECT A.StudentID,
A.AssessmentCode,
A.ResultGroup,
A.ResultValue,
B.IBLevel
FROM CTE_ResultValue AS A
INNER JOIN CTE_IBLevel AS B
ON A.StudentID = B.StudentID
AND A.AssessmentCode = B.AssessmentCode
)
SELECT [StudentID],
[HLResult] = SUM(CASE WHEN IBLevel = 'HL' THEN ResultValue END),
[SLResult] = SUM(CASE WHEN IBLevel = 'SL' THEN ResultValue END),
[TotalResult] = SUM(ResultValue),
[CountOfHL] = SUM(CASE WHEN IBLevel = 'HL' THEN 1 END),
[CountOfSL] = SUM(CASE WHEN IBLevel = 'SL' THEN 1 END)
FROM CTE_Normalized
GROUP BY StudentID
Normalization
Your table is in serious need of normalization. If you can change it, bare minimal change would look like this:
CREATE TABLE dbo.NormalizedTable
(
StudentID INT,
AssessmentCode CHAR(5),
ResultGroup VARCHAR(25),
ResultValue SMALLINT, --smallint range should be plenty. It can store values from -32,768 to 32,767
IBLevel CHAR(2)
)
INSERT INTO dbo.NormalizedTable
SELECT *
FROM CTE_Normalized
If you can't change the way your data is structured, I'd recommend creating a view from my CTE_normalized
CREATE VIEW vw_normalizedTable
AS
WITH CTE_ResultValue
AS
(
SELECT StudentID,
AssessmentCode,
ResultGroup,
CAST(Result AS INT) AS ResultValue
FROM #yourTable
WHERE ISNUMERIC(Result) = 1
),
CTE_IBLevel
AS
(
SELECT StudentID,
AssessmentCode,
Result AS IBLevel
FROM #yourTable
WHERE ISNUMERIC(Result) = 0
),
SELECT A.StudentID,
A.AssessmentCode,
A.ResultGroup,
A.ResultValue,
B.IBLevel
FROM CTE_ResultValue AS A
INNER JOIN CTE_IBLevel AS B
ON A.StudentID = B.StudentID
AND A.AssessmentCode = B.AssessmentCode
GO
Version1:
SELECT t1.StudentID,
SUM(CASE WHEN t1.Result = 'HL' THEN t2.Result ELSE 0 END) HLResult,
SUM(CASE WHEN t1.Result = 'SL' THEN t2.Result ELSE 0 END) SLResult,
SUM(CAST(t2.Result AS INT)) TotalResult,
SUM(CASE WHEN t1.Result = 'HL' THEN 1 ELSE 0 END) CountofHL,
SUM(CASE WHEN t1.Result = 'SL' THEN 1 ELSE 0 END) CountofSL,
CASE WHEN SUM(CAST(t2.Result AS INT)) > 24 THEN 'True' ELSE 'False' END MoreThan24
FROM #t t1
JOIN #t t2 ON t1.StudentID = t2.StudentID AND
t1.AssessmentCode = t2.AssessmentCode AND
t1.ResultGroup = 'IBLevel' AND
t2.ResultGroup = 'Mark'
GROUP BY t1.StudentID
Version2:
WITH cte1 AS(
SELECT *, ROW_NUMBER() OVER(PARTITION BY StudentID, AssessmentCode
ORDER BY CASE WHEN ResultGroup = 'IBLevel' THEN 1 ELSE 2 END) AS rn FROM #t),
cte2 AS(SELECT StudentID,
AssessmentCode,
SUM(CASE WHEN Result = 'HL' THEN 1 ELSE 0 END) HL,
SUM(CASE WHEN Result = 'SL' THEN 1 ELSE 0 END) SL,
MAX(CASE WHEN rn = 2 THEN Result END) R
FROM cte1
GROUP BY StudentID, AssessmentCode)
SELECT StudentID,
SUM(hl*R) HLResult,
SUM(sl*R) SLResult,
SUM((hl + sl)*r) TotalResult,
SUM(hl) CountofHL,
SUM(sl) CountofSL,
CASE WHEN SUM((hl + sl)*r) > 24 THEN 'True' ELSE 'False' END MoreThan24
FROM cte2
GROUP BY StudentID
Output:
StudentID HLResult SLResult TotalResult CountofHL CountofSL MoreThan24
46933 15 14 29 3 3 True
67767 4 5 9 1 1 False
Just replace #t with your table.

SQL select converting transaction rows to columns

I have a table that lists all transactions as follows:
ID Account Date Amount
---------------------------
1 2 02/01/2015 30
2 5 05/01/2015 25
3 2 05/01/2015 12
4 2 07/01/2015 42
5 5 10/012015 19
6 2 11/01/2015 58
7 3 15/01/2015 36
Would like to write a select statement that will list only the last 3 transactions of each account, as follows please.
Account Date1 Amount Date2 Amount Date3 Amount
---------------------------------------------------------------
2 11/01/2015 58 07/01/2015 42 05/01/2015 12
3 15/01/2015 36
5 10/01/2015 19 05/01/2015 25
Thank you for any advice
You can use the row_number() function in a derived table to partition the data by account, and give each date within the partition a number, and then do a conditional aggregation over the rows with the top 3 numbers, grouped by account:
select
account,
date1 = max(case when rn = 1 then date end),
amount = max(case when rn = 1 then amount end),
date2 = max(case when rn = 2 then date end),
amount = max(case when rn = 2 then amount end),
date3 = max(case when rn = 3 then date end),
amount = max(case when rn = 3 then amount end)
from (
select *, rn = row_number() over (partition by account order by date desc)
from your_table
) a
where rn <= 3
group by account
Sample SQL Fiddle

How to optimize for both scenarios when a join is faster than dense_rank only when the result set is small?

When unfiltered, dense_rank on FeedDeliveryNutrients.NutrientID over 150,000 rows is 3.5x faster than joining to Nutrients with row_number on Nutrients.ID and using the joined row number. When filtered to a specific flock, joining with row_number is 9x faster.
Is there any optimization technique that could get the best of both worlds in a single query?
Fastest when unfiltered (150,000 rows returned):
select
FeedDeliveries.FlockID,
FeedDeliveryID,
DeliveryLb,
Bin,
DeliveryDate,
FormulaID,
FeedEnergy,
Nutrient1, Nutrient2, Nutrient3, Nutrient4, Nutrient5, Nutrient6, Nutrient7, Nutrient8, Nutrient9, Nutrient10, Nutrient11, Nutrient12, Nutrient13, Nutrient14, Nutrient15
from (
select
FeedDeliveryID,
sum(case when dense_rank = 1 then Amount end) as Nutrient1,
sum(case when dense_rank = 2 then Amount end) as Nutrient2,
sum(case when dense_rank = 3 then Amount end) as Nutrient3,
sum(case when dense_rank = 4 then Amount end) as Nutrient4,
sum(case when dense_rank = 5 then Amount end) as Nutrient5,
sum(case when dense_rank = 6 then Amount end) as Nutrient6,
sum(case when dense_rank = 7 then Amount end) as Nutrient7,
sum(case when dense_rank = 8 then Amount end) as Nutrient8,
sum(case when dense_rank = 9 then Amount end) as Nutrient9,
sum(case when dense_rank = 10 then Amount end) as Nutrient10,
sum(case when dense_rank = 11 then Amount end) as Nutrient11,
sum(case when dense_rank = 12 then Amount end) as Nutrient12,
sum(case when dense_rank = 13 then Amount end) as Nutrient13,
sum(case when dense_rank = 14 then Amount end) as Nutrient14,
sum(case when dense_rank = 15 then Amount end) as Nutrient15
from (select *, dense_rank() over (partition by FeedDeliveryID order by NutrientID) as dense_rank from dbo.FeedDeliveryNutrients) n
group by FeedDeliveryID
) pvt
join dbo.FeedDeliveries on FeedDeliveries.ID = FeedDeliveryID
Fastest when filtered by dbo.FeedDeliveries.FlockID (~100 rows returned):
select
FeedDeliveries.FlockID,
FeedDeliveryID,
DeliveryLb,
Bin,
DeliveryDate,
FormulaID,
FeedEnergy,
Nutrient1, Nutrient2, Nutrient3, Nutrient4, Nutrient5, Nutrient6, Nutrient7, Nutrient8, Nutrient9, Nutrient10, Nutrient11, Nutrient12, Nutrient13, Nutrient14, Nutrient15
from (
select
FeedDeliveryID,
sum(case when n.row_number = 1 then Amount end) as Nutrient1,
sum(case when n.row_number = 2 then Amount end) as Nutrient2,
sum(case when n.row_number = 3 then Amount end) as Nutrient3,
sum(case when n.row_number = 4 then Amount end) as Nutrient4,
sum(case when n.row_number = 5 then Amount end) as Nutrient5,
sum(case when n.row_number = 6 then Amount end) as Nutrient6,
sum(case when n.row_number = 7 then Amount end) as Nutrient7,
sum(case when n.row_number = 8 then Amount end) as Nutrient8,
sum(case when n.row_number = 9 then Amount end) as Nutrient9,
sum(case when n.row_number = 10 then Amount end) as Nutrient10,
sum(case when n.row_number = 11 then Amount end) as Nutrient11,
sum(case when n.row_number = 12 then Amount end) as Nutrient12,
sum(case when n.row_number = 13 then Amount end) as Nutrient13,
sum(case when n.row_number = 14 then Amount end) as Nutrient14,
sum(case when n.row_number = 15 then Amount end) as Nutrient15
from dbo.FeedDeliveryNutrients
join (select *, row_number() over (order by ID) as row_number from dbo.Nutrients) n on n.ID = NutrientID
group by FeedDeliveryID
) pvt
join dbo.FeedDeliveries on FeedDeliveries.ID = FeedDeliveryID
You already got the answer. Optimize for the most critical scenario.
From start I can use optimize for the 150.000 rows scenario but you must sniff a bit on your actual production server. If that 150.000 only happens a few times /week and the 100 rows ill hit your server many times/minute that is the more critical for you.

SQL Query to calculate remaining running balances based on a given conditions

I have a stock transaction table like this:
StockID Item TransDate TranType BatchNo Qty Price
10001 ABC 01-Apr-2012 IN 71001000 200 750.0
10002 ABC 02-Apr-2012 OUT 100
10003 ABC 03-Apr-2012 IN 71001001 50 700.0
10004 ABC 04-Apr-2012 IN 71001002 75 800.0
10005 ABC 10-Apr-2012 OUT 125
10006 XYZ 05-Apr-2012 IN 71001003 150 350.0
10007 XYZ 05-Apr-2012 OUT 120
10008 XYZ 15-Apr-2012 OUT 10
10009 XYZ 20-Apr-2012 IN 71001004 90 340.0
10010 PQR 06-Apr-2012 IN 71001005 50 510.0
10011 PQR 15-Apr-2012 IN 71001006 60 505.0
10012 MNO 01-Apr-2012 IN 71001007 76 410.0
10013 MNO 11-Apr-2012 OUT 76
Each of my IN transactions has price associated to it and a batch number (lot number). Now I would like to calculate the remaining quantity by First In First Out (FIFO) rule, meaning the first in should be adjusted with first out. After adjusting the quantities the remaining balances are to be calculated against each IN transaction for the same item as shown below:
StockID Item TransDate TranType BatchNo Qty Price RemainingQty
10001 ABC 01-Apr-2012 IN 71001000 200 750.0 0
10002 ABC 02-Apr-2012 OUT 100
10003 ABC 03-Apr-2012 IN 71001001 50 700.0 25
10004 ABC 04-Apr-2012 IN 71001002 75 800.0 75
10005 ABC 10-Apr-2012 OUT 125
10006 XYZ 05-Apr-2012 IN 71001003 150 350.0 20
10007 XYZ 05-Apr-2012 OUT 120
10008 XYZ 15-Apr-2012 OUT 10
10009 XYZ 20-Apr-2012 IN 71001004 90 340.0 90
10010 PQR 06-Apr-2012 IN 71001005 50 510.0 50
10011 PQR 15-Apr-2012 IN 71001006 60 505.0 60
10012 MNO 01-Apr-2012 IN 71001007 76 410.0 0
10013 MNO 11-Apr-2012 OUT 76
As we can see from the above table for item ABC, after adjusting (125 + 100) OUT qty against the IN qty (100 + 50 + 75) using FIFO the quantity remaining for the batch 71001000 is 0, 71001001 is 25 and for batch 71001002 is 75. From the remaining quantity the value can be derived.
Please help me to achieve this using any of the methods (either cursor based or CTE or JOINS, etc)
Thanks in advance for the help.
One of the users of StockOverflow suggested this answer:
SELECT 10001 as stockid,'ABC' as item,'01-Apr-2012' as transdate,'IN' as trantype, 71001000 as batchno, 200 as qty, 750.0 as price INTO #sample
UNION ALL SELECT 10002 ,'ABC','02-Apr-2012','OUT', NULL ,100,NULL
UNION ALL SELECT 10003 ,'ABC','03-Apr-2012','IN', 71001001, 50 , 700.0
UNION ALL SELECT 10004 ,'ABC','04-Apr-2012','IN', 71001002, 75 , 800.0
UNION ALL SELECT 10005 ,'ABC','10-Apr-2012','OUT', NULL ,125,NULL
UNION ALL SELECT 10006 ,'XYZ','05-Apr-2012','IN', 71001003, 150 , 350.0
UNION ALL SELECT 10007 ,'XYZ','05-Apr-2012','OUT', NULL , 120 ,NULL
UNION ALL SELECT 10008 ,'XYZ','15-Apr-2012','OUT', NULL , 10 ,NULL
UNION ALL SELECT 10009 ,'XYZ','20-Apr-2012','IN', 71001004, 90 , 340.0
UNION ALL SELECT 10010 ,'PQR','06-Apr-2012','IN', 71001005, 50 , 510.0
UNION ALL SELECT 10011 ,'PQR','15-Apr-2012','IN', 71001006, 60 , 505.0
UNION ALL SELECT 10012 ,'MNO','01-Apr-2012','IN', 71001007, 76 , 410.0
UNION ALL SELECT 10013 ,'MNO','11-Apr-2012','OUT', NULL ,76 ,NULL
;WITH remaining AS
(
SELECT *,
CASE
WHEN trantype = 'IN' THEN 1
ELSE -1
END * qty AS stock_shift,
ROW_NUMBER() OVER(PARTITION BY item ORDER BY transdate) AS row,
CASE
WHEN trantype = 'OUT' THEN NULL
ELSE ROW_NUMBER()OVER(PARTITION BY item, CASE WHEN trantype = 'IN' THEN 0 ELSE 1 END ORDER BY transdate)
END AS in_row,
SUM(CASE WHEN trantype = 'OUT' THEN qty END) OVER(PARTITION BY item) AS total_out
FROM #sample
)
,remaining2 AS
(
SELECT r1.item,
r1.stockid,
MAX(r1.transdate) AS transdate,
MAX(r1.trantype) AS trantype,
MAX(r1.batchno) AS batchno,
MAX(r1.qty) AS qty,
MAX(r1.price) AS price,
MAX(r1.total_out) AS total_out,
MAX(r1.in_row) AS in_row,
CASE
WHEN MAX(r1.trantype) = 'OUT' THEN NULL
WHEN SUM(CASE WHEN r1.trantype = 'IN' THEN r2.qty ELSE 0 END) - MAX(r1.total_out) < 0 THEN SUM(CASE WHEN r1.trantype = 'IN' THEN r2.qty ELSE 0 END)
- MAX(r1.total_out)
ELSE 0
END AS running_in
FROM remaining r1
LEFT OUTER JOIN remaining r2
ON r2.row <= r1.row
AND r2.item = r1.item
GROUP BY
r1.item,
r1.stockid
)
SELECT r2.item,
r2.stockid,
MAX(r2.transdate) AS transdate,
MAX(r2.trantype) AS trantype,
MAX(r2.batchno) AS batchno,
MAX(r2.qty) AS qty,
MAX(r2.price) AS price,
MAX(CASE WHEN r2.trantype = 'OUT' THEN NULL ELSE ISNULL(r2.qty + r3.running_in, 0) END) AS remaining_stock
FROM remaining2 r2
LEFT OUTER JOIN remaining2 r3
ON r2.in_row - 1 = r3.in_row
AND r2.item = r3.item
GROUP BY
r2.item,
r2.stockid
This sql is having a problem and the result is attached here The records for which the value are not matching are indicated in yellow color. Kindly help to solve the problem.
I think this should do the trick?
SELECT 10001 as stockid,'ABC' as item,'01-Apr-2012' as transdate,'IN' as trantype, 71001000 as batchno, 200 as qty, 750.0 as price INTO #sample
UNION ALL SELECT 10002 ,'ABC','02-Apr-2012','OUT', NULL ,100,NULL
UNION ALL SELECT 10003 ,'ABC','03-Apr-2012','IN', 71001001, 50 , 700.0
UNION ALL SELECT 10004 ,'ABC','04-Apr-2012','IN', 71001002, 75 , 800.0
UNION ALL SELECT 10005 ,'ABC','10-Apr-2012','OUT', NULL ,125,NULL
UNION ALL SELECT 10006 ,'XYZ','05-Apr-2012','IN', 71001003, 150 , 350.0
UNION ALL SELECT 10007 ,'XYZ','05-Apr-2012','OUT', NULL , 120 ,NULL
UNION ALL SELECT 10008 ,'XYZ','15-Apr-2012','OUT', NULL , 10 ,NULL
UNION ALL SELECT 10009 ,'XYZ','20-Apr-2012','IN', 71001004, 90 , 340.0
UNION ALL SELECT 10010 ,'PQR','06-Apr-2012','IN', 71001005, 50 , 510.0
UNION ALL SELECT 10011 ,'PQR','15-Apr-2012','IN', 71001006, 60 , 505.0
UNION ALL SELECT 10012 ,'MNO','01-Apr-2012','IN', 71001007, 76 , 410.0
UNION ALL SELECT 10013,'MNO','11-Apr-2012','OUT', NULL ,76 ,NULL
;with remaining_stock as
(
SELECT *
,CASE WHEN trantype = 'IN' THEN 1 ELSE -1 END * qty AS stock_shift
,row_number() OVER (PARTITION BY item ORDER BY transdate) as row
,CASE WHEN trantype = 'OUT' THEN NULL ELSE
row_number()OVER (PARTITION BY item,CASE WHEN trantype = 'IN' THEN 0 ELSE 1 END ORDER BY transdate) END as in_row
,CASE WHEN trantype = 'IN' THEN NULL ELSE
row_number()OVER (PARTITION BY item,CASE WHEN trantype = 'OUT' THEN 0 ELSE 1 END ORDER BY transdate) END as out_row
,ISNULL(SUM(CASE WHEN trantype = 'OUT' THEN qty END) OVER (PARTITION BY item),0) AS total_out
,ISNULL(SUM(CASE WHEN trantype = 'IN' THEN qty END) OVER (PARTITION BY item),0) AS total_in
FROM #sample
)
,remaining_stock2 AS
(
SELECT
r1.item
,r1.stockid
,MAX(r1.transdate) as transdate
,MAX(r1.trantype) as trantype
,MAX(r1.batchno) as batchno
,MAX(r1.qty) as qty
,MAX(r1.price) as price
,MAX(r1.total_in) as total_in
,MAX(r1.total_out) as total_out
,SUM(r2.qty) as running_in
FROM remaining_stock r1
LEFT OUTER JOIN remaining_stock r2 on r2.in_row <= r1.in_row
AND r2.item = r1.item
GROUP BY
r1.item
,r1.stockid
)
SELECT
item
,stockid
,transdate
,trantype
,batchno
,qty
,price
,CASE WHEN trantype = 'OUT' THEN NULL
WHEN total_out >= running_in THEN 0
WHEN (running_in - total_out) < qty THEN (running_in - total_out)
WHEN (running_in - total_out) >= qty THEN qty
END as remaining_stocks
FROM remaining_stock2
Your question isn't very clear to me on how the FIFO logic is to be applied. I'm going to assume that you want to associate each IN record against the next OUT record if one exists. To achieve this you need to join the table on itself like the following
select
t1.BatchNo,
isnull(t1.Qty,0) as 'IN Qty',
isnull(t2.Qty,0) as 'OUT Qty',
isnull(t1.Qty,0) - isnull(t2.Qty,0) as 'Remaining Qty'
from
tbl_test t1
left join tbl_test t2
on t2.StockID = (t1.StockID + 1)
and t2.TranType = 'OUT'
where
t1.TranType = 'IN'
The results will show you the following for the first 5 records for ABC from your question.
BatchNo | IN Qty | OUT Qty | Remaining Qty
71001000 | 200 | 100 | 100
71001001 | 50 | 0 | 50
71001002 | 75 | 125 | -50
The left join works on the assumption that the StockID for each IN record is always one less number than the associated OUT record. I personally think your data model needs improving.
OUT records should have a BatchNo assigned or a reference to the
StockID of the associated IN record
add a timestamp field for sequential ordering
add a DateTime field for handling IN/OUT occuring on same day