Returning different values based on selected data - sql

I have a stored procedure that selects a row based on an id (simple enough), but only returns the actual result if the data satisfies a few conditions, otherwise it returns specific error codes. So when doing nested checks, the code would look similiar to this:
CREATE PROCEDURE GetStuffById
#StuffId int
AS
BEGIN
IF EXISTS (SELECT TOP 1 * FROM [Stuff] WHERE StuffId = #StuffId)
BEGIN
DECLARE #IsValid bit
SET #IsValid = (SELECT IsValid FROM [Stuff] WHERE StuffId = #StuffId)
IF #IsValid = 1
BEGIN
--More nested checks may occur here
SELECT * FROM [Stuff] WHERE StuffId = #StuffId
END
ELSE
BEGIN
RETURN -2
END
END
ELSE
BEGIN
RETURN -1
END
END
In this approach I already have 3 selects on the same table, which seems redundant and inefficient and another check would mean another select etc. Is there a better pattern to do this (e.g. temp tables)?
UPDATE: edited first check

You can assign to multiple variables in a single select and use ##ROWCOUNT to detect whether a row was found.
DECLARE #IsValid BIT,
#Foo INT
SELECT #IsValid = IsValid,
#Foo = Foo
FROM [Stuff]
WHERE StuffId = #StuffId
/*This must be tested immediately after the assignment statement*/
IF ##ROWCOUNT = 0
RETURN -1
IF ISNULL(#IsValid, 0) = 0
RETURN -2
SELECT #IsValid AS IsValid,
#Foo AS Foo

Related

SQL Server: why variable works outside IF

I have trigger where on top of it I declare
DECALRE #variable VARCHAR(100) = ' '
And then below it, I have this sample IF statement
IF UPDATE ([data])
BEGIN
SET #variable = 'Data change'
END
And then insert with this #variable on the end of the trigger.
My problem is that even though [data] remains unchanged it inserts 'Data change' into DB instead of ''
Documentation of UPDATE - Trigger Functions states clearly:
UPDATE() returns TRUE regardless of whether an INSERT or UPDATE attempt is successful.
Please check:
UPDATE t SET id = 2 WHERE id = 1; /*if block is not executed */
-- 1 rows affected
and(even though there is no real change on data column):
UPDATE t SET id = 1, data = data WHERE id = 2; /*if block is executed */
-- Data change
-- 1 rows affected
db<>fiddle demo
As the others suggested, Update() is always true
So you may try a solution like the following
DECLARE #variable VARCHAR(100) = 'initial data'
if (Select Count(*)
From [<TblToBeUpdated>] t
INNER JOIN Inserted i ON t.[<PK>] = i.[<PK>]
INNER JOIN Inserted d ON t.[<PK>] = d.[<PK>]
WHERE t.[<ColToBeUpdated>] <> i.[<ColOfQuery>] OR
t.[<ColToBeUpdated>] <> d.[<ColOfQuery>]
) > 0
Begin
SET #variable = 'Data change'
End
see the fiddle

Temp table has only one row inserted

Hi I have an SP in which i create a temporary table to store some values.
ALTER PROCEDURE [dbo].[test]
#id int,
#funds_limit money
AS
BEGIN
SET NOCOUNT ON;
DECLARE #threshold money;
CREATE TABLE #ConfigurationTemp
(id int,
name varchar(100) not null,
type varchar(100),
value varchar(100))
INSERT #ConfigurationTemp EXEC get_config #id, 'testType', null
select #threshold = value
from #ConfigurationTemp
where id=#id and name='testLimit'
print #threshold
IF (#funds_limit IS NOT NULL) AND (#threshold < #funds_limit)
BEGIN
DROP TABLE #ConfigurationTemp;
RETURN 1000;
END
select #threshold = value
from #ConfigurationTemp
where id=#id and name='testLimit1'
print #threshold
IF (#funds_limit IS NOT NULL) AND (#threshold < #funds_limit)
BEGIN
DROP TABLE #ConfigurationTemp;
RETURN 1001;
END
END
RETURN 0;
END
The temporary table have multiple rows.
eg:
1, fund_limit, testType, 10
2, fund_min_limit, testType, 20
I need to first validate the value for fund_limit (10) with the user input value (which will be an input parameter to the SP). If the validation fails, i return with an error code. If not, I go for the next check. i.e., fund_min_limit. I do the same with it and return a different error code. If no validation fails, i will return 0 which is considered to be a success.
In this case, I am getting same value for threshold always. i.e., the value of first row... 10.
How can I get the different threshold value from the temp table with respect to the name?
When you assign scalar variable with select - it may be not assigned (unchanged - may keep value from previous assignment) if this select returned zero rows. To ensure your variable changed it's value rewrite it as set expression.
So if you misspelled second threshold name you may be "getting" same #threshold value because second statement does not assign anything to your variable i.e. variable contains value from prior assignment (select). You may test it with additional variable for second threshold - it will be always NULL if i guessed the issue reason.
Also you are applying same #id filter which is a scalar variable. But your rows have different ids. So there is no chances right now to get any other threshold's value than for #id given.
set #threshold = (select t.value
from #ConfigurationTemp t
where t.name='testLimit')
print #threshold
IF #threshold < #funds_limit
RETURN 1000;
set #threshold = (select t.value
from #ConfigurationTemp t
where t.name='testLimit 2')
print #threshold
IF #threshold < #funds_limit
RETURN 1001;
If will succeed only when both arguments are NOT NULL.
One more approach:
declare
#threshold_a int,
#threshold_b int,
#threshold_c int
;with test as
(
select 'a' as name, 25 as value
union all
select 'b', 3
union all
select 'c', 100
union all
select 'd', -1
)
select
#threshold_a = case when t.name = 'a' then t.value else #threshold_a end,
#threshold_b = case when t.name = 'b' then t.value else #threshold_b end,
#threshold_c = case when t.name = 'c' then t.value else #threshold_c end
from test t
select
#threshold_a as [a],
#threshold_b as [b],
#threshold_c as [c]
GO
single select, several variables.
You have RETURN in your IF statment.
... RETURN 1000
and
... RETURN 1001
After insert a row the procedure end.
Maybe you want to assign a result to a variable
#return_Value = ''
#return_Value = #return_Value + '1000, '
....
#return_Value = #return_Value + '1001, '
RETURN #return_Value

Trying to query and then update a table in one transaction

Spec for the stored procedure is:
To select and return the Id from my table tb_r12028dxi_SandpitConsoleProofClient (order is not important just the top 1 found will do) and as soon as I've selected that record it needs to be marked 'P' so that it does not get selected again.
Here is the stored procedure:
ALTER PROCEDURE [dbo].[r12028dxi_SandpitConsoleProofSweep]
#myId INT OUTPUT
AS
/*
DECLARE #X INT
EXECUTE [xxx].[dbo].[r12028dxi_SandpitConsoleProofSweep] #X OUTPUT
SELECT #X
*/
DECLARE #NumQueue INT = (
SELECT [cnt] = COUNT(*)
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient
WHERE [Status] IS NULL
);
IF #NumQueue > 0
BEGIN
BEGIN TRANSACTION;
DECLARE #foundID INT = (SELECT TOP 1 Id FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient WHERE [Status] IS NULL);
UPDATE x
SET x.[Status] = 'P'
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient x
WHERE x.Id = #foundID
SET #myId = #foundID;
RETURN;
COMMIT TRANSACTION;
END;
GO
It is returning the error message:
Transaction count after EXECUTE indicates a mismatching number of
BEGIN and COMMIT statements. Previous count = 0, current count = 1.
I've just added the Update script and the BEGIN TRANSACTION; and COMMIT TRANSACTION; before that it worked fine when it looked like the following...
ALTER PROCEDURE [dbo].[r12028dxi_SandpitConsoleProofSweep]
#myId INT OUTPUT
AS
/*
DECLARE #X INT
EXECUTE [xxx].[dbo].[r12028dxi_SandpitConsoleProofSweep] #X OUTPUT
SELECT #X
*/
DECLARE #NumQueue INT = (
SELECT [cnt] = COUNT(*)
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient
WHERE [Status] IS NULL
);
IF #NumQueue > 0
BEGIN
SELECT TOP 1 #myId = Id FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient;
RETURN;
END;
GO
I added the BEGIN TRANSACTION; / COMMIT TRANSACTION; because I wanted to ensure that the data gets read into the output variable AND that the UPDATE happens. Should I just leave out this section of the procedure?
You have "RETURN;" before "COMMIT TRANSACTION;" which means "COMMIT TRANSACTION;" is never executed.
Give that you want:
and as soon as I've selected that record it needs to be marked 'P' so
that it does not get selected again.
you can achieve that in a single statment (and not in a transaction)
ALTER PROCEDURE [dbo].[r12028dxi_SandpitConsoleProofSweep]
#myId INT OUTPUT
AS
BEGIN
UPDATE x
SET x.[Status] = 'P',
#myID = x.ID
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient x
/* a sample join to get your single row in an update statement */
WHERE x.ID = (SELECT MIN(ID)
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient sub
WHERE ISNULL(sub.[Status], '') != 'P')
END
Note: When dealing with concurrent processing (ie: two threads trying to select from a single queue) it's more about the locking behavior than doing it inside a single transaction.
As an alternative to a perfectly reasonable suggestion by #Andrew Bickerton, you could also use a CTE and the ROW_NUMBER() function, like this:
WITH ranked AS (
SELECT
Id,
[Status],
rnk = ROW_NUMBER() OVER (ORDER BY Id)
FROM xxx.DBO.tb_r12028dxi_SandpitConsoleProofClient
WHERE [Status] IS NULL
)
UPDATE ranked
SET
[Status] = 'P',
#myId = Id
WHERE rnk = 1
;
The ROW_NUMBER() function assigns rankings to all rows where [Status] IS NULL, which allows you to update only a specific one.
The use of the CTE as the direct target of the UPDATE statement is absolutely legitimate in this case, as the CTE only pulls rows from one table. (This is similar to the use of views in UPDATE statements.)

ISNULL function in SQL Server 2008 not working properly

Assume this script:
DECLARE #result TABLE(Id BIGINT);
DELETE FROM [Products].[Product]
OUTPUT DELETED.[Id] INTO #result
WHERE [Products].[Product].[Id] = 1589;
So in continues I try :
1
SELECT CAST(ISNULL([Id], -1) AS BIGINT) AS N'RetValId' FROM #result;
When [Id] is null returned null (nothing), but this one returned -1:
2
DECLARE #mi BIGINT;
SET #mi = (SELECT [Id] FROM #result)
SELECT CAST(ISNULL(#mi, -1) AS BIGINT) AS N'RetValId'
Why? where is the problem with first script?
Update
So is there any way to check if the Deleted Id is null returned -1 And if not Returned Id without declare another variable? what is the simplest way?
If you have no entry for the ID 1589, then in the DELETED table there will be no record, if you have it then it should return 1589.
So if you don't have I think it simple returns nothing, because this statement has no input row:
SELECT CAST(ISNULL([Id], -1) AS BIGINT) AS N'RetValId' FROM #result;
(If you SELECT * from #result it should be no rows there)
The second one return the -1 because you set first to the variable which is getting the NULL value after the select.
DECLARE #mi BIGINT;
SET #mi = (SELECT [Id] FROM #result)
(If you select only #mi after this, then it should be NULL)
I think that is the explanation
UPDATED:
May you can try a small trick to achive it without an other varriable:
SELECT CAST(ISNULL(MAX([ID]),-1) AS BIGINT) AS N'RetValId' FROM #result;
Because of MAX the insie statement will be NULL, so here is the trick. If something was deleted, then the ID will be there.
I hope it helped.
You can use a derived table that will return one row with -1 and then do an outer apply on #result.
select isnull(R.Id, T.Id) RetValId
from (values(-1)) as T(Id)
outer apply #result as R
An easy way to return null if no rows where deleted is the ##rowcount variable. It contains the number of rows affected by the previous operation:
DELETE FROM [Products].[Product]
WHERE [Products].[Product].[Id] = 1589;
IF ##ROWCOUNT = 0
return null
return 1589

Update multiple rows in table from table variable

I'm writing a stored procedure to update multiple records based on a table variable parameter.
The existing table is: Tb_Project_Image with relevant columns:
id PK (identity 1,1)
cat_ord decimal(4,2)
The procedure will receive a temporary table variable (shown in the code below) containing the id as PI_ID, and the new value for cat_ord as newCatOrd. idx is a simple identity for each row containing 1...n where n is the rowcount of #tempTable.
For each row in #tempTable, I want to update Tb_Project_Image where id = PI_ID to the corresponding value.
DECLARE #tempTable table (
idx smallint Primary Key IDENTITY(1,1),
PI_ID bigint,
newCatOrd decimal(4, 2) not null )
INSERT INTO #tempTable values (3, 7.01)
INSERT INTO #tempTable values (4, 7.02)
INSERT INTO #tempTable values (5, 7.03)
--etc...
DECLARE #error int
DECLARE #update int
DECLARE #iter int
SET #iter = 1
BEGIN TRAN
WHILE #iter <= (select COUNT(*) from #tempTable)
BEGIN
UPDATE Tb_Project_Image
SET cat_ord = (SELECT newCatOrd FROM #tempTable
WHERE idx = #iter)
WHERE id = (SELECT PI_ID FROM #tempTable
WHERE idx = #iter)
--error checking
set #error = ##ERROR
set #update = ##ROWCOUNT
IF ((#error = 0) AND (#update = 1))
BEGIN
SET #iter = #iter + 1
CONTINUE
END
ELSE
BREAK
END
IF ((#error = 0) AND (#update = 1))
COMMIT TRAN
ELSE
ROLLBACK TRAN
GO
Now, the error checking is because, to ensure integrity, EACH row in the temporary table MUST make 1 update. (explanation omitted to save space) If a single iteration of the while loop threw an error, or didn't effect exactly 1 row, I want to break the loop and rollback the transaction
THE PROBLEM I'm having is that this error checking is not working. I'm currently running it with 14 rows in #tempTable and the 11th uses a PI_ID not found in the Project_Image table. Therefore, #update = 0... but it continues the loop and commits the data.
I'd be doubly glad if someone had a method of doing this that only used a single update statement.
You cannot do it this way, because even SET resets the state of ##ERROR and ##ROWNUMBER variables. In this case ##ROWCOUNT is set to 1 after set #error = ##ERROR. If you do not assign the values to local variables, your code will work:
IF ((##error = 0) AND (##rowcount = 1))
But you might rather try try...catch error handling and test ##rowcount separately after update.
UPDATE: doing it in single update:
UPDATE t
SET cat_ord = tt.newCatOrd
FROM Tb_Project_Image t
INNER JOIN #tempTable tt
ON t.id = tt.PI_ID
-- If there was PI_ID not found in Tb_Project_Image
-- But I think that this should have been dealt with
-- During the initial loading of temporary table
IF ##ROWCOUNT <> (select count (*) from #tempTable)
BEGIN
-- Error reporting here
ROLLBACK TRANSACTION
END
Instead of updating and then rolling back, you could also use a CTE to determine if any records should be updated prior to performing the update. Something like this should work:
WITH NON_SINGLETON AS (
-- Find any records in #tempTable that don't match
-- exactly one record in Tb_Project_Image
SELECT t.PI_ID, COUNT(pi.id) C
FROM #tempTable t
LEFT JOIN Tb_Project_Image pi ON t.PI_ID = pi.id
GROUP BY t.PI_ID
HAVING COUNT(pi.id) != 1
)
UPDATE Tb_Project_Image
SET cat_ord = t.newCatOrd
FROM Tb_Project_Image pi
JOIN #tempTable t ON pi.id = t.PI_ID
-- If any invalid records were found in the CTE,
-- then this condition will fail for all rows
-- and nothing will be updated
WHERE NOT EXISTS(SELECT 1 FROM NON_SINGLETON)
If it's possible for #tempTable to have duplicate entries for the same PI_ID, then this will handle those scenarios as well. And since it's a single statement, you don't have to explicitly managing the transaction in the proc (if it's the only thing that needs to be included in the transaction).