Calculating Inventory running quantity/value & moving average unit cost - sql-server

Any help to the following problem I have will be appreciated, thank you.
I am trying to perform some retrospective costings on stock inventory and failing to realise my results… I have tried various window functions (lead/Lag/row_number) etc along with multiple ctes and also First_Value….but have failed miserably….so hopefully your thoughts may get me where I want to be.
MS SQL SERVER 2019
This is what I have:
tId
tType
tcode
tDate
tQty
tValue
1
PO_IN
456
20210901
200
3654.00
2
SO_OUT
456
20210903
-155
3
SO_OUT
456
20210904
-15
4
PO_IN
456
20210905
150
3257.00
5
SO_OUT
456
20210906
-120
6
SO_OUT
456
20210907
-10
7
FIN_ADJ
456
20210908
0
-75.00
8
SO_OUT
456
20210909
-20
9
PO_IN
456
20210902
5
0.00
10
SO_OUT
456
20210910
-35
This is what I wish to achieve:
tId
tType
tcode
tDate
tQty
tValue
Run_Qty
Run_Value
avg_cost
1
PO_IN
456
20210901
200
3654.00
200
3654.00
18.27
2
SO_OUT
456
20210903
-155
45
822.15
18.27
3
SO_OUT
456
20210904
-15
30
548.10
18.27
4
PO_IN
456
20210905
150
3257.00
180
3805.10
21.14
5
SO_OUT
456
20210906
-120
60
1268.37
21.14
6
SO_OUT
456
20210907
-10
50
1056.97
21.14
7
FIN_ADJ
456
20210908
0
-75.00
50
981.97
19.64
8
SO_OUT
456
20210909
-20
30
589.18
19.64
9
PO_IN
456
20210902
5
0.00
35
589.18
16.83
10
SO_OUT
456
20210910
-35
0
0.00
PO_IN have a positive tqty and a positive or zero tvalue. This can be used to create/ adjust the moving avg cost per unit.
SO-OUT are negative tqty and have NULL tValue and decrement the running qty column and decrement the running value column by the tqty * the previous row avg cost
FIN_ADJ are tValue only and can be positive/negative.
the order of processing required is by tid and not tDate.
If I can get this to output the correct closing qty/value and the correct avg cost per row, I can then move onto the next step of analysis.
Thanks
EDIT....real world tables will have 100K+ rows
Some initial setup code
CREATE TABLE tst_Inv(
tId INT NOT NULL
,tType VARCHAR(7) NOT NULL
,tcode INT NOT NULL
,tDate DATE NOT NULL
,tQty INTEGER NOT NULL
,tValue NUMERIC(7,2)
);
INSERT INTO tst_Inv(tId,tType,tcode,tDate,tQty,tValue) VALUES
(1,'PO_IN',456,'20210901',200,3654.00),(2,'SO_OUT',456,'20210903',-155,NULL),(3,'SO_OUT',456,'20210904',-15,NULL)
,(4,'PO_IN',456,'20210905',150,3257.00),(5,'SO_OUT',456,'20210906',-120,NULL),(6,'SO_OUT',456,'20210907',-10,NULL)
,(7,'FIN_ADJ',456,'20210908',0,-75.00),(8,'SO_OUT',456,'20210909',-20,NULL),(9,'PO_IN',456,'20210902',5,0.00)
,(10,'SO_OUT',456,'20210910',-35,NULL);
SELECT * FROM tst_Inv

I'm using CTE
;WITH xQ
AS
(
SELECT tId,tType, tcode, tDate, tQty, tValue ,
tQty AS Run_Qty,
CAST (tValue AS NUMERIC(7,2)) AS Run_Value ,
tValue / tQty AS Avg_Cost
FROM tst_Inv
WHERE tId = 1
UNION ALL
SELECT tId,tType, tcode, tDate, tQty, tValue ,
xB.Run_Qty,
xB.Run_Value,
CASE WHEN xB.Run_Qty = 0 THEN NULL ELSE xB.Run_Value / xB.Run_Qty END AS Avg_Cost
FROM
(
SELECT tId,tType, tcode, tDate, tQty, tValue,
Run_Qty,
CAST(
CASE xA.tType
WHEN 'PO_IN' THEN xA.tValue + xA.Run_Value
WHEN 'FIN_ADJ' THEN xA.tValue + xA.Run_Value
ELSE xA.Run_Qty * xA.Avg_Cost
END
AS NUMERIC(7,2)) AS Run_Value,
xA.Avg_Cost
FROM
(
SELECT tst_Inv.* ,
tst_Inv.tQty + xQ.Run_Qty AS Run_Qty,
xQ.Run_Value,
xQ.Avg_Cost
FROM tst_Inv
INNER JOIN xQ ON (tst_Inv.tId -1) = xQ.tId
) AS xA
) AS xB
)
SELECT * FROM xQ

Related

Not in Condition in the SQL

Productid
Dept cd
123
440
123
422
123
248
1234
422
1234
440
1234
196
12
440
12
422
12
19
12
196
12345
196
12345
180
12345
422
** I should get the Product ID who has 422 and 440 but it should not have 248.
Answer should be 1234 and 12
There are a number of approaches to solve this one.
Here's one option that I think is simplest to understand (i.e. might not be the most efficient!):
SELECT product_id
, Sum(CASE WHEN dept_cd = 422 THEN 1 ELSE 0 END) AS has_422
, Sum(CASE WHEN dept_cd = 440 THEN 1 ELSE 0 END) AS has_440
, Sum(CASE WHEN dept_cd = 248 THEN 1 ELSE 0 END) AS has_248
FROM your_table
WHERE dept_cd IN (442, 440, 248)
GROUP
BY product_id
;
Filtering the results of that should then be trivial (make it a CTE or a subquery then add another WHERE clause, or even just use a HAVING clause).
gvee's answer using aggregation is probably the most efficient one. Simplified version (which can be easily extended):
SELECT product_id
FROM your_table
WHERE dept_cd IN (442, 440, 248)
GROUP BY product_id
HAVING -- assuming (product_id,dept_id) is unique
Sum(CASE WHEN dept_cd IN (422, 440) THEN 1 -- both exist -> result = 2
WHEN dep_cd IN (248) THEN -1 -- exists -> result < 2
END) = 2

Calculating the running total

I want to calculate the running total using a Stored Procedure. The base Table is ~10.000 rows and is as follows:
nWordNr nBitNr tmTotals
------------------------
5 14 86404
5 14 146
2 3 438
10 2 3319
5 12 225
2 3 58
.... .... .....
.... .... .....
I want this to be GROUPED BY nWordNr, NBitNr and have the total tmTotals. To do this is started of with the following:
SELECT TOP 10
[nWordNr] as W,
[nBitNr] as B,
SUM([tmTotals]) as total,
COUNT(*) as Amount
FROM Messages_History
GROUP BY nWordNr, nBitNr
ORDER BY total desc
This results in:
W B total Amount
-----------------------
2 3 3578775 745
3 3 3557975 395
5 4 2305229 72
5 3 2183050 33
5 12 2022401 825
5 14 1673295 652
48 12 1658862 302
4 3 1606454 215
48 13 1541729 192
5 9 1463256 761
Now I want to calculate the running total on the column total like this:
W B total Amount running
-------------------------------
2 3 3578775 745 3578775
3 3 3557975 395 7136750
5 4 2305229 72 9441979
5 3 2183050 33 11625029
5 12 2022401 825 etc.
5 14 1673295 652 etc.
48 12 1658862 302 etc.
4 3 1606454 215 etc.
48 13 1541729 192 etc.
5 9 1463256 761 etc.
so what I found was:
COUNT([tmTotals]) over (ORDER BY [nWordNr], [nBitNr]) as Running
But here I get the error that is discussed in this question: Column invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause and I just can't figure out how to solve it in this case
it should be SUM ( SUM ( tmTotals) ) OVER ( ... )
SELECT TOP 10
[nWordNr] as W,
[nBitNr] as B,
SUM([tmTotals]) as total,
COUNT(*) as Amount,
SUM(SUM([tmTotals])) OVER (ORDER BY [nWordNr], [nBitNr]) as Running
FROM Messages_History
GROUP BY nWordNr, nBitNr
ORDER BY total desc
EDIT :
Looking at your expected result, the Running should be
SUM(SUM([tmTotals])) OVER (ORDER BY SUM([tmTotals]) DESC) as Running
if the above is a bit difficult to grasp, then you can use a CTE or derived table and perform the running total on the outer query
; with CTE as
(
SELECT
[nWordNr] as W,
[nBitNr] as B,
SUM([tmTotals]) as total,
COUNT(*) as Amount
FROM Messages_History
GROUP BY nWordNr, nBitNr
)
SELECT TOP 10 *,
SUM(total) OVER (ORDER BY total desc) as Running
FROM CTE
ORDER BY total desc

Group by and sum based on column values without sum() over()?

We have a table [Kpis] that looks like the following:
RawId EmpId Date Hour Min KpiValue KpiName
106 ABC123 20160310 8 0 3 Kpi1
124 ABC123 20160310 8 0 65 Kpi1
121 ABC123 20160310 8 15 12 Kpi2
109 ABC109 20160310 8 0 34 Kpi2
112 ABC908 20160310 9 5 3 Kpi1
118 ABC907 20160310 8 30 24 Kpi1
115 ABC123 20160310 8 15 54 Kpi1
I would like to group by EmpId, KpiName, Date, Hour. So, for example, with this data, Kpi1 for EmpId ABC123 at Hour 8 would be 122.
So I tried using the CASE statement, but the result is incorrect. I haven't checked the actual totals in the result, but the sums should be correct. It's the format of the result that's incorrect; every empid has two rows: one for Kpi1 and one for Kpi2.
select empid,
case kpiname when 'Kpi1' then sum(kpivalue) end as 'Kpi1',
case kpiname when 'Kpi2' then sum(kpivalue) end as 'Kpi2'
from
[Kpis]
where kpiname in ('Kpi1', 'Kpi2')
and date = 20160310 and hour = 8
group by empid, kpiname, hour
How can I use the Case statement to fix the results?
Thanks.
Put the case inside your sum, such that you for each KpiName only sums the relevant values.
SELECT
EmpId,
[Hour],
SUM(
CASE
WHEN KpiName = 'Kpi1' THEN KpiValue
ELSE 0
END
) Kpi1,
SUM(
CASE
WHEN KpiName = 'Kpi2' THEN KpiValue
ELSE 0
END
) Kpi2
FROM
Kpis
GROUP BY
EmpId,
[Hour]
This produces this output
EmpId Hour Kpi1 Kpi2
ABC109 8 0 34
ABC123 8 122 12
ABC907 8 24 0
ABC908 9 3 0
SUM fucntion have to be outside of CASE:
select empid,
sum(case kpiname when 'Kpi1' then kpivalue end) as 'Kpi1',
sum(case kpiname when 'Kpi2' then kpivalue end) as 'Kpi2'
from
[Kpis]
where kpiname in ('Kpi1', 'Kpi2')
and date = 20160310 and hour = 8
group by empid, kpiname, hour
You can also do this with the PIVOT functionality, which I believe is what you're actually trying to accomplish.
SELECT
*
FROM (
SELECT
EmpId,
KpiName,
[Hour],
KpiValue
FROM
Kpis
) SourceTable
PIVOT (
SUM(KpiValue)
FOR KpiName
IN ([Kpi1],[Kpi2])
) PivotTable
Which gives this output. Note the NULLs as opposed to the zeros, correctly showing the lack of data.
EmpId Hour Kpi1 Kpi2
ABC109 8 NULL 34
ABC123 8 122 12
ABC907 8 24 NULL
ABC908 9 3 NULL

Getting records from a table which have non-null values for a particular column in all months

I have a table:
Project_Id Period Value
123 Jan-15 0
123 Feb-15 34
123 Mar-15 78
123 Apr-15 56
456 Jan-15 0
456 Feb-15 0
456 Mar-15 0
456 Apr-15 0
789 Jan-15 45
789 Feb-15 4
789 Mar-15 18
789 Apr-15 26
I need to retrieve Project data only when i do not have 0 for Value field in all the months like:
Project_Id Period Value
123 Jan-15 0
123 Feb-15 34
123 Mar-15 78
123 Apr-15 56
789 Jan-15 45
789 Feb-15 4
789 Mar-15 18
789 Apr-15 26
Project no 456 should not come in my result because for all the months the value is 0 for that particular project.
Can someone help me with the query?
Use SUM and COUNT to determine the number of 0 Values:
SELECT *
FROM tbl
WHERE project_id IN(
SELECT project_id
FROM tbl
GROUP BY project_id
HAVING SUM(CASE WHEN Value = 0 THEN 1 ELSE 0 END) <> COUNT(*)
)
SQL Fiddle
Another solution is to use EXISTS:
SELECT *
FROM tbl t1
WHERE EXISTS(
SELECT 1 FROM tbl t2 WHERE t2.project_id = t1.project_id AND t2.Value > 0
)
SQL Fiddle
The inner select gets all project_ids that have a least one value that is not 0.
select * from your_table
where project_id in
(
select project_id
from your_table
group by project_id
having sum(case when value <> 0 then 1 else 0 end) > 0
)
Some test data but idea remains the same
create table #test123
(
pid int,
value int
)
insert into #test123
select 1,0
union all
select 1,1
union all
select 2,0
union all
select 2,0
union all
select 3,2
select * from #test123 t2 where exists (select 1 from #test123 t1
where t1.pid=t2.pid
group by pid
having sum(value)>0
)
For performance, I prefer not making a join to check for repeating values:
;WITH CTE as
(
SELECT
Project_Id,
Period,
Value,
max(abs(value)) over (Partition by Period) value
FROM YourTable
)
SELECT
Project_Id,
Period,
Value
FROM CTE
WHERE value > 0
*using abs to check for negative values. If all values are positive, the abs can be omitted.

How to split a numeric field into smaller segments in Sql Server

I have a table in SQL Server with two fields.
Total Group
35645 24
12400 55
30000 41
I want to split each group into smaller segments of fixed size 7000, with the remainder of each group into the last segment. So, the output should look like below.
Segment Total Group
1 7000 24
2 7000 24
3 7000 24
4 7000 24
5 7000 24
6 645 24
1 7000 55
2 5400 55
1 7000 41
2 7000 41
3 7000 41
4 7000 41
5 2000 41
This should do it:
declare #t table (Total int,[Group] int)
insert into #t(Total,[Group]) values
(35645,24 ),
(12400,55 ),
(30000,41 )
;With Numbers as (
select ROW_NUMBER() OVER (ORDER BY number)-1 n
from master..spt_values
)
select
n.n+1 as Segment,
CASE WHEN (n.n+1)*7000 < t.Total THEN 7000
ELSE t.Total - (n.n*7000) END as Total,
t.[Group]
from
#t t inner join
Numbers n on n.n*7000 < t.Total
(If you already have a Numbers table you can eliminate that part. I'm using spt_values just as a table that I know has plenty of rows in it, so that the ROW_NUMBER() expression should generate all of the necessary numbers)
Results:
Segment Total Group
-------------------- -------------------- -----------
1 7000 24
2 7000 24
3 7000 24
4 7000 24
5 7000 24
6 645 24
1 7000 55
2 5400 55
1 7000 41
2 7000 41
3 7000 41
4 7000 41
5 2000 41
I prepared following SELECT statement using SQL CTE expression and SQL numbers table function
declare #divisor int = 7000
;with CTE as (
select
Total,
[Group],
#divisor divisor,
(Total / #divisor) quotient,
(Total % #divisor) reminder
from t
), NT as (
SELECT i FROM dbo.NumbersTable(1, (select max(quotient) from CTE) ,1)
)
select
case when i = 0 then reminder else divisor end as Total,
[Group]
from (
select *
from CTE, NT
where quotient >= i
union all
select *, 0 as i
from CTE
where reminder >= 0
) t
order by [Group], i desc

Resources