Optimizing SQL- swapping out functions for JOINS and confusion when variables exist - sql-server-2008

I just started a new job in Dev Ops at a publishing company. My first task is to optimize a huge SQL query that is composed of functions. Functions are really slow. The guy that created the query was smart but didn't know SQL and used functions when he could have used JOINs instead. I am having trouble with converting the functions that have variables. For example, this is one of the functions. Here it is within the query and next is the associated function stored elsewhere.
dbo.rpt_get_isbn(b.bookkey, 21) AS
f_upc
Then the function...
ALTER FUNCTION [dbo].[rpt_get_isbn](
#i_bookkey INT,
#i_isbn_type INT)
/* Returns the identifier such as EAN,
ISBN, with or without dashes
PARAMETER #i_isbn_type
10 = ISBN10
13 = ISBN 13
16 = EAN
17 = EAN (no dashes)
18 = GTIN
19 = GTIN (no dashes)
20 = LCCN
21 = UPC
*/
RETURNS VARCHAR(50)
AS
BEGIN
DECLARE #RETURN VARCHAR(50)
DECLARE #v_desc VARCHAR(50)
IF #i_isbn_type = 10
BEGIN
SELECT #v_desc = isbn10
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 13
BEGIN
SELECT #v_desc = isbn
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 16
BEGIN
SELECT #v_desc = ean
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 17
BEGIN
SELECT #v_desc = ean13
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 18
BEGIN
SELECT #v_desc = gtin
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 19
BEGIN
SELECT #v_desc = gtin14
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 20
BEGIN
SELECT #v_desc = lccn
FROM isbn
WHERE bookkey = #i_bookkey
END
ELSE IF #i_isbn_type = 21
BEGIN
SELECT #v_desc = upc
FROM isbn
WHERE bookkey = #i_bookkey
END
IF LEN(#v_desc) > 0
BEGIN
SELECT #RETURN =
LTRIM(RTRIM(#v_desc))
END
ELSE
BEGIN
SELECT #RETURN = ''
END
RETURN #RETURN
END
So this function can return various different results based on the variable given as the second parameter. If that wasn't there, this would be easy. I would simply convert it with a solution like this one to retrieve the book's cover..
LTRIM(RTRIM(bo.ean13)) AS p_coverimagepath
and the JOIN needed...
LEFT JOIN Isbn bo WITH (NOLOCK) ON bo.bookkey = b.bookkey
But again, now I'm dealing with parameters and a function that uses If/elses to derive an answer. So do I need to add this if/else logic to my main query? I can't think of a ways that will yield as simple an answer. I look forward to figuring this out. Please let me know if I forgot any crucial elements to understand what I'm doing. Thanks!
UPDATE
Here's where the code is going
,pss8.dbo.xml_StripIllegalChars(dbo.rpt_get_series_volume(b.bookkey)) AS
p_seriesvol
,CASE
WHEN dbo.rpt_get_isbn(b.bookkey, 17) = ''
THEN (
SELECT ipg_id
FROM tmmdb.ipg_extra.dbo.vw_Pss8IsbnOrUpc bo
WHERE bo.bookkey = b.bookkey
)
ELSE dbo.rpt_get_isbn(b.bookkey, 17)
END AS p_coverimagepath
,CASE
WHEN dbo.rpt_get_isbn(b.bookkey, 17) = ''
THEN (
SELECT ipg_id
FROM tmmdb.ipg_extra.dbo.vw_Pss8IsbnOrUpc bo
WHERE bo.bookkey = b.bookkey
)
ELSE dbo.rpt_get_isbn(b.bookkey, 17)
END AS TSP_p_coverimagepath
,pss8.dbo.xml_StripIllegalChars(replace(dbo.rpt_get_title(b.bookkey,
'T'), '&', '&')) AS p_title /* 30OCT14 */
,pss8.dbo.xml_StripIllegalChars(replace(dbo.rpt_get_sub_title(b.bookkey),
'&', '&')) AS p_subtitle /* 20OCT14 */

Not a complete answer, but an example structure to help you dig your way out of this. I have made many assumptions because your question requires more detail. But hopefully you can appreciate this is way simpler.
SELECT
CASE
WHEN ISNULL(LTRIM(RTRIM(bo.ean13)),'') = '' THEN T2.ipg_id
ELSE LTRIM(RTRIM(bo.ean13))
END as p_coverimagepath
FROM MainTable b
LEFT JOIN Isbn bo
ON bo.bookkey = b.bookkey
LEFT JOIN tmmdb.ipg_extra.dbo.vw_Pss8IsbnOrUpc T2
ON T2.bookkey=b.bookkey
In your example code, two columns do exactly the same thing so I haven't repeated this.
A few things noted:
-Don't sprinkle NOLOCK in your code thinking it's a performance boost
-If you're using an outer join, you need to accommodate in case a NULL is returned
-It's easier to join once to tmmdb.ipg_extra.dbo.vw_Pss8IsbnOrUpc and use columns out of it rather than repeat the code over and over.

Related

From error 1109 to error 1242 MySQL

I'm using a procedure to calculate the length of user 'hiatus' (aka contingencies) from the program in our system. It runs after a procedure that determines user status depending on whether they are completing their daily treatment and to what extent.
The purpose of this procedure is to log the length of a user's contingency, by adding a row to a table with the following schema:
id_contingency int(11) NOT NULL AUTO_INCREMENT,
id_user int(11) DEFAULT NULL,
date_start date DEFAULT NULL,
program_day int(11) DEFAULT NULL,
date_end date DEFAULT NULL,
total_days int(11) DEFAULT NULL,
latest_tf_id archer(255) DEFAULT NULL
I considered adding this as a trigger on the update of the user_status table, but I can't risk an error preventing that table from updating. So, this procedure first closes contingencies that were previously open, when the user first entered the hiatus, but has now resumed the program, and it later opens new contingencies for users who have now started a hiatus in their treatment for the first time. It then remains open until they resume the program, and calculates how long they were on hiatus for.
This was my original procedure, and it returned error 1109 (unknown table tbl_user_status) :
DELIMITER $$
CREATE DEFINER=CURRENT_USER PROCEDURE `proc_cont_calc`
NO SQL
BEGIN
#CLOSE OPEN CONTINGENCIES FIRST or d0 > d1
CASE
WHEN tbl_user_status.d4 = 1 AND tbl_user_status.d2 > 0 AND tbl_user_status.user_status = 'seguimiento' THEN
UPDATE tbl_user_contingency, tbl_user_status SET
tbl_user_contingency.date_end = CURRENT_DATE,
tbl_user_contingency.total_days = DATEDIFF(tbl_user_contingency.date_start, tbl_user_contingency.date_end),
tbl_user_contingency.updated_by = 'proc_cont.close'
WHERE tbl_user_contingency.date_end = '' AND tbl_user_contingency.id_smoker = tbl_user_status.id_smoker LIMIT 1;
#OPEN NEW CONTINGENCIES
WHEN tbl_user_status.d5 = 1 AND tbl_user_status.d4 = 0 AND tbl_user_status.user_status = 'contingencia' THEN
INSERT INTO tbl_user_contingency (id_smoker, roadmap_day, date_start, latest_tf_id, updated_by) SELECT
id_smoker, roadmap_day, CURRENT_DATE, latest_tf_id, 'proc_cont.open' FROM tbl_user_status;
END CASE;
END$$
DELIMITER;
So I tried this (amongst other things):
CASE
WHEN (SELECT d4 FROM tbl_user_status) = 1 AND (SELECT d2 FROM tbl_user_status) > 0 AND (SELECT user_status FROM tbl_user_status) = 'seguimiento' THEN
UPDATE tbl_user_contingency, tbl_user_status SET
tbl_user_contingency.date_end = CURRENT_DATE,
tbl_user_contingency.total_days = DATEDIFF(tbl_user_contingency.date_start, tbl_user_contingency.date_end),
tbl_user_contingency.updated_by = 'proc_cont.close'
WHERE tbl_user_contingency.id_smoker = tbl_user_status.id_smoker LIMIT 1;
#OPEN NEW CONTINGENCIES
WHEN (SELECT d5 FROM tbl_user_status) = 1 AND (SELECT d4 FROM tbl_user_status) = 0 AND (SELECT user_status FROM tbl_user_status) = 'contingencia' THEN
INSERT INTO tbl_user_contingency (id_smoker, roadmap_day, date_start, latest_tf_id, updated_by) SELECT
id_smoker, roadmap_day, CURRENT_DATE, latest_tf_id, 'proc_cont.open' FROM tbl_user_status;
END CASE;
And now I'm getting error 1242 returning multiple rows.
How can I get this procedure to run properly? Thanks!
UPDATE - I tried #P.Salmon's suggestion to simply update the rows, but not all the fields were filling out, or the update overruns previous contingencies.
Thanks!
The case statement seems unnecessary here just move the conditions to where clauses for example
UPDATE tbl_user_contingency join tbl_user_status on tbl_user_contingency.id_smoker = tbl_user_status.id_smoker
SET
tbl_user_contingency.date_end = CURRENT_DATE,
tbl_user_contingency.total_days = DATEDIFF(tbl_user_contingency.date_start, tbl_user_contingency.date_end),
tbl_user_contingency.updated_by = 'proc_cont.close'
WHERE tbl_user_contingency.date_end = '' AND
tbl_user_status.d4 = 1 AND tbl_user_status.d2 > 0 AND tbl_user_status.user_status = 'seguimiento'
;
INSERT INTO tbl_user_contingency (id_smoker, roadmap_day, date_start, latest_tf_id, updated_by)
SELECT
id_smoker, roadmap_day, CURRENT_DATE, latest_tf_id, 'proc_cont.open'
FROM tbl_user_status
where tbl_user_status.d5 = 1 AND tbl_user_status.d4 = 0 AND tbl_user_status.user_status = 'contingencia'
;
You could improve your question and get thereby a better response if you describe what it is you are trying to do instead of having us guess by reverse engineering two non working code segments, by adding your table definitions, sample data and expected output as text to your question. BTW I hope you have a mechanism that will stop this thing doing stuff more than once.

How to convert non-numeric values to blank for use in case statement?

I am working on the following query:
declare #start date = '06/01/2016';
declare #end date = '07/31/2017';
----------------------------------------------------------------------------- ------------------------------------------------
-- Pull all claims with paid date in range parameter
-----------------------------------------------------------------------------
------------------------------------------------
if object_id('LA_Temp.dbo.Item19') is not null drop table LA_Temp.dbo.Item19
select distinct
c.claimid,
c.formtype,
c.facilitycode + c.billclasscode as BillType,
case when primaryclaimid = '' and resubclaimid = '' then 'Clean' else 'Other' end as CleanClaim,
-- DHHClaimtype 04 needs to be broken out based on provider specialty and location
Case when c.formtype = '1500' and cd.location = 21 and ps.specialtycode in ('05','22','1T','1F','30','1C') then '04-Hospitalist'
when c.formtype = '1500' then '04-Other'
else ' '
end as DHHClaimtype,
c.status,
c.totalpaid,
e.phystate as MemberState,
e.phycounty as MemberParish,
pc.ParishCode as MemberParishCode,
con.contracted as NetworkProvider,
reject
into LA_Temp.dbo.Item19
from claim c
inner join member m on c.memid = m.memid
inner join entity e on m.entityid = e.entid
left join LA_Temp.dbo.ParishCodes pc on e.phycounty = pc.Parish
inner join contract con on c.contractid = con.contractid
inner join provider p on c.provid = p.provid
inner join provspecialty ps on p.provid = ps.provid and ps.spectype =
'PRIMARY'
inner join claimdetail cd on c.claimid = cd.claimid and cd.claimline = 1 -- just pull the first line to
grab the location code, exclude any location codes with non-numeric values
where c.paiddate between #start and #end
and c.status in ('PAID','DENIED');
-- add the claim types to the table
EXECUTE LA_Temp.[dbo].[USP_LA_SetDHHClaimType] #Table = 'Item19';
The problem exists in the first case statement. Specifically here:
and cd.location = 21
Upon further investigation of the claimdetail (cd) table, I have found that column cd.location (datatype = int) has 4 values ('H', 'U8', 'A', 'OH') which are onviously not numeric. I would like to convert these values to blanks (if possible, not sure if blanks (' ') are compatible with int datatype) or zeros if blanks will not work. Due to the NonNumeric values, I am getting the following error (which is to be expected):
Msg 245, Level 16, State 1, Line 13
Conversion failed when converting the varchar value 'H ' to data type int.
I am aware that I can exclude these records in either the join clause or in a where statement such as:
Where cd.location not in ('H', 'U8', 'A', 'OH')
However, I want to keep the records that these values are tied to, I just want the cd.location value to be blank when it is one of these 4 values. Can someone show me how I can keep these records, by converting cd.location to ' ' when cd.location in ('H', 'U8', 'A', 'OH').
I think I got it...
Again we are focusing on the first case statement in the originally posted query:
case
-- DHHClaimtype 04 needs to be broken out based on provider specialty and location
When c.formtype = '1500' and cd.location = 21 and ps.specialtycode in ('05','22','1T','1F','30','1C')
Then '04-Hospitalist'
When c.formtype = '1500'
Then '04-Other'
else ' '
end as DHHClaimtype,
Changed to:
case
-- DHHClaimtype 04 needs to be broken out based on provider specialty and location
When c.formtype = '1500' and Case When IsNumeric(cd.location) = 0 Then ''
Else cd.location End = 21 and ps.specialtycode in ('05','22','1T','1F','30','1C') then '04-Hospitalist'
When c.formtype = '1500'
Then '04-Other'
else ' '
end as DHHClaimtype,

How to make function execute faster in SQL?

I am using function to update to one column , like
DetailedStatus = dbo.fn_GetProcessStageWiseStatus(PR.ProcessID, PR.ProcessRunID, getdate())
Here 500,000 records are continuously UPDATED in this line. Its like like a loop
So using this function for few records its executing fast but when its 500,000 records executing it becomes very slow...
What can I do to make this execute faster using many records?
Any measures to be taken or any split to be used?
Function:
CREATE FUNCTION [dbo].[fn_GetProcessStageWiseStatus]
(
#ProcessID INT
,#ProcessRunID INT
,#SearchDate SMALLDATETIME
)
RETURNS VARCHAR(100)
AS
BEGIN
DECLARE
#iLoopCount SMALLINT
,#iRowCount SMALLINT
,#StepProgress VARCHAR(100)
,#StepCount SMALLINT
IF EXISTS(
SELECT TOP 1 1
FROM dbo.Step S WITH(NOLOCK)
JOIN dbo.vw_FileGroup FG
ON S.FileConfigGroupID = FG.FileConfigGroupID
WHERE S.ProcessID = #ProcessID
AND S.Active = 1
AND FG.FileConfigGroupActive = 1
AND FG.Direction = 'Inbound'
)
BEGIN
SET #StepProgress = 'Not Received'
END
ELSE
BEGIN
SET #StepProgress = 'Not Started'
END
DECLARE #StepRunDetailsTable TABLE
(
KeyNo INT IDENTITY(1,1)
,StepID INT
,StepStartTime SMALLDATETIME
,StepEndTime SMALLDATETIME
,SourceEnv VARCHAR(100)
,DestEnv VARCHAR(100)
)
INSERT INTO #StepRunDetailsTable
SELECT
S.StepID
,MAX(isnull(SR.StepStartTime, '06/06/2079'))
,MAX(isnull(SR.StepEndTime, '06/06/2079'))
,isnull(SENV.EnvironmentName, '')
,isnull(DENV.EnvironmentName, '')
FROM dbo.ProcessRun PR WITH(NOLOCK)
JOIN dbo.StepRun SR WITH(NOLOCK)
ON SR.ProcessRunID = PR.ProcessRunID
JOIN dbo.vw_StepHierarchy SH
ON SR.StepID = SH.StepID
AND SH.Active = 1
JOIN dbo.Step S WITH(NOLOCK)
ON SH.StepID = S.StepID
JOIN dbo.WorkFlow WF WITH(NOLOCK)
ON S.WorkFlowID = WF.WorkFlowID
AND WF.Active = 1
JOIN dbo.Environment SENV WITH(NOLOCK)
ON SENV.EnvironmentID = WF.SourceEnvironmentID
AND SENV.Active = 1
JOIN dbo.Environment DENV WITH(NOLOCK)
ON DENV.EnvironmentID = WF.DestinationEnvironmentID
AND DENV.Active = 1
WHERE PR.ProcessRunID = #ProcessRunID
GROUP BY S.StepID, SENV.EnvironmentName, DENV.EnvironmentName, SH.StepOrder
ORDER BY SH.StepOrder ASC
SELECT #StepCount = COUNT(*)
FROM dbo.ProcessRun PR WITH(NOLOCK)
JOIN dbo.Step S WITH(NOLOCK)
ON PR.ProcessID = S.ProcessID
AND PR.ProcessRunID = #ProcessRunID
AND S.Active = 1
SELECT #iRowCount = COUNT(DISTINCT StepID) FROM #StepRunDetailsTable
SET #iLoopCount = 0
WHILE (#iRowCount > #iLoopCount)
BEGIN
SET #iLoopCount = #iLoopCount + 1
SELECT
#StepProgress =
CASE
--WHEN #SearchDate BETWEEN StepStartTime AND StepEndTime
WHEN #SearchDate >= StepStartTime AND #SearchDate <= StepEndTime
THEN DestEnv + ' Load in Progress'
WHEN #SearchDate > StepEndTime AND #iLoopCount < #StepCount
THEN 'Waiting on next step - Loaded to ' + DestEnv
WHEN #SearchDate > StepEndTime AND #iLoopCount = #StepCount
THEN 'Completed'
WHEN #SearchDate < StepStartTime AND #iLoopCount = 1
THEN 'Load Not Started'
ELSE #StepProgress
END
FROM #StepRunDetailsTable
WHERE KeyNo = #iLoopCount
END
RETURN #StepProgress
END
Thanks in advance.
Seems like you have a change in execution plan when you try to update 500k rows.
You can try and set forceseek hint on the from clause to force using seeks instead of scans.
Also, WHILE (#iRowCount > #iLoopCount) should be replaced with if exists, because you basically check for certain conditions on the results table and you need to return as early as possible.
I see that you use nolock hint everywhere to allow dirty reads, you can set isolation level read uncommitted in the calling stored procedure and remove all of those; or consider to change the database to set read_committed_snapshot on to avoid locks.
By the way, scalar function calls in SQL Server are very expensive, so if you have some massive updates/selects happening in a loop where you call a function you have to avoid using functions as much as possible.

PostgreSQL conditional use of function parameters

I'd like to know how to use a function parameter conditionaly. This is my function, and you can read the comment inside the query:
CREATE OR REPLACE FUNCTION mediabase.select_media(sysEnvironment character varying, statusId integer)
RETURNS refcursor AS
$BODY$
DECLARE
ref refcursor;
BEGIN
OPEN ref FOR
SELECT media.id, media.title, media.unique_filename, media.owner_id, media.status_id, media.location_name_id, media.upload_user_id, media.upload_ip, media.metadata_id, media.type_id, media.description, media.system_environment, media.upload_date, media.gps_location, media.language_id, media_publications.publication_id, media.limitations, media_categories.category_id, metadata.width, metadata.height, metadata.equipment, metadata.copyright, metadata.creation_time, metadata.file_format, metadata.resolution, metadata.resolution_unit, metadata.gps_longitude, metadata.gps_latitude, metadata.artist, metadata.color_space, metadata.gps_altitude, metadata.software_used, metadata.user_comment
FROM mediabase.media, mediabase.metadata, mediabase.media_categories, mediabase.media_publications
WHERE media.metadata_id = metadata.id
AND media.id = media_categories.media_id
AND media.id = media_publications.media_id
-- Problem: this CASE doesn't work of course
CASE statusId <> -1 THEN
AND media.status_Id = statusId
END
-- End problem
AND media.system_environment = sysEnvironment
ORDER BY media.upload_date DESC;
RETURN ref;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
I only need to use the 'statusId' parameter, if it's different from -1, otherwise I'll recieve no results as there of-course is no status -1. Later on, I'll need to add some more filters of that sort.
Try something like:
AND (media.status_Id = statusId OR statusId = -1)
It wont check media.status_Id = statusId if statusId = -1.

Need to use a nested cursor and getting "invalid object name"

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!