I have a simple table that stores stock levels. ie.
ID int PK
LocationID int
StockLevel real
There could be multiple rows in this table for each location ie:
ID | LocationID | StockLevel
----------------------------
1 | 1 | 100
2 | 1 | 124
3 | 2 | 300
In this example its trivial to see that 224 units exist at location 1.
When I come to decrement the stock level at location 1 I am using
a cursor to iterate over all rows at where LocationID is 1 and using some simple
logic decide whether the stock available at the current row will satisfy the passed in
decrement value. If the row has sufficient quantity to satisfy the requirement I decrement the rows value and break out of the cursor, and end the procedure, however if the row doesnt have sufficient quantity available I decrement its value to zero and move to the next row and try again (with the reduced quantity)
Its quite simple and works ok, but the inevitable question is: Is there a way of performing
this RBAR operation without a cursor?? I have attempted to search for alternatives but even wording
the search criteria for such an operation is painful!
Thanks in advance
Nick
ps. I am storing data in this format because each row also contains other columns that are unique, and hence cant simply be aggregated into one row for each location.
pps. Cursor Logic as requested (where '#DecrementStockQuantityBy' is the quantity that we need
to reduce the stock level by at the specified location):
WHILE ##FETCH_STATUS = 0
BEGIN
IF CurrentRowStockStockLevel >= #DecrementStockQuantityBy
BEGIN
--This row has enough stock to satisfy decrement request
--Update Quantity on the Current Row by #DecrementStockQuantityBy
--End Procedure
BREAK
END
IF CurrentRowStockStockLevel < #DecrementStockQuantityBy
BEGIN
--Update CurrentRowStockStockLevel to Zero
--Reduce #DecrementStockQuantityBy by CurrentRowStockStockLevel
--Repeat until #DecrementStockQuantityBy is zero or end of rows reached
END
FETCH NEXT FROM Cursor
END
Hope this is clear enough? Let me know if further/better explanation is required.
Thanks
You are correct sir a simple update statement can help you in this scenario I'm still trying to find a legitimate use for a cursor or while that I can't solve with CTE or set based.
After looking a little deeper into your question I will also propose an alternate solution:
Declare #LocationValue int = 1,#decimentvalue int = 20
with temp (id,StockLevel,remaining) as (
select top 1 id, Case when StockLevel - #decimentvalue >0 then
StockLevel = StockLevel - #decimentvalue
else
StockLevel = 0
end, #decimentvalue - StockLevel
from simpleTable st
where st.LocationID = #LocationValue
union all
select top 1 id, Case when StockLevel - t.remaining >0 then
StockLevel = StockLevel -t.remaining
else
StockLevel = 0
end, t.remaining - StockLevel
from simpleTable st
where st.LocationID = #LocationValue
and exists (select remaining from temp t
where st.id <> t.id
group by t.id
having min(remaining ) >0) )
update st
set st.StockLevel = t.StockLevel
from simpleTable st
inner join temp t on t.id = st.id
Related
EDIT: I apologize for phrasing my question first time the wrong way ... I hope this one will make it clearer:
I query the DB for quantities of a certain item (in my case item_id = 321) on all possible locations he might be having quantities on.
I am getting the following result:
item_location_id item_def_loc_name qty_per_location item_id
8 A08 0.00 321
962 POL_113 30.00 321
5 A05 60.00 321
From these three given results (might be more or less in some cases) where we have one line containing zero qty result and couple others with greater quantities - I am aiming for the first "qty_per_location" that is greater than zero but lesser than the next quantity (in this example it is 30).
But for other items that have only one location assigned to them, query will be returning only one result line with qty_per_location = 0 - and I need it to be displayed because there is no greater quantities.
So if I have three quantities on stock for current item, like in the example below, the one with qty = 30 would be the desired one. But if I have only one location with 0 qty on it - then I should get zero as a result.
Hi once again - I ended up doing some fiddling and ended up using this as a possible solution:
DECLARE #QTY as decimal(5,2);
SELECT #QTY = (SELECT SUM(qty_per_location)
FROM [asm].[dbo].[mdta_item-item_def_loc]
where item_id = 321
group by item_id)
SELECT CASE (#QTY) WHEN 0 THEN
(Select top 1 IsNull(qty_per_location, 0) as qty
from [asm].[dbo].[mdta_item-item_def_loc] -- change this to your table name
Where qty_per_location = 0 and item_id = 321
Order by qty_per_location)
ELSE
(Select top 1 IsNull(qty_per_location, 0) as qty
from [asm].[dbo].[mdta_item-item_def_loc] -- change this to your table name
Where qty_per_location > 0 and item_id = 321
Order by qty_per_location)
END
But, the trouble with this script is that I am unable to get [qty_per_location] to be displayed as header / column name above the qty result - I am getting "(No column name)". Also I am not able to get [item_location_id] column to show up beside [qty_per_location] as well ...
Please share your thoughts.
Thank you all!
This query will return the first quantity value in the table or 0 if there aren't any rows with positive value in the table:
Select IsNull(firstQty.qty_per_location, 0) as qty_per_location,
tbl.item_location_id
from mdta_item-item_def_loc tbl
left outer join (
select top 1 tbl2.item_id, tbl2.qty_per_location, tbl2.item_location_id
from mdta_item-item_def_loc tbl2
Where tbl2.qty_per_location > 0 and tbl2.item_id = 321
Order by tbl2.qty_per_location
) firstQty on tbl.item_id = firstQty.item_id and tbl.item_location_id = firstQty.item_location_id
Where tbl.item_id = 321
If you sort the query on the qty_per_location, you can get the lowest or the highest value too.
I want to thank to all people of good will, that helped me solve this SQL puzzle of mine by spending their time to point me on my way :)
This is what I adopted and ended up using, that satisfied my requirement:
DECLARE #QTY as decimal(5,2);
SET #QTY = 0;
IF #QTY < (SELECT SUM(qty_per_location)
FROM [asm].[dbo].[mdta_item-item_def_loc]
where item_id = 321
group by item_id)
Select top 1 qty_per_location, [item_def_loc_name]
from [dbo].[mdta_item-item_def_loc]
inner join [dbo].[mdta_item_def_loc] on
[dbo].[mdta_item_def_loc].[item_def_loc_id] = [dbo].[mdta_item-item_def_loc].[item_location_id]
where item_id = 321
order by qty_per_location desc
ELSE
Select top 1 qty_per_location, [item_def_loc_name]
from [dbo].[mdta_item-item_def_loc]
inner join [dbo].[mdta_item_def_loc] on
[dbo].[mdta_item_def_loc].[item_def_loc_id] = [dbo].[mdta_item-item_def_loc].[item_location_id]
where item_id = 321
Best regards y'all!
I am working with an ERP that stores its retail sales and any discounts related to that sale as separate lines in the same table.
Product lines are identified by a line type of 0 whereas discount lines are a line type of 6, and loyalty discount is a line type of 7.
Unfortunately, the discount records do not have any direct relation back to the product line that the discount applies to.
I need to extract this data and for the discount lines, provide the key for the product line that it applies to.
Discount Lines (6 and 7) ALWAYS come up after 0 LineType and can be identified by the sequence number. For example, in my table below, the second and third line are discounts for the first line because they have a LineType of 6 and 7 that come after LineType 0.
http://sqlfiddle.com/#!3/80249
I need my results to look like this:
Id LineType SequenceNumber Description ProductLineId
---------------------------------------------------------------------
1 0 1 Product A NULL
2 6 2 Discount on Product A 1
3 7 3 Loyalty Discount 1
4 0 4 Product B NULL
5 0 5 Product C NULL
6 6 6 Discount on Product C 5
I know I can use a LAG function, however the offset is not known, as there may be more discounts. I am currently using a cursor similar to the below which is working, but got awfully slow (the below is the same concept however it sets a Flag against the data, not the ID of the record:
OPEN SalesLines
FETCH NEXT FROM SalesLines INTO #SourceKey, #HeaderSourceKey, #IsMarkedDownFlag, #LineSequenceNumber, #RetailLineTypeId
WHILE ##FETCH_STATUS = 0
BEGIN
IF #RetailLineTypeId = 1 OR #CurrentHeaderSourceKey != #HeaderSourceKey
BEGIN
SET #CurrentIsMarkedDownFlag = #IsMarkedDownFlag
SET #CurrentHeaderSourceKey = #HeaderSourceKey
END
ELSE
BEGIN
IF #CurrentIsMarkedDownFlag != #IsMarkedDownFlag
BEGIN
UPDATE #RawData
SET IsMarkedDown = #CurrentIsMarkedDownFlag
WHERE SourceKey = #SourceKey
END
END
You can achieve this using OUTER APPLY:
SELECT s.*, productLine.Id as ProductLineId FROM Sales s
OUTER APPLY (SELECT TOP 1 s1.Id FROM Sales s1
WHERE s1.SequenceNumber < s.SequenceNumber AND s1.LineType=0 AND s.LineType>0
ORDER BY SequenceNumber DESC) as productLine
Creating index on SequenceNumber and LineType can speed it up.
This is just a quick try, but i think it provides the desired output. Any improvements are welcome!
;WITH products
AS
(SELECT Id, SequenceNumber
FROM sales
WHERE LineType = 0)
SELECT s.Id, s.LineType, s.SequenceNumber, s.Description, p.Id AS ProductLineId
FROM sales s
LEFT JOIN products p
ON p.SequenceNumber =
(SELECT MAX(SequenceNumber) FROM products p2
WHERE p2.SequenceNumber < s.SequenceNumber)
AND s.LineType <> 0
Find next lines with type <> 0 which less than next 0 line
edited
My query is as follows:
I have a table Named CALLS as given below:
CALL_NUMBER PART_NUMBER QTY ARRIVAL_DATE
A1 XXX1 5
B2 YYY2 25
C3 ZZZ3 120
D4 ZZZ3 80
E5 ZZZ3 25
And another table SHIPPING as given below:
PART_NUMBER QTY SHIP_DATE ARRIVAL_DATE
XXX1 100 26-Dec 28-Dec
YYY2 5 29-Dec 6-Jan
ZZZ3 200 29-Dec 18-Jan
Now my aim is to put in an arrival date in the calls table based on if the qty required is satisfied by the quantity shipped. If not then, no date should be given in the CALLS table.
I thought the update query below may work:
UPDATE CALLS
SET ARRIVAL_DATE = s.ARRIVAL_DATE
FROM
(
SELECT PART_NUMBER,QTY,ARRIVAL_DATE
FROM SHIPPING
)s
WHERE PART_NUMBER = s.PART_NUMBER
AND QTY < s.QTY
But then how do I subtract the assigned quantities from the remining ones in SHIPPING?
Kindly help me out with this one.
Correct me if wrong. You want to update ARRIVAL_DATE column from Calls table and if the ARRIVAL_DATE column updates then at the same time you want to reduce its QTY value from SHIPPING table.
If yes then this can only be done by looping through each rows in calls table and updating the shipping table after. I have used while loop instead of cursors as mentioned below :
declare #i int = (select count(*) from calls)
declare #j int = 0
declare #t table
(part_number varchar(10),qty int)
while #j <= #i
begin
update a
set a.arrival_date = b.arrival_date
output inserted.part_number,inserted.qty into #t
from
(select *,row_number() over (order by (select 1)) as rn
from calls
) as a
inner join shipping as b
on a.part_number = b.part_number
where a.qty < = b.qty and a.rn = (case when #j = 0 then 1 else #j end)
update a
set a.qty = a.qty - b.qty
from shipping as a
inner join #t as b
on a.part_number = b.part_number
delete from #t
set #j = #j + 1
end
Shouldn't you just have to subtract the QTY in CALLS from the QTY in SHIPPING?
(updated 12/16 with the following as the original update didn't lend itself well to what I'm proposing...take a look)
UPDATE CALLS
SET C.PART_NUMBER = S.PART_NUMBER,
C.QTY = SUM(C.QTY - S.QTY),
C.ARRIVAL_DATE = S.ARRIVAL_DATE
FROM SHIPPING S JOIN CALLS C ON C.PART_NUMBER = S.PART_NUMBER
WHERE C.QTY < S.QTY
Since you are only updating the ones with enough quantities in CALLS then all that satisfy updating the ARRIVAL_DATE field - unless I'm misunderstanding something.
Please, as always with SQL.. test this first somewhere before using on live data. :)
First attempt at a cursor so take it easy =P The cursor is supposed to grab a list of company ids that are all under a umbrella group. Then target a specific company and copy its workflow records to the companies in the cursor.
It infinitely inserts these workflow records into all the companies ... what is the issue here?
Where is the n00b mistake?
DECLARE #GroupId int = 36;
DECLARE #CompanyToCopy int = 190
DECLARE #NextId int;
Declare #Companies CURSOR;
SET #Companies = CURSOR FOR
SELECT CompanyId
FROM Company C
INNER JOIN [Group] G
ON C.GroupID = G.GroupID
WHERE C.CompanyID != 190
AND
G.GroupId = #GroupId
AND
C.CompanyID != 0
OPEN #Companies
FETCH NEXT
FROM #Companies INTO #NextId
WHILE (##FETCH_STATUS = 0)
BEGIN
INSERT INTO COI.Workflow(CompanyID, EndOfWorkflowAction, LetterType, Name)
(SELECT
#NextId,
W.EndOfWorkflowAction,
W.LetterType,
W.Name
FROM COI.Workflow W)
FETCH NEXT
FROM #Companies INTO #NextId
END
CLOSE #Companies;
DEALLOCATE #Companies;
Edit:
I decided to attempt making this set based just because after being told to do it ... I realized I didn't really quite have the answer as to how to do it as a set based query.
Thanks for all the help everyone. I'll post the set based version for posterity.
INSERT INTO COI.Workflow(CompanyID, EndOfWorkflowAction, LetterType, Name)
(
SELECT
CG.CompanyId,
W.EndOfWorkflowAction,
W.LetterType,
W.Name
FROM COI.Workflow W
CROSS JOIN (SELECT C.CompanyID
FROM Company C
INNER JOIN [Group] G
ON G.GroupID = C.GroupID
WHERE C.CompanyID != 190
AND
C.CompanyID != 0
AND
G.GroupID = 36
) AS CG
WHERE W.CompanyID = 190
)
You have no WHERE condition on this:
SELECT
#NextId,
W.EndOfWorkflowAction,
W.LetterType,
W.Name
FROM COI.Workflow W
-- WHERE CompanyID = #CompanyToCopy -- This should be here
So you are getting a kind of doubling effect.
initial state, company 190, seed row (0)
pass one, company 2, copy of seed row (1)
now 2 rows
pass two, company 3, copy of seed row (0) - call this (2)
pass two, company 3, copy of copy of seed row (1) - call this (3)
now 4 rows
then 8 rows, etc
You are inserting a new copy of all workflow records in the workflow table for each iteration, so it will double in size each time. If you for example have 30 items in your cursor, you will end up with a workflow table with 1073741824 times more records than it had before.
I beieve your logic is wrong (it's somewhat hidden because of the use of a cursor!).
Your posted code is attempting to insert a row into into COI.Workflow for every row in COI.Workflow times the number of companies matching your first select's conditions. (Notice how your insert's SELECT statement has no condition: you are selecting the whole table). On each time through the loop, you are doubling the number of rows in COI.Workflow
So, it's not infinite but it could well be very, very long!
I suggest you rewrite as a set based statement and the logic will become clearer.
First use of cursor is OK, all problems in INSERT ... SELECT logic.
I cannot understand what do you need to insert into COI.Workflow table.
I agree with previous commentatorts that your current WHERE condition doubles records, but I cannot believe that you want to insert the full-doubled records for each company each time.
so, I think you need something like
INSERT INTO COI.Workflow(CompanyID, EndOfWorkflowAction, LetterType, Name)
(SELECT TOP 1
#NextId,
W.EndOfWorkflowAction,
W.LetterType,
W.Name
FROM COI.Workflow W)
Or, we need to know more about your logic of inserting the records.
Technologies: SQL Server 2008
So I've tried a few options that I've found on SO, but nothing really provided me with a definitive answer.
I have a table with two columns, (Transaction ID, GroupID) where neither has unique values. For example:
TransID | GroupID
-----------------
23 | 4001
99 | 4001
63 | 4001
123 | 4001
77 | 2113
2645 | 2113
123 | 2113
99 | 2113
Originally, the groupID was just chosen at random by the user, but now we're automating it. Thing is, we're keeping the existing DB without any changes to the existing data(too much work, for too little gain)
Is there a way to query "GroupID" on table "GroupTransactions" for the next available value of GroupID > 2000?
I think from the question you're after the next available, although that may not be the same as max+1 right? - In that case:
Start with a list of integers, and look for those that aren't there in the groupid column, for example:
;WITH CTE_Numbers AS (
SELECT n = 2001
UNION ALL
SELECT n + 1 FROM CTE_Numbers WHERE n < 4000
)
SELECT top 1 n
FROM CTE_Numbers num
WHERE NOT EXISTS (SELECT 1 FROM MyTable tab WHERE num.n = tab.groupid)
ORDER BY n
Note: you need to tweak the 2001/4000 values int the CTE to allow for the range you want. I assumed the name of your table to by MyTable
select max(groupid) + 1 from GroupTransactions
The following will find the next gap above 2000:
SELECT MIN(t.GroupID)+1 AS NextID
FROM GroupTransactions t (updlock)
WHERE NOT EXISTS
(SELECT NULL FROM GroupTransactions n WHERE n.GroupID=t.GroupID+1 AND n.GroupID>2000)
AND t.GroupID>2000
There are always many ways to do everything. I resolved this problem by doing like this:
declare #i int = null
declare #t table (i int)
insert into #t values (1)
insert into #t values (2)
--insert into #t values (3)
--insert into #t values (4)
insert into #t values (5)
--insert into #t values (6)
--get the first missing number
select #i = min(RowNumber)
from (
select ROW_NUMBER() OVER(ORDER BY i) AS RowNumber, i
from (
--select distinct in case a number is in there multiple times
select distinct i
from #t
--start after 0 in case there are negative or 0 number
where i > 0
) as a
) as b
where RowNumber <> i
--if there are no missing numbers or no records, get the max record
if #i is null
begin
select #i = isnull(max(i),0) + 1 from #t
end
select #i
In my situation I have a system to generate message numbers or a file/case/reservation number sequentially from 1 every year. But in some situations a number does not get use (user was testing/practicing or whatever reason) and the number was deleted.
You can use a where clause to filter by year if all entries are in the same table, and make it dynamic (my example is hardcoded). if you archive your yearly data then not needed. The sub-query part for mID and mID2 must be identical.
The "union 0 as seq " for mID is there in case your table is empty; this is the base seed number. It can be anything ex: 3000000 or {prefix}0000. The field is an integer. If you omit " Union 0 as seq " it will not work on an empty table or when you have a table missing ID 1 it will given you the next ID ( if the first number is 4 the value returned will be 5).
This query is very quick - hint: the field must be indexed; it was tested on a table of 100,000+ rows. I found that using a domain aggregate get slower as the table increases in size.
If you remove the "top 1" you will get a list of 'next numbers' but not all the missing numbers in a sequence; ie if you have 1 2 4 7 the result will be 3 5 8.
set #newID = select top 1 mID.seq + 1 as seq from
(select a.[msg_number] as seq from [tblMSG] a --where a.[msg_date] between '2023-01-01' and '2023-12-31'
union select 0 as seq ) as mID
left outer join
(Select b.[msg_number] as seq from [tblMSG] b --where b.[msg_date] between '2023-01-01' and '2023-12-31'
) as mID2 on mID.seq + 1 = mID2.seq where mID2.seq is null order by mID.seq
-- Next: a statement to insert a row with #newID immediately in tblMSG (in a transaction block).
-- Then the row can be updated by your app.