FIFO Implementation in Inventory using SQL - sql

This is basically an inventory project which tracks the "Stock In" and "Stock Out" of items through Purchase and sales respectively.
The inventory system follows FIFO Method (the items which are first purchased are always sold first). For example:
If we purchased Item A in months January, February and March
When a customer comes we give away items purchased during January
only when the January items are over we starts giving away February items and so on
So I have to show here the total stock in my hand and the split up so that I can see the total cost incurred.
Actual table data:
The result set I need to obtain:
My client insists that I should not use Cursor, so is there any other way of doing so?

As some comment already said a CTE can solve this
with cte as (
select item, wh, stock_in, stock_out, price, value
, row_number() over (partition by item, wh order by item, wh) as rank
from myTable)
select a.item, a.wh
, a.stock_in - coalesce(b.stock_out, 0) stock
, a.price
, a.value - coalesce(b.value, 0) value
from cte a
left join cte b on a.item = b.item and a.wh = b.wh and a.rank = b.rank - 1
where a.stock_in - coalesce(b.stock_out, 0) > 0
If the second "Item B" has the wrong price (the IN price is 25, the OUT is 35).
SQL 2008 fiddle
Just for fun, with sql server 2012 and the introduction of the LEAD and LAG function the same thing is possible in a somewhat easier way
with cte as (
select item, wh, stock_in
, coalesce(LEAD(stock_out)
OVER (partition by item, wh order by item, wh), 0) stock_out
, price, value
, coalesce(LEAD(value)
OVER (partition by item, wh order by item, wh), 0) value_out
from myTable)
select item
, wh
, (stock_in - stock_out) stock
, price
, (value - value_out) value
from cte
where (stock_in - stock_out) > 0
SQL2012 fiddle
Update
ATTENTION -> To use the two query before this point the data need to be in the correct order.
To have the details with more then one row per day you need something reliable to order the row with the same date, like a date column with time, an autoincremental ID or something down the same line, and it's not possible to use the query already written because they are based on the position of the data.
A better idea is to split the data in IN and OUT, order it by item, wh and data, and apply a rank on both data, like this:
SELECT d_in.item
, d_in.wh
, d_in.stock_in - coalesce(d_out.stock_out, 0) stock
, d_in.price
, d_in.value - coalesce(d_out.value, 0) value
FROM (SELECT item, wh, stock_in, price, value
, rank = row_number() OVER
(PARTITION BY item, wh ORDER BY item, wh, date)
FROM myTable
WHERE stock_out = 0) d_in
LEFT JOIN
(SELECT item, wh, stock_out, price, value
, rank = row_number() OVER
(PARTITION BY item, wh ORDER BY item, wh, date)
FROM myTable
WHERE stock_in = 0) d_out
ON d_in.item = d_out.item AND d_in.wh = d_out.wh
AND d_in.rank = d_out.rank
WHERE d_in.stock_in - coalesce(d_out.stock_out, 0) > 0
SQLFiddle
But this query is NOT completely reliable, the order of data in the same order group is not stable.
I haven't change the query to recalculate the price if the IN.price is different from the OUT.price

If cursors aren't an option, a SQLCLR stored procedure might be. This way you could obtain the raw data into .net objects, manipulate / sort it using c# or vb.net and set the resulting data as the procedure's output. Not only this will give you what you want, it may even turn up being much easier than trying to do the same in pure T-SQL, depending on your programming background.

Related

Rolling sum on sequentially linked data

I am working on large a legacy dataset with sequentially related data of which I lack the words to explain so I made a beautiful paint image. This isn't of course of the real dataset but it is close. In the example there are three sequences.
Each record has an ID and a value. It also has a pointer to the next related ID. The sequence length is random and stops when the next related ID hits a 0 value. All records are only used once in one sequence, meaning they cant merge or split. A sequence can consist of only one record.
What I need to accomplish is to get the rolling sum on each record of a sequence using a SQL query(SQL server 2014). I know how to do this if there is a common identifier in the sequence, but in this case there is not.
I have been able to accomplish it in Excel (for what it's worth) by finding the previous sum (if it exists) and adding the current value. But I'm unable to translate it to SQL. Does anyone know where to start to get to the end goal of the 'rolling sum result' column in SQL?
[previous sum] formule: =IFNA(INDEX([rolling sum formula],MATCH([#id],[next_pointer],0),0),0)
[rolling sum result] formula: =[#[previous sum]]+[#value]
*The data sequences aren't sorted like in the Excel example. This just makes it easier to read in the example.
You need something like a RECURSIVE query.
You can do this with CTE. This is a test with your data (the column you seek is "cumul", the others are there to help understand what's going on):
WITH sequenza AS (
SELECT
id,
value,
nextid,
id AS lastid,
value as cumul
FROM
items
WHERE nextid = 0
UNION ALL
SELECT
curr.id,
curr.value,
curr.nextid,
prev.lastid,
prev.cumul + curr.value AS cumul
FROM
items AS curr
INNER JOIN sequenza AS prev
ON prev.id = curr.nextid
)
SELECT * FROM sequenza
WHERE id = 31;
To do this in reverse order... there is probably more than one way. Off the top of my head I'd get, for each chain (identified by its lastid), the minimum and maximum cumul value, then I'd apply the ladder algorithm - in this case the descending rolling sum is VALMIN+VALMAX-ROLLING.
So, something like
WITH sequenza AS (
SELECT
id,
value,
nextid,
id AS lastid,
value as cumul
FROM
items
WHERE nextid = 0
UNION ALL
SELECT
curr.id,
curr.value,
curr.nextid,
prev.lastid,
prev.cumul + curr.value AS cumul
FROM
items AS curr
INNER JOIN sequenza AS prev
ON prev.id = curr.nextid
),
sequenza2 AS (
SELECT
id,
value,
nextid,
id AS lastid,
value as cumul
FROM
items
WHERE nextid = 0
UNION ALL
SELECT
curr.id,
curr.value,
curr.nextid,
prev.lastid,
prev.cumul + curr.value AS cumul
FROM
items AS curr
INNER JOIN sequenza2 AS prev
ON prev.id = curr.nextid
)
SELECT sequenza.*, m1+m2-cumul AS cumulasc FROM sequenza
JOIN (
SELECT lastid, MIN(cumul) AS m1, MAX(cumul) AS m2
FROM sequenza2
GROUP BY lastid
) AS cirpo ON (sequenza.lastid = cirpo.lastid)
ORDER BY sequenza.lastid, cumul DESC

Different results in SQL based on what columns I display

I am trying to run a query to gather the total items on hand in our database. However it seems i'm getting incorrect data. I am selecting selecting just the amount field and summing it using joins from separate tables based on certain parameters, however if I display additional fields such as order number, and date all of a sudden im getting different data, even though those fields are being used as filters in the query. Is it because its not in the select statement? If it needs to be in the select statement is it possible to not display them?
Here are the two queries.
-- Items On Hand
select CONVERT(decimal(25, 2), SUM(tw.amount)) as 'Amt'
from [Sales Header] sh
join
(
select *
from TWAllOrders
where [Status] like 'Released'
) tw
on tw.[Order Nb] = sh.No_
join
(
select *
from OnHand
) oh
on tw.No_ = oh.[Item No_]
where sh.[Requested Delivery Date] < getdate()
HAVING SUM(tw.Quantity) <= SUM(oh.Qty)
providing a sum of 21667457.20
and with the added columns
-- Items On Hand
select CONVERT(decimal(25, 2), SUM(tw.amount)) as 'Amt', [Requested Delivery Date], sh.No_, tw.[Status]
from [Sales Header] sh
join
(
select *
from TWAllOrders
where [Status] like 'Released'
) tw
on tw.[Order Nb] = sh.No_
join
(
select *
from OnHand
) oh
on tw.No_ = oh.[Item No_]
where sh.[Requested Delivery Date] < getdate()
group by sh.[Requested Delivery Date], sh.No_, tw.[Status]
HAVING SUM(tw.Quantity) <= SUM(oh.Qty)
order by sh.[Requested Delivery Date] ASC
Providing a sum of 12319998
I'm self taught in SQL so I may be misunderstanding something obvious, thanks for the help.
With no sample data, I am going to have to demonstrate this in principle. In the latter query you have a GROUP BY meaning the scope of the values in the HAVING will differ, and thus the filtering from said HAVING will be different.
Let's take the following sample data:
CREATE TABLE dbo.MyTable (Grp char(1),
Quantity int,
Required int);
INSERT INTO dbo.MyTable (Grp, Quantity, [Required])
VALUES('a',2,7),
('a',14,2),
('b',4, 7),
('b',3,4),
('c',17,5);
Now we'll perform an overly simplified version of your query:
SELECT SUM(Quantity)
FROM dbo.MyTable
HAVING SUM(Quantity) > SUM(Required);
This brings back the value 40; which is the SUM of all the values in Quantity. A value is returned because the total SUM of Required is 25.
Now let's add a GROUP BY like your second query:
SELECT SUM(Quantity)
FROM dbo.MyTable
GROUP BY Grp
HAVING SUM(Quantity) > SUM(Required);
Now we have 2 rows, with the values 16 and 17 giving a total value of 33. That's because the rows where Grp have a value of 'B' are filtered out, as the SUM of Quantity is lower that Required for 'B'.
The same is happening in your data; in the grouped data you have groups where the HAVING condition isn't met, so those rows aren't returned.

Cumulative substract throught multiple rows

I would like to substract one row from multiple rows. I need to get remaining Quantity (differentiated by BusTransaction_ID and Artikl, and ordered by X_PDateMonth$DATE), which is result of this substract:
And expected results:
Result can be with or without "zero rows". I don't know, how to accomplish this result. And will be better use some "stored procedure" or something, because it will be use to a pretty large data set?
Thanks for all replies.
Here is a solution that works by doing the following:
Calculates the cumulative sums of the values in the first table.
Based on the cumulative sum, determines the value to subtract.
The query looks like this:
select t.bustransaction_id, t.artikl, t.xpldate,
(case when cumeq <= subt.quantity then 0
when cumeq - t.quantity <= subt.quantity
then cumeq - subt.quantity
else t.quantity
end) as newquantity
from (select t.*,
sum(quantity) over (partition by bustransaction_id, artikl order by xpldate) as cumeq
from start_table t
) t left join
subtract_table subt
on t.bustransaction_id = subt.bustransaction_id and
t.artikl = subt.artikl
order by t.bustransaction_id, t.artikl, t.xpldate;
Here is the SQL Fiddle (based on Brians).
This will give you the result with the 'zero rows' using analytic functions:
select x.*,
case
when subqty >= runner
then 0
when runner > subqty
and lag(runner, 1) over( partition by bustransaction_id, artikl
order by bustransaction_id, artikl, xpldate ) > subqty
then quantity
else runner - subqty
end as chk
from (select s.bustransaction_id,
s.artikl,
s.xpldate,
s.quantity,
sum(s.quantity) over( partition by s.bustransaction_id, s.artikl
order by s.bustransaction_id, s.artikl, s.xpldate ) as runner,
z.quantity as subqty
from start_table s
join subtract_table z
on s.bustransaction_id = z.bustransaction_id
and s.artikl = z.artikl) x
order by bustransaction_id, artikl, xpldate
Fiddle: http://sqlfiddle.com/#!6/20987/1/0
The CASE statement combined with the LAG function is what identifies the first "half-depleted" row, which is the biggest piece of your calculation.
In that fiddle I included my derived columns that were necessary to get what you wanted. If you don't want to show those columns you can just select those you need from the inline view, as shown here: http://sqlfiddle.com/#!6/20987/2/0

Datediff between two tables

I have those two tables
1-Add to queue table
TransID , ADD date
10 , 10/10/2012
11 , 14/10/2012
11 , 18/11/2012
11 , 25/12/2012
12 , 1/1/2013
2-Removed from queue table
TransID , Removed Date
10 , 15/1/2013
11 , 12/12/2012
11 , 13/1/2013
11 , 20/1/2013
The TansID is the key between the two tables , and I can't modify those tables, what I want is to query the amount of time each transaction spent in the queue
It's easy when there is one item in each table , but when the item get queued more than once how do I calculate that?
Assuming the order TransIDs are entered into the Add table is the same order they are removed, you can use the following:
WITH OrderedAdds AS
( SELECT TransID,
AddDate,
[RowNumber] = ROW_NUMBER() OVER(PARTITION BY TransID ORDER BY AddDate)
FROM AddTable
), OrderedRemoves AS
( SELECT TransID,
RemovedDate,
[RowNumber] = ROW_NUMBER() OVER(PARTITION BY TransID ORDER BY RemovedDate)
FROM RemoveTable
)
SELECT OrderedAdds.TransID,
OrderedAdds.AddDate,
OrderedRemoves.RemovedDate,
[DaysInQueue] = DATEDIFF(DAY, OrderedAdds.AddDate, ISNULL(OrderedRemoves.RemovedDate, CURRENT_TIMESTAMP))
FROM OrderedAdds
LEFT JOIN OrderedRemoves
ON OrderedAdds.TransID = OrderedRemoves.TransID
AND OrderedAdds.RowNumber = OrderedRemoves.RowNumber;
The key part is that each record gets a rownumber based on the transaction id and the date it was entered, you can then join on both rownumber and transID to stop any cross joining.
Example on SQL Fiddle
DISCLAIMER: There is probably problem with this, but i hope to send you in one possible direction. Make sure to expect problems.
You can try in the following direction (which might work in some way depending on your system, version, etc) :
SELECT transId, (sum(add_date_sum) - sum(remove_date_sum)) / (1000*60*60*24)
FROM
(
SELECT transId, (SUM(UNIX_TIMESTAMP(add_date)) as add_date_sum, 0 as remove_date_sum
FROM add_to_queue
GROUP BY transId
UNION ALL
SELECT transId, 0 as add_date_sum, (SUM(UNIX_TIMESTAMP(remove_date)) as remove_date_sum
FROM remove_from_queue
GROUP BY transId
)
GROUP BY transId;
A bit of explanation: as far as I know, you cannot sum dates, but you can convert them to some sort of timestamps. Check if UNIX_TIMESTAMPS works for you, or figure out something else. Then you can sum in each table, create union by conveniently leaving the other one as zeto and then subtracting the union query.
As for that devision in the end of first SELECT, UNIT_TIMESTAMP throws out miliseconds, you devide to get days - or whatever it is that you want.
This all said - I would probably solve this using a stored procedure or some client script. SQL is not a weapon for every battle. Making two separate queries can be much simpler.
Answer 2: after your comments. (As a side note, some of your dates 15/1/2013,13/1/2013 do not represent proper date formats )
select transId, sum(numberOfDays) totalQueueTime
from (
select a.transId,
datediff(day,a.addDate,isnull(r.removeDate,a.addDate)) numberOfDays
from AddTable a left join RemoveTable r on a.transId = r.transId
order by a.transId, a.addDate, r.removeDate
) X
group by transId
Answer 1: before your comments
Assuming that there won't be a new record added unless it is being removed. Also note following query will bring numberOfDays as zero for unremoved records;
select a.transId, a.addDate, r.removeDate,
datediff(day,a.addDate,isnull(r.removeDate,a.addDate)) numberOfDays
from AddTable a left join RemoveTable r on a.transId = r.transId
order by a.transId, a.addDate, r.removeDate

"Partitioned" sorting in a SQL query

The following SQL query that displays products sold sorted by cost and number of orders have to be sorted in a partitioned manner. Namely, products with the cost of under $100 should go first and then everything else that is > $100 should follow it. Adding HAVING TS.TotalSold < 100 to the query would accomplish this for the first partition, but would filter out other products. The operation should be atomic, so that this query can be executed only once.
NOTE: cost by which the query has to be partitioned is calculated as a max of two cost columns, which makes things a bit more complicated (the proposed solutions of CASE WHEN won't work as HighestCost is not a column)
SELECT PS.ProductName, TS.TotalSold,
((PS.Cost1 + PS.Cost2 + ABS(PS.Cost1-PS.Cost2)) / 2) as HighestCost
FROM Products as PS
CROSS APPLY
(SELECT
(SELECT COUNT(OrderId)
FROM Orders as OS
WHERE OS.ProductId=PS.ProductId)
as TotalSold) TS
ORDER BY HighestCost ASC, TS.TotalSold
EDIT: modified the query to include calculated cost by which the query has to be partitioned.
EDITED
SELECT *
FROM
(
SELECT PS.ProductName, TS.TotalSold,
((PS.Cost1 + PS.Cost2 + ABS(PS.Cost1-PS.Cost2)) / 2) as HighestCost
FROM Products as PS
CROSS APPLY
(SELECT COUNT(OrderId) as TotalSold
FROM Orders as OS
WHERE OS.ProductId=PS.ProductId) TS
) SQ
ORDER BY CASE WHEN HighestCost > 100 THEN 1 END ASC, TotalSold
original below
SELECT PS.ProductName, TS.TotalSold
FROM Products as PS
CROSS APPLY
(SELECT COUNT(OrderId) as TotalSold
FROM Orders as OS
WHERE OS.ProductId=PS.ProductId) TS
ORDER BY
CASE WHEN TS.TotalSold > 100 THEN 1 END, PS.Cost ASC, TS.TotalSold
You may notice I removed a subquery level since it was extraneous.
I don't know which dbms you are using but in mine I would use a calculated column to assign a partitionId, and sort by that. Something like this:
SELECT PS.ProductName, TS.TotalSold,
(if cost < 100 then 1 else 2 endif) as partition
FROM Products as PS
CROSS APPLY
(SELECT
(SELECT COUNT(OrderId)
FROM Orders as OS
WHERE OS.ProductId=PS.ProductId)
as TotalSold) TS
ORDER BY partition, PS.Cost ASC, TS.TotalSold