T-SQL use different WHERE clause based on value of input parameter - sql-server

I would like to change the WHERE clause of a query based upon the value of an input parameter. I'm guessing this is possible, but it seems that I'm approaching it in the wrong way?
A simplified version on my SP query:
CREATE PROCEDURE [dbo].[GetMailboxMessagesByStatus]
#UserId UNIQUEIDENTIFIER,
#MessageStatus INT
AS
BEGIN
SELECT *
FROM MailboxMessages m
WHERE
CASE #MessageStatus
WHEN 4 THEN m.SenderId = #UserId --Sent
ELSE m.RecipientId = #UserId --Inbox
END
END
GO

No need for case or iif constructs:
WHERE #MessageStatus = 4 AND m.SenderId = #UserId
OR #MessageStatus <> 4 AND m.RecipientId = #UserId
EDIT:
Be aware on big tables using this construct when the table being queried is quite large. Using 2 seperate queries using a IF statement like Chester Lim suggested might be the better solution. Preventing parameter sniffing might also be a good idea

Use an IF statement.
CREATE PROCEDURE [dbo].[GetMailboxMessagesByStatus]
#UserId UNIQUEIDENTIFIER ,
#MessageStatus INT
AS
BEGIN
IF ( #MessageStatus = 4 )
BEGIN
SELECT *
FROM MailboxMessages
WHERE m.SenderId = #UserId; --Sent
END;
ELSE
BEGIN
SELECT *
FROM MailboxMessages m
WHERE m.RecipientId = #UserId; --Inbox
END;
END;
GO
EDIT - a much better way provided by LukStorms (since i did not know IIF until i saw his answer)
CREATE PROCEDURE [dbo].[GetMailboxMessagesByStatus]
#UserId UNIQUEIDENTIFIER ,
#MessageStatus INT
AS
BEGIN
SELECT *
FROM MailboxMessages m
WHERE IIF (#MessageStatus = 4, m.SenderId, m.RecipientId) = #UserId; --Sent
END
GO

You could change that WHERE clause to
WHERE (CASE WHEN #MessageStatus = 4 THEN m.SenderId ELSE m.RecipientId END) = #UserId
Because what you put after the THEN in a CASE should just be a value, not a comparison.
Or use IIF instead of a CASE:
WHERE IIF(#MessageStatus = 4,m.SenderId,m.RecipientId) = #UserId
But the SQL will run more efficient if you use an IF ... ELSE ... and run a different Query based on the #MessageStatus.
Was writing an example for that, but Chester Lim already beat me to it. ;)
(so no need to repeat that approach in this answer)

Related

SQL IF ELSE performance issue

I have a stored procedure that can get data from 2 different sources depending on if the user requests data from a single closed period (archived into a data warehouse table) or from an open period (data from transaction tables).
If I pass parameters that limit the select to the data warehouse table (providing a year and period for a closed period) the procedure takes a very long time to return results unless I comment out the ELSE BEGIN… code. No data is coming from the ELSE portion of code but it is still slowing down the procedure. If I comment out the ELSE portion of code, it is very fast.
I have tried OPTION (RECOMPILE) and I’m using local variables to avoid parameter sniffing but it’s not helping. Is there any way to get around this?
The following is an example of what I’m doing that runs slow:
IF #Year <> 0 AND #Period <> 0 AND (SELECT PerClosedTimestamp
FROM Period
WHERE
PerCompanyID = #CompanyID AND
PerYear = #Year AND
PerPeriod = #Period) IS NOT NULL
BEGIN
SELECT
datawhse.column1, datawhse.column2, etc …
FROM
datawhse
END
ELSE
BEGIN
SELECT
trantable.column1, trantable.column2, etc…
FROM
trantable
END
If I exclude the ELSE statement it runs very fast:
IF #Year <> 0
AND #Period <> 0
AND (SELECT PerClosedTimestamp
FROM Period
WHERE PerCompanyID = #CompanyID
AND PerYear = #Year
AND PerPeriod = #Period) IS NOT NULL
BEGIN
SELECT datawhse.column1
,datawhse.column2, etc …
FROM datawhse
END
Are #Year and #Period directly from the input of the stored procedure? like in your sproc definition, did you write in this following way?
create proc USP_name #Year int, #Period int as
begin
...
end
You can try using local variable, according to my experience in many cases like this, local variables help a lot.
create proc USP_name #Year int, #Period int as
begin
declare #Year_local int, #Period_local int
set #Year_local = #Year, #Period_local = #period
if #Year_local <> 0 AND #Period_local <> 0 AND ...
....
end
As mentioned in the comments, the definitive answer to why is it slow is always to be found in the query plan.
At a guess, the appearance of trantable in the procedure is biasing the query optimizer in a way that disfavors datawhse. I'd be tempted to at least try UNION ALL instead of IF/THEN, something along the lines of
SELECT
datawhse.column1, datawhse.column2, etc …
FROM
datawhse
WHERE #Year <> 0 AND #Period <> 0 AND (SELECT PerClosedTimestamp
FROM Period
WHERE
PerCompanyID = #CompanyID AND
PerYear = #Year AND
PerPeriod = #Period) IS NOT NULL
UNION ALL
SELECT
trantable.column1, trantable.column2, etc…
FROM
trantable
WHERE #Year = 0 OR #Period = 0 OR (SELECT PerClosedTimestamp
FROM Period
WHERE
PerCompanyID = #CompanyID AND
PerYear = #Year AND
PerPeriod = #Period) IS NULL
It would be interesting to see how the query plans compare.
Thanks everyone for your suggestions. I ended up creating 2 separate functions to return data from either the data warehouse table or the transaction tables. I select from the functions within the IF THEN ELSE statement and that seems to have solved my problem.

T-SQL different result between code in stored and same code in query pane

I'm working on a procedure that should return a o or a 1, depending on result from parameter calculation (parameters used to interrogate 2 tables in a database).
When I excute that code in a query pane, it gives me the results i'm expecting.
code looks like:
SELECT TOP 1 state, updDate INTO #history
FROM [xxx].[dbo].[ImportHystory] WHERE (db = 'EB') ORDER BY addDate DESC;
IF (SELECT state FROM #history) = 'O'
BEGIN
SELECT TOP 1 * INTO #process_status
FROM yyy.dbo.process_status WHERE KeyName = 'eb-importer';
IF(SELECT s.EndDate FROM #process_status s) IS NOT NULL
IF (SELECT s.EndDate FROM #process_status s) > (SELECT h.updDate FROM #history h)
BEGIN
IF (SELECT MessageLog from #process_status) IS NOT NULL SELECT 1;
ELSE SELECT 0;
END
ELSE
SELECT 1;
ELSE
SELECT 1;
END
ELSE
SELECT 0
I'm in the situation where EndDate from #process_status is null, so the execution returns 1.
Once i put the SAME code in a SP, and pass 'EB' and 'eb-importer' as parameters, it returns 0.
And I exec the procedure with the data from the table right in front of me, so i know for sure that result is wrong.
Inside the procedure:
ALTER PROCEDURE [dbo].[can_start_import] (#keyName varchar, #db varchar, #result bit output)
DECLARE #result bit;
and replace every
SELECT {0|1}
with
SELECT #result = {0|1}
Executed from the Query pane:
DECLARE #result bit;
EXEC [dbo].[can_start_import] #KeyName = 'eb-importer', #db = 'EB', #result = #result OUTPUT
SELECT #result AS N'#result'
Why does this happen?
You are doing a top(1) query without an order by. That means SQL Server can pick any row from table1 that matches the where clause.
If you want to guarantee that the result is the same every time you execute that code you need an order by statement that unambiguously orders the rows.
So, apparently 2 things needed to be done:
set the length of the varchar parameter with a higher length,
filter with ' like ' instead of ' = ' for god knows what reason
Now it work as i expected to do, but i still don't get the different results between the query pane and the procedure if i use the equal...

SQL - UPDATE within a SWITCH CASE

I'm trying to run an update based on the value of a flag sent into a procedure but it does not like the syntax of the UPDATE statement here. What's wrong with it?
CREATE PROC [dbo].[TestProc]
#ID int,
#GET_COUNT bit
AS
BEGIN
SELECT
CASE
WHEN #GET_COUNT = 1
THEN (SELECT COUNT(*) FROM [ORDERS]EMPLOYEE_ID = #ID)
WHEN #GET_COUNT = 0
THEN UPDATE ORDERS SET EMPLOYEE_ID = null WHERE EMPLOYEE_ID = #ID
END
GO
You are confusing a SELECT . . . CASE with IF. Your code looks like T-SQL, so this is probably what you intend:
IF #GET_COUNT = 1
BEGIN
SELECT COUNT(*) FROM [ORDERS] EMPLOYEE_ID = #ID;
END;
ELSE IF #GET_COUNT = 0
BEGIN
UPDATE ORDERS SET EMPLOYEE_ID = null WHERE EMPLOYEE_ID = #ID
END;
Some SQL scripting languages do use CASE for control-flow as well as expression evaluations. However, I think that IF is clearer in this context.

T-SQL Stored Procedure passing input, using output

Be easy on me... still a newbie T-SQL programmer :)
I have a Stored Procedure that is intended to take three input parameters, evaluate them and send back one parameter (#eligible). If I just execute a T-SQL script with variables hard coded in, my ##ROWCOUNT sets #eligible to 1. When I EXECUTE a call to it as a stored procedure, it does not return #eligible correctly. Here is the procedure:
ALTER PROCEDURE [dbo].[proc_Eligible]
(#control AS nvarchar(10),
#checkno AS nvarchar(10),
#event_type AS nvarchar(7),
#eligible AS bit OUTPUT)
AS
BEGIN
SET #eligible = 0
SELECT #control AS Control, #checkno AS CheckNum
-- Is the check drawn on an eligible bank?
SELECT
H.CONTROL,
H.NAME,
H.RECV_DATE,
H.CHECK_NUM,
H.BANK,
SUM(D.RECV_AMOUNT)
FROM
[ZZZ].[dbo].[MRRECVH] H INNER JOIN
[ZZZ].[dbo].[MRRECVD] D ON H.control = D.CONTROL
WHERE
BANK IN (SELECT
RIMAS_Code
FROM
[Custom].[dbo].[Bank_Account])
AND H.CONTROL = #control
AND H.CHECK_NUM = #checkno
GROUP BY
H.CONTROL,
H.BANK,
H.NAME,
H.CHECK_NUM,
H.RECV_DATE
HAVING
SUM(D.RECV_AMOUNT) > 0
IF ##ROWCOUNT > 0
SELECT #eligible = 1
END
(On the next to last line, I have tried 'SET #eligible = 1', but that didn't make any difference).
To call the procedure:
DECLARE
#eligible AS bit
EXECUTE proc_Eligible
#Control = '3034'
,#Checkno = '5011'
,#event_type = 'update'
,#eligible = #eligible
SELECT #eligible
As I mentioned, if I isolate the stored procedure SELECT statement and hard code the variables, it works great, so it's probably my newbie inexperience with passing data.
(#event_type will be used later when I adapt this into a table trigger)
Thank you,
Kevin
It seems that you have to specify OUTPUT keyword as well, when you call your stored proc:
EXECUTE proc_Eligible
#Control = '3034'
,#Checkno = '5011'
,#event_type = 'update'
,#eligible = #eligible OUTPUT -- <--- mark parameter as output

Could I write the following query without the while loop and improve efficiency?

Is there another way that I could do the following query
SELECT #MinID = MIN(ID), #MaxID = MAX(ID)
FROM #stp
WHILE (#MinID <= #MaxID AND IsNull(#MaxID, 0) > 0)
BEGIN
SELECT #result_mstr_id = result_mstr_id
FROM #stp
WHERE ID = #MinID
EXECUTE #err = f_rslm_publish #result_mstr_id
UPDATE dbo.results
SET result_stat_cd = CASE result_stat_cd
WHEN 'IP' THEN 'C'
WHEN 'IS' THEN 'S'
ELSE result_stat_cd
END
WHERE result_mstr_id = #result_mstr_id
SELECT #MinID = #MinID + 1
END
Its the typical loop being fed by a counter but could I re-write this to be one query?
You can't. If you need to execute the stored procedure f_rslm_publish for every ID in the table #stp, then you'll need to use a WHILE loop or a cursor.
Unless of course you can change the definition of the stored procedure f_rslm_publish, in which case it very well may be possible to rewrite it to accept a range of ID values (and possibly without also adding WHILE loops to the stored procedure code).
WHERE result_mstr_id between #MinId and #Maxid

Resources