Using different set of WHERE clauses in stored procedure depending on Parameter value - sql

I have 2 stored procedures which return the same columns that I am trying to merge into a single procedure. They both have a different set of parameters and both have different WHERE clauses, but they use the same tables and select the exact same rows.
WHERE clause 1: (uses #UIOID, and #Level)
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
Where clause 2: (Uses #TeamCode, #Year, #IncludeQCodes)
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )
Ideally I want to add a new parameter which basically tells it which of the two WHERE clauses to run, but I can't seem to recreate that with CASE statement because as far as I can tell, they only work for a single WHERE clause, not a whole set of different clauses
I want to do this without having to repeat the select statement again and putting the whole thing in IF statements, and i don't want to put the query into a string either. I just want one select statement ideally.
The problem with using temp tables is the query itself takes a while to run without any parameters and is used in a live website, so I don't want it to have to put all records in a temp table and then filter it.
The problem with using a CTE is you can't follow it with an IF statement, so that wouldn't work either.
Here is the sort of logic I am trying to achieve:
SELECT A
B
C
FROM X
IF #WhichOption = 1 THEN
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
ELSE IF #WhichOption = 2 THEN
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )

Save the following process in a procedure. You can also directly insert into a physical table.
declare #varTable Table (columns exactly as Procedures return)
if(condition is met)
begin
insert into #varTable
exec proc1
end
else
begin
insert into #varTable
exec proc2
end

Add the parameter that you said that it would indicate what filter apply :
select XXXXX
from XXXXX
where (#Mode = 1 and ( filter 1 ))
or
(#Mode = 2 and ( filter 2 ))
option(recompile)
If the #Mode parameter is 1 then it will evaluate the filter 1, otherwise it will evaluate the filter 2.
Add an option(recompile) at the end of the statement, so the SQL engine will replace the variables with their values, eliminate the filter that won't be evaluated, and generate an execution plant for just the filter that you want to apply.
PS: Please notice that although these catchall queries are very easy to code and maintain, and generate a perfectly functional and optimal execution, they are not advised for high-demand applications. The option(recompile) forces the engine to recompile and generate a new execution plan at every execution and that would have a noticeable effect on performance if your query needs to be executed hundreds of times per minute. But for the occasional use it's perfectly fine.

Try to use dynamic SQL:
DECLARE #sql NVARCHAR(max), #where NVARCHAR(max), #WhichOption INT = 1;
SET #sql = 'SELECT A
B
C
FROM X';
IF #WhichOption = 1
SET #where = 'WHERE ( #UIOID = CASE WHEN #Level = ''Single'' THEN C.C_UIOID_PK
WHEN #Level = ''Children'' THEN CLC.UIOL_P
WHEN #Level = ''Parent'' THEN CLP.UIOL_C
END
OR ( #UIOID = ''0''
AND #Level = ''All''
)
)';
ELSE IF #WhichOption = 2
SET #where = ' WHERE C.C_IsChild = 0
AND C.C_MOA <> ''ADD''
AND #TeamCode = C.C_OffOrg
AND C.C_Active = ''Y''
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate)
AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE ''Q%''
OR #IncludeQCodes = 1 ) ';
SET #sql = CONCAT(#sql,' ', #where)
PRINT #sql
EXECUTE sp_executesql #sql

Related

SQL Indexes to be used in WHERE statement

I have a table with 3 fields that I create a composite index. I need it to check if the record exists.
Here is my table
tblGamePlayLog
UserId (PK)
LogId (PK)
...
...
ProviderId
ResellerId
GameId
...
...
-- ProviderId, ResellerId and GameId is indexed (composite index)
And I have stored procedure like this
CREATE PROCEDURE [AS.uspProviderResellerGame_IsDeletable]
(
#ProviderId INT = -1, --Use -1 to ignore this field
#ResellerId INT = -1, --Use -1 to ignore this field
#GameId INT = 1, --Use -1 to ignore this field
#IsDeletable BIT = 0 OUT
)
AS
BEGIN
SET #IsDeletable = 1;
...
...
...
ELSE IF (EXISTS(SELECT TOP 1 1 FROM [tblGamePlayLog] WHERE ((#ProviderId = -1) OR ([ProviderId] = #ProviderId)) AND ((#ResellerId = -1) OR ([ResellerId] = #ResellerId)) AND ((#GameId = -1) OR ([GameId] = #GameId)))) SET #IsDeletable = 0;
END;
This stored procedure allowed the calling function to pass -1 to ignore the check on a particular field. However, it caused a significant slowness on the query (1000x slower as the log consists of 1 million records).
If I remove -1 check, the speed improve significantly.
...
...
...
ELSE IF (EXISTS(SELECT TOP 1 1 FROM [tblGamePlayLog] WHERE ([ProviderId] = #ProviderId) AND ([ResellerId] = #ResellerId) AND ([GameId] = #GameId))) SET #IsDeletable = 0;
I suspect, but adding -1 check, the SQL doesn't use index check. My question is, how to allow -1 check in WHERE clause but preserve the index check.
I would recommend option(recompile), so the database evaluates the literal parameters for every runs and computes a proper execution plan accordingly. The overhead of the recompilation should be less than the price you are paying now for a suboptimal plan.
IF(
EXISTS(
SELECT 1
FROM [tblGamePlayLog]
WHERE
(#ProviderId = -1 OR [ProviderId] = #ProviderId)
AND (#ResellerId = -1 OR [ResellerId] = #ResellerId)
AND (#GameId = -1 OR [GameId] = #GameId)
)
OPTION (RECOMPILE)
)
Then, we need to look at the indexing part of the question. If you want the query to run efficiently in all situations, you need several indexes, basically all possible combinations of search parameters:
(providerid)
(resellerid)
(gameid)
(providerid, resellerid)
(providerid, gameid)
(gameid, resellerid)
(providerid, resellerid, gameid)
We can factorize a little:
(providerid, resellerid, gameid)
(providerid, gameid)
(gameid, resellerid)
(resellerid)
You might find that dynamic SQL -- with the right indexes -- is more consistent:
DECLARE #sql NVARCHAR(max) = '
SELECT #exists = MAX(flag)
FROM (SELECT TOP (1) 1 as flag
FROM [tblGamePlayLog]
WHERE 1=1 #WHERE
) gpl
';
DECLARE #WHERE NVARCHAR(MAX) = '';
SET #WHERE = #WHERE +
(CASE WHEN #ProviderId <> -1 THEN ' AND [ProviderId] = #ProviderId' ELSE '' END) +
(CASE WHEN #ResellerId <> -1 THEN ' AND [ResellerId] = #ResellerId' ELSE '' END) +
(CASE WHEN #ProviderId <> -1 THEN ' AND [GameId] = #GameId' ELSE '' END);
SET #sql = REPLACE(#sql, '#WHERE', #WHERE);
DECLARE #exists INT;
exec sp_executesql #sql,
N'#ProviderId int, #ResellerId int, #ProviderId int, #exists INT OUTPUT',
#exists=#exists OUTPUT;
IF (#exists = 1) BEGIN
. . .
END;
The construction of the SQL and the overhead for running it should be relatively small -- you need to run a query anyway. This will guarantee the recompile. You will also want to be sure that you have appropriate indexes on table, which requires a lot of combinations (GMB points this out in that answer).

How to select multiple columns from a table in an IF ELSE stored procedure

I am trying to create a stored procedure which has 3 IF ELSE sections, in one of them I would like to select 2 columns to evaluate but am getting this error :
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS
In this case the guid&number are unique, here is the section of the procedure in question:
IF ((SELECT [guid],[number] from [tws] where [guid] = #ReferenceNumber and #Number = [number] and ([instruction_submitted] = '' or [instruction_submitted] is null )) is not null)
SET #ReturnValue = 2
ELSE ```
I think you want exists:
IF (EXISTS (SELECT [guid],[number]
from [tws]
where [guid] = #ReferenceNumber and #Number = [number] and
([instruction_submitted] = '' or [instruction_submitted] is null )
)
)
BEGIN
SET #ReturnValue = 2;
END;
ELSE . . .
I also strongly encourage you to use BEGIN/END around in IF statements -- that makes it less error-prone to add additional statements in the future.

Using SQL Server CASE statement in WHERE

I want to select records from a table in a stored procedure. Given parameters can be empty or a string including some keys separated by comma (1, 2, etc)
I want to manage that when a parameter is an empty string, "WHERE" ignore searching.
I'm using this code:
where (CASE when #PatientID <> 0 then ( dental.ID_Sick in (1,2)) else (1=1) end)
Something like that is working in W3School. I mean:
SELECT * FROM Customers
WHERE (case when 1=1 then (Country IN ('Germany', 'France', 'UK')) else 1=1 end);
What is the problem in my query that does not work? SQLServerManagementStudio is giving error on "IN" statement.
Solution:
The best way to handle such optional parameters is to use dynamic SQL and built the query on the fly. Something like....
CREATE PROCEDURE myProc
#Param1 VARCHAR(100) = NULL
,#Param2 VARCHAR(100) = NULL
,#Param3 VARCHAR(100) = NULL
,#ListParam VARCHAR(100) = NULL
--, etc etc...
AS
BEGIN
SET NOCOUNT ON;
Declare #Sql NVARCHAR(MAX);
SET #Sql = N' SELECT *
FROM TableName
WHERE 1 = 1 '
-- add in where clause only if a value was passed to parameter
+ CASE WHEN #Param1 IS NOT NULL THEN
N' AND SomeColumn = #Param1 ' ELSE N'' END
-- add in where clause a different variable
-- only if a value was passed to different parameter
+ CASE WHEN #Param2 IS NOT NULL THEN
N' AND SomeOtherColumn = #Param3 ' ELSE N'' END
-- List Parameter used with IN clause if a value is passed
+ CASE WHEN #ListParam IS NOT NULL THEN
N' AND SomeOtherColumn IN (
SELECT Split.a.value(''.'', ''VARCHAR(100)'') IDs
FROM (
SELECT Cast (''<X>''
+ Replace(#ListParam, '','', ''</X><X>'')
+ ''</X>'' AS XML) AS Data
) AS t CROSS APPLY Data.nodes (''/X'') AS Split(a) '
ELSE N'' END
Exec sp_executesql #sql
, N' #Param1 VARCHAR(100), #Param2 VARCHAR(100) ,#Param3 VARCHAR(100) ,#ListParam VARCHAR(100)'
, #Param1
, #Param2
,#Param3
, #ListParam
END
Problem with Other approach
There is a major issue with this other approach, you write your where clause something like...
WHERE ( ColumnName = #Parameter OR #Parameter IS NULL)
The Two major issues with this approach
1) you cannot force SQL Server to check evaluate an expression first like if #Parameter IS NULL, Sql Server might decide to evaluate first the expression ColumnName = #Parameterso you will have where clause being evaluated even if the variable value is null.
2) SQL Server does not do Short-Circuiting (Like C#), even if it decides to check the #Parameter IS NULL expression first and even if it evaluates to true, SQL Server still may go ahead and evaluating other expression in OR clause.
Therefore stick to Dynamic Sql for queries like this. and happy days.
SQL Server does not have a Bool datatype, so you can't assign or return the result of a comparison as a Bool as you would in other languages. A comparison can only be used with IF-statements or WHERE-clauses, or in the WHEN-part of a CASE...WHEN but not anywhere else.
Your specific example would become this:
SELECT * FROM Customers
WHERE 1=1 OR Country IN ('Germany', 'France', 'UK')
It would be better readable to rewrite your statement as follows:
WHERE #PatientID = 0
OR dental.ID_Sick in (1,2)
Referring to your actual question, I'd advise to read the linked question as provided by B House.
May be this straight way will work for you
IF (#PatientID <> 0)
BEGIN
SELECT * FROM Customers
WHERE Country IN ('Germany', 'France', 'UK')
END
try this:
WHERE 1=(CASE WHEN #PatientID <>0 AND dental.ID_Sick in (1,2) THEN 1
WHEN #PatientID =0 THEN 1
ELSE 0
END)

Adding dynamic condition in where clause

I want to add condition in 'where' clause depends upon 'if' condition like below
Declare #strSQLClause varchar(50)
If (#QMasterCompanyId='09' and #State='FL' and (#LOB='HO' or #LOB='HP'))
Begin
Declare #strMonthsOccupied char(1)
select Distinct #strMonthsOccupied=sm.MonthsOccupiedDesc from HOStructureRating sr
join HOSeleMonthsOccupied sm on sr.MonthsOccupied=sm.MonthsOccupiedCd
where AppId=#AppId
If(CONVERT(int,LTRIM(RTrim(#strMonthsOccupied))) > 9)
Begin
set #strSQLClause ='AND QuestionCd!=Q8'
End
Else
set #strSQLClause =''
End
so that in my Query will work as
select * from SHSeleQuestions where MasterCompanyId='09' + #strSQLClause
But this approach is not working fine, can anyone please help me on this.
There are two ways to do this one is use dynamic sql or other one is below mention :
select *
from SHSeleQuestions
where MasterCompanyId='09' AND
1 = CASE WHEN LEN(#strSQLClause) > 0 AND QuestionCd != 'Q8' THEN 1
WHEN LEN(#strSQLClause) = 0 THEN 1 END
Using Dynamic SQL
EXEC('select * from SHSeleQuestions where MasterCompanyId=''09''' + #strSQLClause ')
You would need to use dynamic SQL, but why not just have two statements that execute SQL, so rather than set #strSQLClause = 'AND ...', simply have a select statement here with the condition(s) you need
IF (#QMasterCompanyId='09' AND #State='FL' AND (#LOB='HO' OR #LOB='HP'))
BEGIN
DECLARE #strMonthsOccupied CHAR(1)
SELECT DISTINCT #strMonthsOccupied = sm.MonthsOccupiedDesc
FROM HOStructureRating sr
INNER JOIN HOSeleMonthsOccupied sm
ON sr.MonthsOccupied=sm.MonthsOccupiedCd
WHERE AppId=#AppId;
IF(CONVERT(INT,LTRIM(RTRIM(#strMonthsOccupied))) > 9)
BEGIN
SELECT *
FROM SHSeleQuestions
WHERE MasterCompanyId='09'
AND QuestionCd!='Q8';
RETURN;
END
END
SELECT *
FROM SHSeleQuestions
WHERE MasterCompanyId='09';
That being said, there are so many issues with the above I don't really know where to begin. You declare your variable then assign it an indeterminate value:
DECLARE #strMonthsOccupied CHAR(1)
SELECT DISTINCT #strMonthsOccupied = sm.MonthsOccupiedDesc
FROM HOStructureRating sr
INNER JOIN HOSeleMonthsOccupied sm
ON sr.MonthsOccupied=sm.MonthsOccupiedCd
WHERE AppId=#AppId;
If the query returns multiple rows then there is no clear logic for which value the variable should be assigned. The nex issue is that this CHAR(1) variable is clearly a number based on your attempted conversion:
IF(CONVERT(INT,LTRIM(RTRIM(#strMonthsOccupied))) > 9)
Why not just cut out the middle man and declare an INT to begin with. The next point is that it is a CHAR(1) so isn't actually big enough to store anything greater than 9, so your above condition will never be true.
Even if sm.MonthsOccupiedDesc, was 10, the variable would simply be truncated to 1, which is smaller than 9, so fails the condition, e.g.
DECLARE #strMonthsOccupied CHAR(1) = '10';
IF(CONVERT(INT,LTRIM(RTRIM(#strMonthsOccupied))) > 9)
PRINT 'TRUE';
ELSE
PRINT 'FALSE';

how to get results of function with datatype nvarchar

I have a database table like this
Id Code Amount Formula
-------------------------------------
1 A01 20.00
2 A08 0.00 dbo.ufn_Test(40)
3 A03 0.00 dbo.ufn_Test(60)
My Formula column is a string with name as a function in my database, how can I return the result into the Amount column?
My table has about 100000 rows so when I used while() it takes a lot of time.
I'm using SQL Server 2012
I've used dynamic SQL like this:
DECLARE #_j INT = 1
WHILE (#_j<=(SELECT MAX(Id) FROM #Ct_Lv))
BEGIN
SET #_CtLv = (SELECT Formula FROM #Ct_Lv WHERE Id = #_j)
DECLARE #sql NVARCHAR(MAX)
DECLARE #result NUMERIC(18, 2) = 0
SET #sql = N'set #result = N''''SELECT''' + #_CtLv
EXEC sp_executesql #sql, N'#result float output', #result out
UPDATE #Ct_Lv
SET Amount = #result
WHERE Id = #_j
SET #_j = #_j + 1
END
but my max #_j = 100000, I've run my code for 3 hours and it's still running
one thing, i would like know here is, does id attribute is identity column ?
2nd most important part is, you are declaring variables #sql and #result for each row and you are taking max at every row iterate, which might decrease the performance. I am not sure, how much faster the solution i have been given here, but you can try it once.
Set Nocount On;
Declare #_count Int
,#_j Int
,#_cnt Int
,#_dynamicSql Varchar(Max)
,#_formula Varchar(Max)
,#_row25Cnt Int
Select #_count = Count(1)
,#_j = 0
,#_cnt = 0
,#_dynamicSql = ''
,#_formula = ''
,#_row25Cnt = 1
From #Ct_Lv As ct With (Nolock)
While (#_cnt < #_count)
Begin
Select Top 1
#_j = ct.Id
,#_formula = ct.Formula
From #Ct_Lv As ct With (Nolock)
Where ct.Id > #_j
Order By ct.Id Asc
Select #_dynamicSql = 'Update ct Set ct.Amount = f.result From #Ct_Lv As ct Join ( Select ' + Cast(#_j As Varchar(20)) + ' As Id, [fuctionResultAttribute] As result From ' + #_formula + ' ) As f On ct.Id = f.Id; '
If (#_row25Cnt = 25)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Else If ((#_cnt + 1) = #_count)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Select #_cnt = #_cnt + 1
,#_row25Cnt = #_row25Cnt + 1
End
Here, what i have done so far is, I am looping Id by Id and generating dynamic sql for each 25 rows, once count is reach to 25, that dynamic sql will be executed which will update your amount. and again start generating dynamic sql for next 25 rows, and when count is about to end and there would be no 25 rows as end then dynamic sql will be executed when loop about to end in 'else if' condition.
above my solution will work only in that case when there would be only one formula in formula column for each row.
I just suggest one thing if Formula field calling the same function each time then better to store only parameter that you want to pass to the function, then you can easily process over huge data.
Else looping over huge data is not preferable way to perform any operation. So it's advisable to use some other trick over there in table structure and storing data.