Update statement, update value is conditionally evaluated - sql

I have the below pseudo code written that I want to implement in T-SQL. I need this code included in an existing stored procedure, I was trying to achieve the below with function call passing in a temp table as a parameter, is it possible to pass a temp table as a function parameter. Please let me know if there is a better approach to this.
Table: #Temp_Table has a column RefId which refers to #TempReadUpdateValue.Id. There are rules to identify if the #TempReadUpdateValue.Id can be applied to #Temp_Table.RefId.
Rule 1: the data qualifies in the DateRange
Rule 2: the #TempReadUpdateValue.Id is available if (Allowed - Used) > 0.
Allowed is fixed value and used will increment as its assigned.
I want to achieve the above with an UPDATE statement on Temp_Table, the challenge that I face is #Temp_Table.RefId = #TempReadUpdateValue.Id, need to increment
#TempReadUpdateValue.Used = #TempReadUpdateValue.Used + #Temp_Table.Units
every next row in #Temp_Table need to re-evaluate rules #1 and #2 for RefId assignment.
Update statement:
DECLARE #OLD INT = 0; -- THIS CAN ALSO BE SET TO 1, basically passed in as param to the stored procedure.
CREATE TABLE #TempReadUpdateValue
(
Id INT,
From_Date DateTime,
Thru_Date DateTime,
Allowed int,
Used int
)
CREATE TABLE #Temp_Table
(
Pk_ID INT,
DOS DateTime,
Units Int,
Ref_Id int
)
UPDATE #Temp_Table
SET Ref_Id = CASE
WHEN #OLD = 0 THEN 121
ELSE NewImplementation(DOS, Units, #TempReadUpdateValue)
END
CREATE OR ALTER FUNCTION NewImplementation
(#DOS DATETIME, #Units INT, #TempReadUpdateValue)
RETURNS INT
AS
BEGIN
DECLARE #Id INT
DECLARE #Allowed INT
DECLARE #Used INT
SELECT
#Id = Id,
#Allowed = Allowed,
#Used = Used
FROM
#TempReadUpdateValue
DECLARE #ReturnValue INT = 0
IF (#Id > 0) AND (#Allowed - #Used ) > 0
BEGIN
#ReturnValue = #Id;
UPDATE #TempReadUpdateValue
SET Used = (Used + #Units);
END
RETURN #ReturnValue
END

Related

Alternative to While Loop In SQL Server stored procedure

I have a stored procedure where a populate a base table with key fields and then loop through that table to get those key fields to requery a source data table to get detailed counts. The issue I am having is that when there are a lot of rows in the base table, the SP takes a long time to run. I've loaded the source data into temp tables and created index's and made the base table a temp table with an index as well.
CREATE TABLE #SupplementalData1
(
ROWID int IDENTITY(1, 1),
LOB varchar(100),
Program varchar(100),
Project varchar(100),
Container varchar(255),
RPTNG_Week date,
Scheduled_Open int,
Still_Open int,
Scheduled_Closed int,
Actual_Closed int
);
CREATE INDEX t1
ON #SupplementalData1 (LOB, Program, Project, Container, RPTNG_Week);
INSERT INTO #SupplementalData1 (LOB, Program, Project, Container, RPTNG_Week)
SELECT DISTINCT
a.LOB_CODE,
a.PRGRM_NAME,
a.PRJCT_NAME,
a.CNTNR_NAME,
b.Monday
FROM
#data a,
Schedule_Date_Lookup b
WHERE
b.Monday >= #MinMonday
AND b.Monday <= #MaxMonday
ORDER BY
a.LOB_CODE,
a.PRGRM_NAME,
a.PRJCT_NAME,
b.Monday;
DELETE FROM #SupplementalData1
WHERE RPTNG_Week > #EndDate;
-- Get the number of rows in the looping table
DECLARE #RowCount int;
SET #RowCount = (SELECT COUNT(ROWID)FROM #SupplementalData1);
-- Declare an iterator
DECLARE #I int;
-- Initialize the iterator
SET #I = 1;
--Declare Common Variables
DECLARE #iLOB varchar(MAX),
#iProgram varchar(MAX),
#iProject varchar(MAX),
#iContainer varchar(MAX),
#iRPTNG_Week date,
#Value int;
-- Loop through the rows of a table #myTable
WHILE (#I <= #RowCount)
BEGIN
-- Declare variables to hold the data which we get after looping each record
-- Get the data from table and set to variables
SELECT #iLOB = LOB,
#iProgram = Program,
#iProject = Project,
#iContainer = Container,
#iRPTNG_Week = RPTNG_Week
FROM #SupplementalData1
WHERE ROWID = #I;
SET #Value = (SELECT COUNT(CNTNR_NAME) AS Scheduled_Open_Sum
FROM #data c
WHERE (c.NEED_DATE >= #iRPTNG_Week)
AND c.LOB_CODE = #iLOB
AND c.PRGRM_NAME = #iProgram
AND c.PRJCT_NAME = #iProject
AND c.CNTNR_NAME = #iContainer);
UPDATE #SupplementalData1
SET Scheduled_Open = #Value
WHERE LOB = #iLOB
AND Program = #iProgram
AND Project = #iProject
AND Container = #iContainer
AND RPTNG_Week = #iRPTNG_Week;
-- -- Increment the iterator
SET #I = #I + 1;
END;
Is there an alternative way that would improve speed?
Without sample data, desired output and your logic the following wasn't tested, but should get you moving in the right direction.
Do away with the entire while statement and go with a set based approach.
Here is the while loop rewritten as a SELECT. I will usually do that first to double check and validate data.
SELECT *
FROM [#SupplementalData1] [supdata]
CROSS APPLY (
SELECT COUNT([CNTNR_NAME]) AS [Scheduled_Open_Sum]
FROM [#data] [c]
WHERE [c].[NEED_DATE] >= [supdata].[RPTNG_Week]
AND [c].[LOB_CODE] = [supdata].[LOB]
AND [c].[PRGRM_NAME] = [supdata].[Program]
AND [c].[PRJCT_NAME] = [supdata].[Project]
AND [c].[CNTNR_NAME] = [supdata].[Container]
) AS [cd];
Then once you have validated that is correct you can easily rewrite that has an update. Which would be what replaces your while loop.
UPDATE [supdata]
SET [Scheduled_Open] = [cd].[Scheduled_Open_Sum]
FROM [#SupplementalData1] [supdata]
CROSS APPLY (
SELECT COUNT([CNTNR_NAME]) AS [Scheduled_Open_Sum]
FROM [#data] [c]
WHERE [c].[NEED_DATE] >= [supdata].[RPTNG_Week]
AND [c].[LOB_CODE] = [supdata].[LOB]
AND [c].[PRGRM_NAME] = [supdata].[Program]
AND [c].[PRJCT_NAME] = [supdata].[Project]
AND [c].[CNTNR_NAME] = [supdata].[Container]
) AS [cd];

SQL Function is INCREDIBLY slow

I have a SQL function that is used to return a single value in another view, this function takes well over 30 seconds sometimes on larger databases, I think it might be running over and over?
Honestly I'm just losing my mind at this point and need the help. Does anyone know the best way to optimize this?
The T-SQL function looks like this:
IF OBJECT_ID('Base.fn_AssetPriority') IS NOT NULL
DROP FUNCTION Base.fn_AssetPriority
GO
CREATE FUNCTION Base.fn_AssetPriority
(#LID BIGINT)
RETURNS NVARCHAR(20)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #Priority NVARCHAR(20)
DECLARE #RGID BIGINT
DECLARE #CP TABLE
(
ConsequenceAssessmentID BIGINT,
[Sign] VARCHAR(2),
Score DECIMAL(18,2),
AssetPriority CHAR(1),
ConsNo INT
)
SET #Priority = 'Not Allocated'
INSERT INTO #CP
SELECT
ConsequenceAssessmentID, [Sign], Score, AssetPriority, ConsNo
FROM
Base.ConsequencePriority
ORDER BY
ConsNo DESC
SELECT #RGID = MAX(ID)
FROM ACA.ReviewGroup
WHILE EXISTS (SELECT * FROM #CP)
BEGIN
DECLARE #CAID BIGINT
DECLARE #ConsNo INT
DECLARE #Sign VARCHAR(2)
DECLARE #Score DECIMAL(18,2)
DECLARE #AP CHAR(1)
SELECT TOP 1
#CAID = ConsequenceAssessmentID,
#ConsNo = ConsNo,
#Sign = [Sign],
#Score = Score,
#AP = AssetPriority
FROM
#CP
ORDER BY
ConsNo DESC
IF #Sign = '='
BEGIN
IF EXISTS (SELECT * FROM ACA.ConsequenceAssessment
WHERE LID = #LID AND RGID = #RGID
AND BaseCAID = #CAID AND Score = #Score)
BEGIN
SET #Priority = #AP
BREAK
END
END
ELSE BEGIN
IF EXISTS (SELECT * FROM ACA.ConsequenceAssessment
WHERE LID = #LID AND RGID = #RGID
AND BaseCAID = #CAID AND Score >= #Score)
BEGIN
SET #Priority = #AP
BREAK
END
END
DELETE FROM #CP
WHERE ConsequenceAssessmentID = #CAID
AND ConsNo = #ConsNo
END
RETURN #Priority
END
There is another view that calls this as a field:
Base.fn_AssetPriority(BaseAS.ID) AS AssetPriority,
How on Earth do I optimize this? or get it to run a bit quicker?
It's possible the execution plan for your stored function is stale.
Try doing this and rerunning it.
EXEC sp_recompile N'Base.fn_AssetPriority';
If it gets faster you may want to run that recompile every so often. Maybe use a job to recompile it every day.
You probably don't want to put WITH RECOMPILE in the function's definition, because you use it a lot and the reason for recompilation is changing statistics in the tables it queries.

Is it safe to save previous value of a column as a variable in update?

Think of a simple update stored procedure like this:
CREATE PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
Now, to return a result based on previous value of ModifiedOn, I changed it like this:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT #ModifiedOn
ELSE
SELECT -1
Is it safe to fill #PreviousModifiedOn variable, with previous value of ModifiedOn, in SET part? Or is it possible that ModifiedOn value changes before it is saved into variable?
UPDATE
Same query using OUTPUT:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn AS TABLE (ModifiedOn datetime)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
OUTPUT
Deleted.[ModifiedOn] INTO #PreviousModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF EXISTS (SELECT * FROM #PreviousModifiedOn WHERE [ModifiedOn] <= #GeneratedOn)
SELECT #ModifiedOn
ELSE
SELECT -1
It seems that OUTPUT is the correct way to solve the problem, but because of the variable table, I think it has more performance cost.
So my question is... Why using OUTPUT is better than my solution? Is there anything wrong with my solution? Which one is better in terms of performance and speed?
I believe that this is safe. Although variable assignment is a proprietary extension, the rest of the SET clause follows the SQL Standard here - all assignments are computed as if they occur in parallel. That is, all expressions on the right of assignments are computed based on pre-update values for all columns.
This is e.g. why UPDATE Table SET A=B, B=A will swap the contents of two columns, not set them both equal to whatever B was previously.
The one thing to be wary of here, for me, would be that the UPDATE may have performed no assignments (due to the WHERE clause) and so still be NULL, or may have performed multiple assignments; In the latter case, your variable will be set to one of the previous values but it is not guaranteed which row's value it will have retained.
It is not required, since MS SQL Server 2005 you can use OUTPUT for this kind of scenarios.
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
--Declare a table variable for storing the info from Output
DECLARE #ModifiedOnTable AS TABLE
(
ModifiedOn DATETIME
)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
OUTPUT DELETED.ModifiedOn INTO #ModifiedOnTable
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT ModifiedOn FROM #ModifiedOnTable
ELSE SELECT -1

Error Number:141,State:1,Class:15 A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations

I am trying to write a function to get count of appointments with matching doctorId, here is function :
CREATE FUNCTION dbo.GetSumOfAppointments (#doctorId int)
RETURNS int
AS
BEGIN
DECLARE #count int;
SET #count = (SELECT
COUNT(*)
FROM dbo.Appointments
WHERE DoctorId = #doctorId);
RETURN ISNULL(#count, 0)
END
but its giving me error :
Error Number:141,State:1,Class:15 A SELECT statement that assigns a
value to a variable must not be combined with data-retrieval
operations.
what exactly is the problem with above?
CREATE FUNCTION dbo.GetSumOfAppointments (#doctorId int)
RETURNS int
AS
BEGIN
DECLARE #count int;
select #count = COUNT(*)
FROM dbo.Appointments
WHERE DoctorId = #doctorId;
RETURN ISNULL(#count, 0)
END
No need to declare a variable and applying Isnull function since count(*) always returns number of records in that table suppose if the table is empty then you would get zero. Try like this make it simple.
CREATE FUNCTION dbo.GetSumOfAppointments (#doctorId INT)
RETURNS INT
AS
BEGIN
RETURN (
SELECT COUNT(*)
FROM dbo.Appointments
WHERE DoctorId = #doctorId
);
END

SQL - Getting Updated Value

I use Sql Server 2008.
I have a table that generates ID.
I want to retrieve the generated ID and store it in a bigint variable.
How can I do it?
Here is the Stored Proc that gives the ID as result set. But I cannot store it in a bigint variable.
ALTER PROC SCN.TRANSACTION_UNIQUE_ID_SELECT
AS
UPDATE COR.TRANSACTION_UNIQUE_ID
SET ID = ID + 1
OUTPUT INSERTED.ID AS ID
If you want to use output you can;
declare #ID table (ID bigint)
update the_table
set ID = ID + 1
output INSERTED.ID into #ID
declare #bi bigint = (select ID from #ID)
The BigInt should be an identity insert column, this will make SQL Server automatically generate bigints in sequence for you. Just pass the rowID as an OUTPUT parameter and set it before the procedure ends after the insert/update.
Then you can read it coming back and set it as needed.
The stored procedure could look something like this (I've only included the rowID for clarity):
CREATE PROCEDURE [Sample].[Save]
(
#rowID bigint OUTPUT
)
AS
BEGIN
SET NOCOUNT ON
--Do your insert/update here
--Set the RowID
SET #rowID = (SELECT SCOPE_IDENTITY())
END
That UPDATE looks fishy...it will increment every ID in your table by one, no?
Anyway, variables have an #sign, so just SET #myvar = ...whatever...
Use OUTPUT INTO:
DECLARE #TblID TABLE ( ID int )
UPDATE COR.TRANSACTION_UNIQUE_ID
SET ID = ID + 1
OUTPUT INSERTED.ID INTO #TblID (ID) --the output values will be inserted
--into #TblID table-variable
DECLARE #id BIGINT
EXEC #id = SCN.TRANSACTION_UNIQUE_ID_SELECT