Conditional WHERE Clauses In A Stored Procedure - sql

This question may boil down to something simpler, but I am still curious as to how close SQL Server / TSQL can get to conditional WHERE clauses (and reasoning behind why they don't exist would also be interesting).
I have a stored procedure that, for a few parameters, takes in an enumeration array (which has been accordingly translated to a user-defined table type which essentially mocks an int array). For reference the data type is as follows:
CREATE TYPE myIntArray AS TABLE (
val INT
);
My stored procedure is along the following lines (altered to be more simplistic):
CREATE PROCEDURE myProc
#homeID INT,
#name VARCHAR(500),
#hometype_enum myIntArray READONLY,
#country_enum myIntArray READONLY
AS
BEGIN
SELECT * FROM my_table
WHERE name=#name
END
GO
What I am wanting to do is additionally filter the results of my query based upon the values of the enum arrays that were passed in as INT tables, IFF they even have values passed in (it is possible the tables might be empty). The pseudo code would look something like this:
SELECT *
FROM my_table
WHERE name = #name
IF((SELECT COUNT(val) FROM #hometype_enum) > 0)
BEGIN
AND hometype IN (SELECT val FROM hometype_enum)
END
IF((SELECT COUNT(val) FROM #country_enum ) > 0)
BEGIN
AND country IN (SELECT val FROM country_enum )
END
The two enums are independent of each other, so it's possible a search could be made and be filtered on no enum (both tables empty), either-or, or both enums.
My actual query involves multiple columns, tables, and unions (ugly, I know), so it's not as nice as just being able to copy/paste a 3-line SELECT for each scenario. I am currently using some pretty ugly temp table logic that I'll spare the reader's eyes from at the moment.
Aside from figuring out my particular problem, my main question is: does SQL Server support conditional WHERE clause statements (I am convinced it does not from my research)? Why is this (architectural, time complexity, space complexity issues)? Are there any more-or-less terse ways to go about emulating a conditional clause, such as taking advantage of conditional short-circuiting?
Thank you all for your insights. Another day of learning!

As suggested in the comments, the best way to handle this kind of conditional where clause would be to use dynamic-sql ..... Something like....
CREATE PROCEDURE myProc
#homeID INT,
#name VARCHAR(500),
#hometype_enum myIntArray READONLY,
#country_enum myIntArray READONLY
AS
BEGIN
SET NOCOUNT ON
Declare #Sql NVarchar(MAX);
SET #Sql = N' SELECT * FROM my_table '
+ N' WHERE name = #name '
+ CASE WHEN EXISTS (Select * FROM #hometype_enum)
THEN N' AND hometype IN (SELECT val FROM hometype_enum) ' ELSE N' ' END
+ CASE WHEN EXISTS (Select * FROM #country_enum)
THEN N' AND country IN (SELECT val FROM country_enum ) ' ELSE N' ' END
Exec sp_executesql #Sql
,N'#homeID INT , #name VARCHAR(500),
#hometype_enum myIntArray, #country_enum myIntArray'
,#homeID
,#name
,#hometype_enum
,#country_enum
END
GO
Using sp_executesql will allow sql server to store parameterised execution plans for the same stored procedure. It is different execution plans for different sets/combinations of a parameters for the same stored procedure for optimal performance.

The below one works fine too. There is no need for dynamic-sql. The MS SQL will handle it without issues (with good performance).
CREATE PROCEDURE myProc
#homeID INT,
#name VARCHAR(500),
#hometype_enum myIntArray READONLY,
#country_enum myIntArray READONLY
AS
BEGIN
SELECT * FROM my_table
WHERE name=#name
AND ( ( SELECT count(val) FROM #hometype_enum ) = 0
OR hometype IN (SELECT val FROM #hometype_enum) )
AND ( ( SELECT count(val) FROM #country_enum ) = 0
OR country IN (SELECT val FROM #country_enum) )
END
GO
The OR will make the job for you. When first part : SELECT count(val) FROM #hometype_enum ) = 0 will return true then the second one will not be executed at all - no error from empty IN clause. When the first part will return some value grater than 0 then the second one will be evaluated correctly.

Related

I'm having trouble with the sql language functions

I have been working in the SQL language for 1 month. That's why I may not understand things. But I always get an error when creating these functions. I wrote the codes down there. The error message is as follows:
Only one expression can be specified in the select list when
the subquery is not introduced with EXISTS.
CREATE FUNCTION deneme(
#ID int
)
RETURNS nvarchar(max)
AS
BEGIN
DECLARE #value nvarchar(max)
SET #value = (
SELECT * FROM information
WHERE #ID = Person_id
)
RETURN #value
END
You cannot assign all columns values into one variable like that, and since you're passing an ID of the person and you want your function to returns the info of that person
CREATE FUNCTION dbo.deneme
(
#ID int
)
RETURNS NVARCHAR(300)
AS
BEGIN
DECLARE #Value NVARCHAR(300) = N'';
SELECT #Value = CONCAT(I.FirstName, N' ', I.LastName)
FROM Information I
WHERE I.PersonId = #ID;
RETURN #Value;
END
As others have pointed out you are trying to place multiple columns/fields in a single column/field.
#ID is a single column. "Select *" is presumably returning more than a single column or else it wouldn't be much help!
In order to change this and make it work as you are trying here, you would need to concat the columns you are trying to return. This is almost surely not the best way to accomplish this but sometimes concating names (for example) is fine.
The other issue you may be running into is even if you changed this to "Select ID" but still have errors it may be because the query returns more than one row matching that criteria. You can work around this (it is a work around most of the time) by limiting the number of rows returned with "TOP 1". But be careful as this may not return the information you want. You can use an order by statement to help ensure it is the correct information (such as order by Time_entered).
The code below with "TOP 1" and concatenating multiple columns (and casting as the same type) will always work.
Again, these are not Best Practices and shouldn't be used to sanitize data in production... but it does show you why you are getting these errors and how to prevent them.
CREATE FUNCTION deneme(
#ID int
)
RETURNS nvarchar(max)
AS
BEGIN
DECLARE #value nvarchar(max)
SET #value = (
SELECT TOP 1 cast(First_name as nvarchar) + N' ' + cast(Last_name as nvarchar) FROM information
WHERE #ID = Person_id
Order by Time_entered desc
)
RETURN #value
END

How do you call a variable (that is a list of values) in a conditional statement?

I am trying to create a procedure where the conditional statement changes based on the value that the user inputs. A simplified example of what I'm trying to achieve is below.
Create Procedure [LossRatioReport] (#construction AS VARCHAR(2), #state AS VARCHAR(2)) AS
DECLARE #construction_condition AS NVARCHAR(MAX)
DECLARE #construction AS VARCHAR(2)
SET #construction = '01'
SET #construction_condition = CASE #construction WHEN '01'
THEN #construction_condition = '('01', 'Frame Construction', 'Frame')'
ELSE #construction_condition = '00'
END
BEGIN
SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction = #construction_condition)
END
GO
I want to do it this way because there is a list of both integer and strings for each type and I don't want to rewrite the code over and over for each condition. But when I try and run my code, I keep getting incorrect syntax messages for the variables in the WHERE statement.
You'll need to use dynamic sql for this:
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = 'SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction IN( ' + #construction_condition
EXEC sp_executesql #sql
As Sean Lange stated, something should probably be mentioned on SQL Injection. Be sure you are aware of the danger before implementing a solution with dynamic SQL. Here is an overview, and Google can tell you much, much more.

Variable "IN" expression in SQL [duplicate]

This question already has answers here:
Closed 12 years ago.
Possible Duplicates:
SQL Multiple Parameter Values
SQL Server (2008) Pass ArrayList or String to SP for IN()
I would like to SELECT some rows from a table that have certain values which are not known at the time a stored procedure is written. For example, searching for books of a particular type or types in a library database:
SELECT * FROM Books WHERE Type IN (_expr_);
Where I want _expr_ to be ('Humor', 'Thriller') one run, and maybe ('Education') the next, depending on the user's choices. How can I vary the expression at run-time?
Unfortunately, I still have a lot to learn about SQL in general and am not sure if I'm even asking a question that makes sense. I would appreciate any guidance!
This is trickier than you might think in SQL Server 2005 (2008 has table valued parameters which makes it easier)
See http://www.sommarskog.se/arrays-in-sql-2005.html for a review of the methods.
I feel like I've answered this question before...
anyway, I've long used the following user defined split function:
Usage: dbo.Split("#ParamName", ",") where the 2nd parameter is the separator.
You can then join this onto a table, as it returns a table value function with the elementID and Element.
CREATE FUNCTION [dbo].[Split]
(
#vcDelimitedString varchar(max),
#vcDelimiter varchar(100)
)
RETURNS #tblArray TABLE
(
ElementID smallint IDENTITY(1,1), --Array index
Element varchar(1000) --Array element contents
)
AS
BEGIN
DECLARE #siIndex smallint, #siStart smallint, #siDelSize smallint
SET #siDelSize = LEN(#vcDelimiter)
--loop through source string and add elements to destination table array
WHILE LEN(#vcDelimitedString) > 0
BEGIN
SET #siIndex = CHARINDEX(#vcDelimiter, #vcDelimitedString)
IF #siIndex = 0
BEGIN
INSERT INTO #tblArray VALUES(#vcDelimitedString)
BREAK
END
ELSE
BEGIN
INSERT INTO #tblArray VALUES(SUBSTRING(#vcDelimitedString, 1,#siIndex - 1))
SET #siStart = #siIndex + #siDelSize
SET #vcDelimitedString = SUBSTRING(#vcDelimitedString, #siStart , LEN(#vcDelimitedString) - #siStart + 1)
END
END
RETURN
END
another approach, is to build a sql string and use execute to execute it. The string is of "INSERT...SELECT form" and inserts the results into a temporary table. Then you select from the temp.
declare #sql varchar(1000)
set #sql = 'INSERT INTO sometemptable SELECT * FROM Books WHERE Type IN ('
set #sql = #sql + {code that builds a syntactically correct list}
set #sql = #sql + ')'
execute #s_sql
select * from sometemptable
What you do here for sql server 2005 and prior is put the user parameters in a table, and then select from the table:
select columns
from books
where type in
(
select choices
from userchoices
where sessionkey= #sessionkey and userid= #userid
)

TSQL Statement IN

I am having a small problem with the IN SQL statement. I was just wondering if anyone could help me?
#Ids = "1,2,3,4,5"
SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (CONVERT(VARCHAR,#Ids))
This is coming back with the error below, I am sure this is pretty simple!
Conversion failed when converting the varchar value '1,' to data type int.
The SQL IN clause does not accept a single variable to represent a list of values -- no database does, without using dynamic SQL. Otherwise, you could use a Table Valued Function (SQL Server 2000+) to pull the values out of the list & return them as a table that you can join against.
Dynamic SQL example:
EXEC('SELECT *
FROM Nav
WHERE NavigationID IN ('+ #Ids +')')
I recommend reading The curse and blessings of dynamic SQL before using dynamic SQL on SQL Server.
Jason:
First create a function like this
Create FUNCTION [dbo].[ftDelimitedAsTable](#dlm char, #string varchar(8000))
RETURNS
--------------------------------------------------------------------------*/
/*------------------------------------------------------------------------
declare #dlm char, #string varchar(1000)
set #dlm=','; set #string='t1,t2,t3';
-- tHIS FUNCION RETUNRS IN THE ASCENDING ORDER
-- 19TH Apr 06
------------------------------------------------------------------------*/
--declare
#table_var TABLE
(id int identity(1,1),
r varchar(1000)
)
AS
BEGIN
declare #n int,#i int
set #n=dbo.fnCountChars(#dlm,#string)+1
SET #I =1
while #I <= #N
begin
insert #table_var
select dbo.fsDelimitedString(#dlm,#string,#i)
set #I= #I+1
end
if #n =1 insert #TABLE_VAR VALUES(#STRING)
delete from #table_var where r=''
return
END
And then
set quoted_identifier off
declare #ids varchar(max)
select #Ids = "1,2,3,4,5"
declare #nav table ( navigationid int identity(1,1),theother bigint)
insert #nav(theother) select 10 union select 11 union select 15
SELECT * FROM #Nav WHERE CONVERT(VARCHAR,NavigationID) IN (select id from dbo.ftDelimitedAsTable(',',#Ids))
select * from dbo.ftDelimitedAsTable(',',#Ids)
What you're doing is not possible with the SQL IN statement. You cannot pass a string to it and expect that string to be parsed. IN is for specific, hard-coded values.
There are two ways to do what you want to do here.
One is to create a 'dynamic sql' query and execute it, after substituting in your IN list.
DECLARE #query varchar(max);
SET #query = 'SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (' + #Ids + ')'
exec (#query)
This can have performance impacts and other complications. Generally I'd try to avoid it.
The other method is to use a User Defined Function (UDF) to split the string into its component parts and then query against that.
There's a post detailing how to create that function here
Once the function exists, it's trivial to join onto it
SELECT * FROM Nav
CROSS APPLY dbo.StringSplit(#Ids) a
WHERE a.s = CONVERT(varchar, Nav.NavigationId)
NB- the 'a.s' field reference is based on the linked function, which stores the split value in a column named 's'. This may differ based on the implementation of your string split function
This is nice because it uses a set based approach to the query rather than an IN subquery, but a CROSS JOIN may be a little complex for the moment, so if you want to maintain the IN syntax then the following should work:
SELECT * FROM Nav
WHERE Nav.NavigationId IN
(SELECT CONVERT(int, a.s) AS Value
FROM dbo.StringSplit(#Ids) a

Passing SQL stored procedure entirety of WHERE clause

I have a SQL stored procedure of the form
SELECT [fields] FROM [table] WHERE #whereSql
I want to pass the procedure an argument (#whereSql) which specifies the entire WHERE clause, but the following error is returned:
An expression of non-boolean type specified in a context where a condition is expected
Can this be done?
The short answer is that you can't do it like this -- SQL Server looks at the contents of a variable as a VALUE. It doesn't dynamically build up the string to execute (which is why this is the correct way to avoid SQL injection attacks).
You should make every effort to avoid a dynamic WHERE as you're trying to do, largely for this reason, but also for the sake of efficiency. Instead, try to build up the WHERE clause so that it short-circuits pieces with lots of ORs, depending on the situation.
If there's no way around it, you can still build a string of your own assembled from the pieces of the command, and then EXEC it.
So you could do this:
DECLARE #mywhere VARCHAR(500)
DECLARE #mystmt VARCHAR(1000)
SET #mywhere = ' WHERE MfgPartNumber LIKE ''a%'' '
SELECT #mystmt = 'SELECT TOP 100 * FROM Products.Product AS p ' + #mywhere + ';'
EXEC( #mystmt )
But I recommend instead that you do this:
SELECT TOP 100 *
FROM Products.Product AS p
WHERE
( MfgPartNumber LIKE 'a%' AND ModeMfrPartNumStartsWith=1)
OR ( CategoryID = 123 AND ModeCategory=1 )
I believe this can be done using Dynamic SQL. See below:
CREATE PROCEDURE [dbo].[myProc]
#whereSql nvarchar(256)
AS
EXEC('SELECT [fields] FROM [table] WHERE ' + #whereSql)
GO
That said, you should do some serious research on dynamic SQL before you actually use it.
Here are a few links that I came across after a quick search:
http://www.sommarskog.se/dynamic_sql.html
http://msdn.microsoft.com/en-us/library/aa224806%28SQL.80%29.aspx
http://www.itjungle.com/fhg/fhg100505-story02.html
Make sure you read this fully
www.sommarskog.se/dynamic_sql.html
Dynamic SQL listed in some of the Answers is definitely a solution. However, if Dynamic SQL needs to be avoided, one of the solutions that I prefer is to make use of table variables (or temp tables) to store the parameter value that is used for comparison in WHERE clause.
Here is an example Stored Procedure implementation.
CREATE PROCEDURE [dbo].[myStoredProc]
#parameter1 varchar(50)
AS
declare #myTempTableVar Table(param1 varchar(50))
insert into #myTempTableVar values(#parameter1)
select * from MyTable where MyColumn in (select param1 from #myTempTableVar)
GO
In case you want to pass in multiple values, then the comma separated values can be stored as rows in the table variable and used in the same way for comparison.
CREATE PROCEDURE [dbo].[myStoredProc]
#parameter1 varchar(50)
AS
--Code Block to Convert Comma Seperated Parameter into Values of a Temporary Table Variable
declare #myTempTableVar Table(param1 varchar(50))
declare #index int =0, #tempString varchar(10)
if charindex(',',#parameter1) > 0
begin
set #index = charindex(',',#parameter1)
while #index > 0
begin
set #tempString = SubString(#parameter1,1,#index-1)
insert into #myTempTableVar values (#tempString)
set #parameter1 = SubString(#parameter1,#index+1,len(#parameter1)-#index)
set #index = charindex(',',#parameter1)
end
set #tempString = #parameter1
insert into #myTempTableVar values (#tempString)
end
else
insert into #myTempTableVar values (#parameter1)
select * from MyTable where MyColumn in (select param1 from #myTempTableVar)
GO
http://sqlmag.com/t-sql/passing-multivalued-variables-stored-procedure
try this it works!!
CHARINDEX (',' + ColumnName + ',', ',' +
REPLACE(#Parameter, ' ', '') + ',') > 0
execute syntax set #Parameter= 'nc1,nc2'