First, let me state right off that I'm well aware that cursors are generally evil and shouldn't be used - I'm all about using sets, but just couldn't come up with a set-based solution to this particular problem. If you tell me to go do some set-based operations, well, I'm all for it, if you can tell me how you'd code this particular problem.
Basically, I've got a number of stock items for which I need to make purchases. I want to make purchases based upon the cheapest available price, where I know the suppliers' prices and their stock levels. There's also a pack-size issue in here, wherein I want to buy by pack-size if possible.
I've already pulled a list of the things I need to purchase into #needorders, and suppliers' stock levels and prices into #orderedprices. Below I'm iterating through cursor CUR_NEEDED and creating a secondary cursor CUR_AVAILABLE:
DECLARE CUR_NEEDED CURSOR LOCAL SCROLL_LOCKS
FOR
SELECT GoodID
, ConditionID
, QuantityToShip
, OrderStatusID
, RetailerID
, PackSize
FROM #needorders
ORDER BY GoodID
, ConditionID
, PurchaseDate DESC
FOR UPDATE
OPEN CUR_NEEDED
FETCH NEXT FROM CUR_NEEDED INTO #GoodID, #ConditionID, #QuantityToShip, #OrderStatusID, #RetailerID, #PackSize
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE CUR_AVAILABLE CURSOR LOCAL SCROLL_LOCKS
FOR
SELECT SupplierStocklistItemID
, SupplierID
, StockLevel
, SupplierCurrencyID
, CostPrice
FROM #orderedprices
WHERE #orderedprices.GoodID = #GoodID
AND #orderedprices.ConditionID = #ConditionID
AND #orderedprices.StockLevel > 0
ORDER BY #orderedprices.PriceRank
FOR UPDATE
OPEN CUR_AVAILABLE
FETCH NEXT FROM CUR_AVAILABLE INTO #SupplierStocklistItemID, #SupplierID, #StockLevel, #SupplierCurrencyID, #CostPrice
WHILE ##FETCH_STATUS = 0
BEGIN
/*
Buy as many #PackSize as we need to cover how many we require, unless the supplier
only has a certain number, in which case buy that number.
E.g., need 14, pack size 5, 2 suppliers
Supplier A has 11
Supplier B has 40
Buy 9 from Supplier A, with our remaining need being 3.
Buy 5 from supplier B, with our remaining need being -2
*/
--feed rows into #supplierpurchasesbase while #StockLevel > 0
--Figure out how many we need to buy, based upon PackSize
IF #QuantityToShip % #PackSize > 0
BEGIN
SET #Buy = #QuantityToShip - #QuantityToShip % #PackSize + #PackSize
END
ELSE
BEGIN
SET #Buy = #QuantityToShip
END
IF #StockLevel < #Buy
BEGIN
--PRINT 'Supplier only has ' + CAST(#StockLevel AS VARCHAR) + ' for us to buy.'
SET #Buy = #StockLevel
END
INSERT INTO #supplierpurchasesbase (
GoodID
, ConditionID
, SupplierStocklistItemID
, Quantity
, SupplierID
, SupplierCurrencyID
, CostPrice
, RetailerID )
SELECT #GoodID
, #ConditionID
, #SupplierStocklistItemID
, #Buy
, #SupplierID
, #SupplierCurrencyID
, #CostPrice
, #RetailerID
--update #QuantityToShip & the row in CUR_AVAILABLE
IF #StockLevel <= #Buy
BEGIN
UPDATE CUR_AVAILABLE
SET StockLevel = #StockLevel - #Buy
WHERE CURRENT OF CUR_AVAILABLE
SET #QuantityToShip = 0
END
ELSE
BEGIN
UPDATE CUR_AVAILABLE
SET StockLevel = 0
WHERE CURRENT OF CUR_AVAILABLE
SET #QuantityToShip = #QuantityToShip - #Buy
END
--update the stocklevel so we don't see the thing again if we've used it up.
IF #QuantityToShip = 0 --Don't need any more
BEGIN
UPDATE CUR_NEEDED
SET OrderStatusID = #StatusPendingPO
WHERE CURRENT OF CUR_NEEDED
BREAK
END
ELSE --Need more, move next, if we can
FETCH NEXT FROM CUR_AVAILABLE INTO #SupplierStocklistItemID, #SupplierID, #StockLevel, #SupplierCurrencyID, #CostPrice
END
CLOSE CUR_AVAILABLE
DEALLOCATE CUR_AVAILABLE
FETCH NEXT FROM CUR_NEEDED INTO #GoodID, #ConditionID, #QuantityToShip, #OrderStatusID, #RetailerID, #PackSize
END
CLOSE CUR_NEEDED
DEALLOCATE CUR_NEEDED
The problem I'm running into is that I get I'm getting the error
Invalid object name 'CUR_AVAILABLE'.
when I'm attempting to update CURRENT OF CUR_AVAILABLE.
I've tried defining the CUR_AVAILABLE cursor as #CUR_AVAILABLE but get a different error. I've tried defining the CUR_AVAILABLE cursor outside of the WHILE loop of CUR_NEEDED, I've tried not closing / deallocating the cursor, etc. None of this seems to work.
Any ideas where I'm going wrong, here (other than not using sets, unless you've got a set-based solution)?
The following query uses a recursive CTE and, therefore, can't be considered a truly set-based solution. Nevertheless, I would still expect it to perform better than your two cursors (or to be worth trying, at the very least):
WITH buys (
GoodID,
ConditionID,
SupplierStocklistItemID,
Quantity,
SupplierID,
SupplierCurrencyID,
CostPrice,
RetailerID,
PriceRank,
RemainingNeed,
PackSize
)
AS (
SELECT
GoodID,
ConditionID,
SupplierStocklistItemID = 0,
Quantity = 0,
SupplierID = 0,
SupplierCurrencyID = 0,
CostPrice = CAST(0.00 AS decimal(10,2)),
RetailerID,
PriceRank = 0,
RemainingNeed = QuantityToShip,
PackSize
FROM #needorders
UNION ALL
SELECT
p.GoodID,
p.ConditionID,
p.SupplierStockListItemID,
Quantity = y.CurrentBuy,
p.SupplierID,
p.SupplierCurrencyID,
p.CostPrice,
b.RetailerID,
p.PriceRank,
RemainingNeed = b.RemainingNeed - y.CurrentBuy,
b.PackSize
FROM #orderedprices p
INNER JOIN buys b ON p.GoodID = b.GoodID
AND p.ConditionID = b.ConditionID
AND p.PriceRank = b.PriceRank + 1
CROSS APPLY (
SELECT RemainingNeedAdjusted =
(b.RemainingNeed + b.PackSize - 1) / b.PackSize * b.PackSize
) x
CROSS APPLY (
SELECT CurrentBuy = CASE
WHEN x.RemainingNeedAdjusted > p.StockLevel
THEN p.StockLevel
ELSE x.RemainingNeedAdjusted
END
) y
WHERE p.StockLevel > 0
AND b.RemainingNeed > 0
)
SELECT
GoodID,
ConditionID,
SupplierStocklistItemID,
Quantity,
SupplierID,
SupplierCurrencyID,
CostPrice,
RetailerID
FROM buys
WHERE PriceRank > 0
ORDER BY
GoodID,
ConditionID,
PriceRank
Basically, the CTE forms the rows almost identical to those your query is inserting into #supplierpurchasesbase, except it additionally features auxiliary columns serving as kind of internal variables. (They are not pulled by the final SELECT, though.)
The anchor part forms a set of 0-quantity records based on the #needordered table, together with the initial values for the auxiliary columns. The recursive part contains all the logic: calculates the quantity to buy, updates the "remaining need" quantity for the next iteration, checks whether the next iteration is needed.
Certain assumptions have been made, and I hope you'll be able find your way around them if they do not match your real situation. For instance, quantities, pack sizes are assumed to be integer, and part of the logic relies on that, because it uses integral division. It is also assumed that PriceRank is a sequence of integers starting from 1, unique per (GoodID, ConditionID).
This script, as well as a minimal test setup, can be found, tested, modified, and tested on SQL Fiddle.
The problem was twofold: The update syntax should not be:
UPDATE CUR_AVAILABLE
SET StockLevel = #StockLevel - #Buy
WHERE CURRENT OF CUR_AVAILABLE
Rather, the syntax should be:
UPDATE #orderedprices
SET StockLevel = #StockLevel - #Buy
WHERE CURRENT OF CUR_AVAILABLE
Also, in order to be updatable, the temp table needed to have a primary key:
ALTER TABLE #orderedprices ADD CONSTRAINT PRIMARY KEY CLUSTERED (RowCtr)
Lesson learned, I guess, but it certainly took me a fair bit of grief to find the solution!
Related
I think I have this almost figured out but after 50+ Google searches, I ask this: How can I add a column to a db that is essentially a sumif function? I've seen many related questions as simple Select statements for just looking at the table in a mini table but I was hoping to actually add a column that would show these totals. I'm taking this and then pulling the data into R for further analysis.
In Excel it works like so with [ ] denoting columns of a table. It is split into 2 areas via the Serial #. The first 6 digits of the serial indicate the "parent" and the later half indicate the "child". One parent can have multiple children, as seen with BSA101 below. What I'm trying to do is sum all the costs that went into making the child (parent + child costs). So the total parent costs, get allocated to both children below.
"Packing" is the last step so this is where I'd want the totals to end up so there are no duplicates.
Example
=IF(LEN([serial])>6,IF([process]="Packing",SUMIF([serial],[#serial],[process_cost])+SUMIF([serial],LEFT([#serial],6),[process_cost]),""),"")
serial process process_cost total_child_cost
BSA101A33 Packing 10 160
BSA101A34 Packing 10 195
BSA101 Cast 50 ""
BSA101 Mold 30 ""
BSA101 Mold 30 ""
BSA101A33 Finish 15 ""
BSA101A34 Finish 25 ""
BSA101A33 Polish 25 ""
BSA101A34 Polish 50 ""
^desired table result above
MySQL attempt:This post helped me Adding Case Statements
SQL Fiddle: http://sqlfiddle.com/#!9/b0e58
Here I've added a column in data called total_cost. Right now I'm getting an "Invalid use of group function" error which after researching, talks about a HAVING clause but not sure where to place it.
UPDATE data
SET total__child_cost =
(CASE WHEN length(serial) > 6
AND process = 'Packing'
THEN
IF(serial = serial, sum(process_cost),0) END)
+
(CASE WHEN left(serial,6) = serial
THEN sum(process_cost)
END)
This ended up being the solution.
DELIMITER //
CREATE FUNCTION `getParent1`(inSerialn Varchar(20)) RETURNS int(11)
BEGIN
Declare parent varchar(20);
Declare result int;
set parent = left(inSerialn, 6);
set result = (Select sum(process_cost) From mfng.data where serialn = parent);
return result;
END //
DELIMITER //
CREATE FUNCTION `getChild1`(inSerialn Varchar(20)) RETURNS int(11)
BEGIN
Declare result int;
set result = (Select sum(process_cost) FROM mfng.data where serialn = inSerialn);
return result;
END//
UPDATE mfng.data set total_child_cost =
(case when length(serialn) > 6 AND pdn_process = 'Packing'
THEN
getChild1(serialn) + getParent1(serialn)
ELSE
0 END);
//
I have multiple update statements in a stored procedure (as shown below).
Question is I am trying to combine them into one UPDATE statement as there is a performance issue (takes longer to execute stored procedure). I tried putting columns (such as PONUMBER, VENDORID etc) in a single update statement but it is throwing errors.
Please suggest.
UPDATE rptMaster SET PONUMBER = (select top 1 poMaster.PONUMBER from poMaster where poMaster.ITEMNMBR =rptMaster.ITEMNMBR and
poMaster.UnCommited > 0)
UPDATE rptMaster SET VENDORID = (select top 1 poMaster.VENDORID from poMaster where poMaster.ITEMNMBR =rptMaster.ITEMNMBR and
poMaster.UnCommited > 0)
UPDATE rptMaster SET DUEDATE = (select top 1 poMaster.REQDATE from poMaster where poMaster.ITEMNMBR =rptMaster.ITEMNMBR and
poMaster.UnCommited > 0)
UPDATE rptMaster SET POQTYORDER = (select top 1 (poMaster.QTYORDER / rptMaster.UOMQTY) from poMaster where poMaster.ITEMNMBR =rptMaster.ITEMNMBR and
poMaster.UnCommited > 0)
Mine is similar to polkduran's:
WITH PO AS (
SELECT PONUMBER
, VENDORID
, REQDATE
, QTYORDER
, ITEMNMBR
, ROW_NUMBER() OVER (PARTITION BY ITEMNMBR ORDER BY ??) as RN
FROM poMaster
WHERE UnCommited > 0
)
UPDATE rptMaster
SET PONUMBER = po.PONUMBER
, VENDORID = po.VENDORID
, DUEDATE = po.REQDATE
, POQTYORDER = po.QTYORDER / rptMaster.UOMQTY
FROM rptMaster
JOIN PO
ON PO.ITEMNMBR = rptMaster.ITEMNMBR
and PO.RN = 1
I'm using a Common Table Expression (CTE) to assign a row number to each poMaster record, with the records for each value of ITEMNMBR numbered separately. This allows us to pick to the first record for each ITEMNBR in our JOIN, later, similar to the way you were using Top 1 in your subqueries.
Please note, though: because you didn't indicate how you wanted to select the Top 1 record in your query, I had to leave the ORDER BY clause in the CTE unspecified. (I put ?? in as a placeholder.) You need to specify one or more sort fields in place of the ?? so it knows how to sort and number the records.
You can make an update using a join clause:
update rpt
set
PONUMBER = po.PONUMBER,
VENDORID = po.VENDORID,
DUEDATE = po.REQDATE,
POQTYORDER = (po.QTYORDER / rpt.UOMQTY)
from rptMaster rpt
inner join poMaster po
on po.ITEMNMBR = rpt.ITEMNMBR
where po.UnCommited > 0
I don't have a way to test it right now but that might work.
I am stucked with following issue,
I ve delcared a varibale (DOUBLE) in a stored procedure, and i need to assign a value (item price) from a table (item) to this declared variable. however, i need to get this value from a select query which uses case inside, the item price could be in 2 columns based on a logic i have to find the correct item price. Please help me solve this as when i execute it gives me a error,
Here how is layered,
DECLARE no_more_users INT DEFAULT 0;
DECLARE user_id INT DEFAULT 0;
DECLARE cart_id INT DEFAULT 0;
DECLARE cart_item_id INT DEFAULT 0;
DECLARE user_gift_id INT DEFAULT 0;
DECLARE itemPrice DOUBLE DEFAULT 0.0;
SELECT
CASE
WHEN sale_price=0 OR sale_price IS NULL THEN (price - ( price * discount ))
ELSE sale_price
END
INTO itemPrice
FROM item WHERE item_id = p_item_id ;
DECLARE checked_in_users CURSOR FOR
SELECT DISTINCT ul.user_id
FROM user_location ul
LEFT JOIN location_section ls ON ul.location_section_id = ls.location_section_id
INNER JOIN user u ON ul.user_id = u.user_id
INNER JOIN user_profile up ON u.user_id = up.user_id
INNER JOIN location_event le ON ul.location_event_id = le.location_event_id
WHERE ul.location_id = p_location_id AND ul.location_event_id = p_event_id
AND ul.checked_out_on IS NULL AND (ul.checked_in_on BETWEEN le.start_time AND le.end_time )
AND u.status = 1 ;
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET no_more_users = 1;
OPEN checked_in_users;
FETCH checked_in_users INTO user_id;
read_loop: LOOP
some more code...
please note, sale_price, price, and discount are coulmns of item table, The logic is if sale_price is null or value is 0, then i actual sale price should be obtained from price coulmn. Finally what i need is asign corect item price in to previously declared varibale.
Any help will be highly appriciated.
There are two foms to the CASE statement, one with expressions and one with values. You are mixing them up.
with values:
CASE variable
WHEN value_1 THEN foo
WHEN value_2 THEN bar
END
with expressions:
CASE
WHEN expression_1 THEN foo
WHEN expression_2 THEN bar
END
Try
CASE
WHEN sale_price=0 OR sale_price IS NULL THEN (price - ( price * discount ))
ELSE sale_price
END
Also, you do not need the "AS correct_price" when you are using an INTO
Your select query has some problems.
The version of CASE you are are using - ie CASE var WHEN val1 THEN - doesn't work when trying to match a NILL, because in SQL null is not equal to null.
Also, you can't combine values with conditions as you hAve.
Instead, use the version if CASS that simply conditions. There are other suntactic problems. Converting your broken query then gives:
SELECT
CASE
WHEN ifnull(sale_price, 0) = 0 THEN price - (price * discount)
ELSE sale_price
END
INTO itemPrice
FROM item
WHERE item_id = p_item_id;
Note: if your table has s column called itemPrice, you must choose another name from your variable. Mysql gets confused :/
I am using MySQL to make a report showing the number of hours billed for a particular date range and project. The complexity is that the date range is variable for each project (different start month and start day). This information is coming from a value in another database/table.
I have the following UDF in MySQL:
DELIMITER //
CREATE FUNCTION TimeLeft(startday INT, today INT) RETURNS INT
DETERMINISTIC
BEGIN
DECLARE s INT;
IF startday < today THEN SET s = 0;
ELSE SET s = 1;
END IF;
RETURN s;
END //
DELIMITER;
I use that function in the following query, which is supposed to take the value returned in the TimeLeft function to determine the values for the start month (month(curdate())-#xx) and start day (#yy) for each project to calculate the hours:
AND time_records.record_date >= concat('2012/', month(curdate())-#xx , '/' , #yy)
Here's how I am setting the values for #xx and #yy:
SET #xx = 0; #this is the value that we will use to manipulate the month for the date range
SET #yy = 0;
#yy:= SELECT start_day_of_month FROM dashboard.client; #this doesn't seem to work
SELECT #xx:= TimeLeft(#yy,dayofmonth(curdate()));
I am getting some issues:
#yy is not getting the value - possibly my syntax is wrong?
The variables are set at the top of the code, so they are not getting changed for each project as they should be (there should be a different #xx and #yy for each project since each one has a different start and end date).
Here's the full query:
#below is where I assign the variables
SET #xx = 0; #this is the value that we will use to manipulate the month for the date range
SET #yy = 0;
#yy:= SELECT start_day_of_month FROM dashboard.client; #this doesn't seem to work
SELECT #xx:= TimeLeft(#yy,dayofmonth(curdate()));
# below is the MySQL query that is meant to use the variables assigned above
SELECT X.expr1 AS 'Project Name', #monthly_hours - SUM(X.expr2) AS 'Hours Billed
FROM
(SELECT
projects.name AS expr1
, sum(time_records.value) AS expr2
FROM project_objects
INNER JOIN projects
ON projects.id = project_objects.project_id
INNER JOIN time_records
ON time_records.parent_id = project_objects.id
WHERE time_records.parent_type = 'Task'
AND time_records.record_date >= concat('2012/', month(curdate())-#xx , '/' , #yy)
AND time_records.record_date <= curdate()
GROUP BY projects.name
UNION
SELECT
projects.name AS expr1
, sum(time_records.value) as expr2
FROM projects
INNER JOIN time_records
ON projects.id = time_records.parent_id
WHERE time_records.parent_type = 'Project'
AND time_records.record_date >= concat('2012/', month(curdate())-#xx , '/' , #yy)
AND time_records.record_date <= curdate()
GROUP BY projects.name) X
GROUP BY X.expr1
I think there is some issue of where I am assigning the variables #xx and #yy. These should be done for each individual Project, so putting them up on the top is probably not the best idea. I'm also not sure if I am assigning the #yy value correctly. It's supposed to query the value of the field of a table that is in another database but it keeps throwing a syntax error on the #yy assignment to that field.
Assign value to #yy inside select:
SELECT #yy:= start_day_of_month FROM dashboard.client;
I have the an existing table that for some reason the designer decided to manually control the Primary Key value by storing the last used value in a seperate table (changing the table to use Identity is not an option right now).
I now need to do a mass update to this table as follows:
DECLARE #NeedFieldID int
SET #NeedFieldID = 62034
INSERT INTO T_L_NeedField (NeedID, NeedFieldID, FieldName, Sequence, DisplayAs, FieldPrompt, DataType, ValidOptions, IsRequiredForSale)
(
SELECT
DISTINCT n.NeedID,
#NeedFieldID + 1,
'DetailedOutcome',
999,
'Detailed Outcome',
'Select appropriate reason for a No Sale outcome',
'T',
'Pricing, Appointment Date / Time Not Available, Will Call Back, Declined',
0
FROM T_L_Need n
INNER JOIN T_L_NeedField nf
ON n.NeedID = nf.NeedID
WHERE (n.Need LIKE 'Schedule%' AND n.Disabled = 0)
)
Obviously '#NeedFieldID + 1' doesn't work (just using it to show what I want to do). How can I increment #NeedFieldID as SQL inserts the values for each of the distinct NeedId's? I am using SQL Server 2008.
You want row_number():
DECLARE #NeedFieldID int
SET #NeedFieldID = 62034
INSERT INTO T_L_NeedField (NeedID, NeedFieldID, FieldName, Sequence, DisplayAs, FieldPrompt, DataType, ValidOptions, IsRequiredForSale)
(
SELECT
DISTINCT n.NeedID,
#NeedFieldID + row_number() over (order by n.NeedID),
'DetailedOutcome',
999,
'Detailed Outcome',
'Select appropriate reason for a No Sale outcome',
'T',
'Pricing, Appointment Date / Time Not Available, Will Call Back, Declined',
0
FROM T_L_Need n
INNER JOIN T_L_NeedField nf
ON n.NeedID = nf.NeedID
WHERE (n.Need LIKE 'Schedule%' AND n.Disabled = 0)
)
However, your best bet is to make NeedFieldID an identity column and just let SQL Server do the work for you.