Dynamic WHERE condition SQL SERVER - sql-server

I have the below requirement. When FirstName present I should return the only condtionFirstName when LastName present I should return the only condtionlastName when First and email both are present I should return condtionFirstName + condtionEmail.
Basically whichever value is present I should return that condition, If 1 value 1 condition, if 1 and 2 then condition 1 and 2, if 1 and 3 then condition 1 and 3, if only 3 then condition 3.
Kindly help me to get this logic.
DECLARE #FirstName NVARCHAR(100)
DECLARE #LastName NVARCHAR(100)
DECLARE #Email NVARCHAR(200)
DECLARE #condtionFirstName NVARCHAR(200)
DECLARE #condtionlastName NVARCHAR(200)
DECLARE #condtionEmail NVARCHAR(200)
SET #FirstName = 'JOhn'
SET #LastName = 'David'
SET #Email = 'john.david#abc.com'
SET #condtionFirstName = ' AND FirstName = ' + '''' + #FirstName + ''''
SET #condtionlastName = ' AND LastName = ' + '''' + #LastName + ''''
SET #condtionEmail = ' AND Email = ' + '''' + #Email + ''''

This is what I've long called "the kitchen sink" - you want a single query that can support any combination of search criteria. Some thoughts:
Stop trying to concatenate user input with executable strings - this is dangerous and error-prone. These should always be passed in as explicitly-typed parameters.
You can build a single string containing all the conditions instead of trying to create a new condition string for every possible condition.
You can declare and pass parameters to sp_executesql even if they don't all end up in the dynamic SQL statement. This is just like declaring a local variable and not using it.
e-mail addresses can be 320 characters long. If you only support 200, that could bite you.
Sample:
DECLARE #FirstName nvarchar(100),
#LastName nvarchar(100),
#Email nvarchar(320),
#conditions nvarchar(max) = N'';
SET #FirstName = N'John';
SET #LastName = N'David';
SET #Email = N'john.david#abc.com';
SET #conditions += CASE WHEN #FirstName IS NOT NULL THEN
N' AND FirstName = #FirstName' ELSE N'' END
+ CASE WHEN #LastName IS NOT NULL THEN
N' AND LastName = #LastName' ELSE N'' END
+ CASE WHEN #Email IS NOT NULL THEN
N' AND Email = #Email' ELSE N'' END;
DECLARE #sql nvarchar(max) = N'SELECT ... FROM dbo.table
WHERE 1 = 1' + #conditions;
PRINT #sql; -- try this when populating or not populating each of
-- #FirstName, #LastName, #Email
EXEC sys.sp_executesql #sql,
N'#FirstName nvarchar(100), #LastName nvarchar(100), #Email nvarchar(320)',
#FirstName, #LastName, #Email;
More details:
#BackToBasics: An Updated Kitchen Sink Example
Protecting Yourself from SQL Injection in SQL Server - Part 1
Protecting Yourself from SQL Injection in SQL Server - Part 2

Related

Optimize dynamic SQL stored procedure by reducing the unique query plans generated

For the below query, SQL Server is creating a unique query plan depending on the parameter which is passed, Is there any way to optimize the below query to reduce the number of query plans and optimize the query.
CREATE PROCEDURE [dbo].[Foo_search]
#ItemID INT,
#LastName VARCHAR(50),
#MiddleName VARCHAR(40),
#FirstName VARCHAR(50)
AS
BEGIN
DECLARE #Sql NVARCHAR(max)
SELECT #Sql = N 'Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email, From Employees Where DeletedOn Is Null ' +
CASE WHEN #LastName IS NULL OR #LastName = '' THEN '' ELSE ' And FamilyName=''' + #LastName + ''' ' END +
CASE WHEN #MiddleName IS NULL OR #MiddleName = '' THEN '' ELSE ' And MiddleName=''' + #MiddleName + ''' ' END +
CASE WHEN #FirstName IS NULL OR #FirstName = '' THEN '' ELSE ' And FirstName=''' + #FirstName + ''' ' END +
CASE WHEN #ItemID IS NOT NULL AND #ItemID > 0 THEN ' And ItemID=' + CONVERT(VARCHAR(10), #ItemID) + ' ' ELSE ' ' END
EXEC Sp_executesql #sql
END
Yes there is a way you can improve it, use the parameters correctly, rather than using string concatenation. Your method will generate a different query plan for every different combination of values of parameter, rather than just every different combination of parameters, which will generate orders of magnitude more query plans.
DECLARE #Sql nvarchar(max) = N'
Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email
From Employees
Where DeletedOn Is Null
'
+ CASE WHEN #LastName <> '' THEN ' And FamilyName = #LastName' ELSE '' END
+ CASE WHEN #MiddleName <> '' THEN ' And MiddleName = #MiddleName' ELSE '' END
+ CASE WHEN #FirstName <> '' THEN ' And FirstName = #FirstName' ELSE '' END
+ CASE WHEN #ItemID > 0 THEN ' And ItemID = #ItemID' ELSE '' END;
EXEC sp_executesql
#Sql,
N'#LastName varchar(50), #MiddleName varchar(40), #FirstName varchar(50)',
#LastName = #LastName,
#MiddleName = #MiddleName,
#FirstName = #FirstName;
And as Aaron Bertrand points out, this also fixes the issue where any parameter value containing a single quote (') will fail.
Beyond this however, as Aaron also mentions, the performance is most likely down to other issues such as indexing.
I don't think you're blaming the right thing, but if you really think that multiple plans are causing your CPU spikes, it's easy to change this back to a single plan strategy:
ALTER PROCEDURE dbo.Foo_search
#ItemID INT,
#LastName VARCHAR(50),
#MiddleName VARCHAR(40),
#FirstName VARCHAR(50)
AS
BEGIN
Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email
From dbo.Employees
Where DeletedOn Is Null
AND (FamilyName = #LastName OR #LastName IS NULL)
AND (MiddleName = #MiddleName OR #MiddleName IS NULL)
AND (FirstName = #FirstName OR #FirstName IS NULL)
AND (ItemID = #ItemID OR #ItemID IS NULL);
END
Let us know how that works out for you; I'm curious what index you're going to implement to make that perform well for all possible parameter combinations.

Use null result of exec sp_execute

I have some dynamic SQL that I use to see if a certain property for a certain client is in a table. If so, it will return the value, if not, it will return null. However, I would like it to return a default value if the property is not found.
The table:
CREATE TABLE [test].[customers](
[customer] [nvarchar](256) NULL,
[key_name] [nvarchar](256) NULL,
[key_value] [nvarchar](256) NULL
)
GO
INSERT INTO [test].[customers]
([customer]
,[key_name]
,[key_value])
VALUES
('JohnDoe'
,'periodlength'
,'3')
GO
The Dynamic SQL:
declare #customer as nvarchar(256) = 'JohnDoe'
declare #table as nvarchar(256) = 'test.customers'
declare #sql as nvarchar(4000)
set #sql = 'select [key_value] from ' + #table + ' where [key_name]= ''periodlength'' and customer= ''' + #customer + ''''
exec sp_executesql #sql
So when Qqerying for John Doe you get result 3 which is perfect. However, I would like to reurn 1 for Jane Doe. So I was thinking along the lines
IF exec sp_executesql #sql IS NULL 1 ELSE exec sp_executesql #sql
but that doesn't work.
How to change my dynamic query so that is returns a default value if the property is not found?
Could you try the following query but this usage only return a single value;
declare #customer as nvarchar(256) = 'JohnDoe'
declare #table as nvarchar(256) = 'test.customers'
DECLARE #ValReturn nvarchar(50)
declare #sql as nvarchar(4000)
set #sql = 'select #ValOutPut=[key_value] from ' + #table + ' where [key_name]= ''periodlength'' and customer= ''' + #customer + ''''
exec sp_executesql #sql , N'#ValOutPut nvarchar(25) OUTPUT' ,#ValOutPut = #ValReturn OUTPUT
BEGIN
IF #ValReturn IS NULL
SELECT NULL
ELSE
SELECT #ValReturn
END

Retrieve data from SQL Server with AND/OR when some parameters may be left blank

I am using a stored procedure to retrieve data from SQL Server. The application allows the user to search by several parameters. There may be times when the user needs to leave one or more fields empty.
For example, they may search by last name, zip code, race; or they may search by last name and zip but not race; or they may search by zip and race only. You get the idea.
It's easy to do with only a few parameters. However, now I have a dozen parameters and it would take a long time to code each possible option separately.
Here is what I've tried that doesn't work:
#LastName VARCHAR(50)
#Zip NUMERIC(5)
#Race VARCHAR(10)
AS
BEGIN
SELECT *
FROM myTable
WHERE LastName = #LastName AND LastName != ''
OR ZIP = #Zip AND ZIP != ''
OR Race = #Race AND Race != ''
How could I use AND/OR at the same time? This would get me the results I'm looking for.
You need to use parenthesis
SELECT *
FROM myTable
WHERE (LastName = #LastName AND LastName != '')
OR (ZIP =#Zip AND ZIP != '')
OR (Race = #Race AND Race != '')
This way, first the conditions in the parenthesis are executed and after that whole condition is tested.
But this query also has a bug. If the parameters #Lastname ='Smith', #ZIP=12345, #Race ='' then a record with LastName ='Doe', ZIP=12345 will also match .
I think
SELECT *
FROM myTable
WHERE (LastName = #LastName OR #LastName = '')
AND (ZIP =#Zip OR #Zip = '')
AND(Race = #Race OR #Race = '')
better suits your needs
Following code is harder to write but much more efficient as OR condition in the WHERE causes several performance problems as is not SARGable:
DECLARE #LastName varchar (50) = 'LAST'
DECLARE #Zip NUMERIC (5) = 1
DECLARE #Race varchar (10) = '1'
DECLARE #SQL AS NVARCHAR(MAX),
#WHERE AS NVARCHAR(MAX) = '';
SELECT #SQL = 'SELECT *
FROM myTable';
IF LEN(#LastName) > 0
SELECT #WHERE = #WHERE + 'LastName = ''' + #LastName + '''';
IF LEN(#Zip) > 0
IF LEN(#WHERE) > 0
SELECT #WHERE = #WHERE + ' AND ZIP = ''' + CONVERT(NVARCHAR(18), #Zip) + '''';;
ELSE
SELECT #WHERE = 'ZIP = ' + CONVERT(NVARCHAR(18), #Zip) ;
IF LEN(#Race) > 0
IF LEN(#WHERE) > 0
SELECT #WHERE = #WHERE + ' AND Race = ''' + #Race + '''';
ELSE
SELECT #WHERE = 'Race = ''' + #Race + '''';
IF LEN(#WHERE) > 0
SELECT #SQL = #SQL + ' WHERE ' + #WHERE
-- PRINT #SQL
EXEC sp_ExecuteSQL #SQL
Try to use parentheses
#LastName varchar (50)
#Zip numberic (5)
#Race varchar (10)
AS
BEGIN
SELECT *
FROM myTable
WHERE (LastName = #LastName AND LastName != '')
OR (ZIP =#Zip AND ZIP != '')
OR (Race = #Race AND Race != '')
When executing your query, code into parentheses is read with priority

How to search with parameter colums in a stored procedure?

I'm trying to write a stored procedure in SQL Server that gets the columns as parameters. The user will select the column name from a combo box and will write the searched value for that column on a textbox.
I've been searching how to do this and so far i have this:
ALTER PROCEDURE [dbo].[SP_Select_TBL_Folio]
#cant int,
#Column1 nvarchar(50),
#Value1 nvarchar(50),
#Column2 nvarchar(50),
#Value2 nvarchar(50),
#Column3 nvarchar(50),
#Value3 nvarchar(50)
AS
BEGIN
declare #query nvarchar (max)
SET NOCOUNT ON;
if #cant = 1
BEGIN
set #query = 'SELECT * FROM TBL_Folio WHERE ' + #Column1 + ' LIKE '+ #Value1 + ' ORDER BY 1 DESC';
exec sp_executesql #query, N' '
END
else
BEGIN
if #cant = 2
BEGIN
set #query = 'SELECT * FROM TBL_Folio WHERE ' + #Column1 + ' LIKE '+ #Value1 + ' AND ' + #Column2 + ' LIKE '+ #Value2 + ' ORDER BY 1 DESC';
exec sp_executesql #query, N' '
END
ELSE
if #cant = 3
BEGIN
set #query = 'SELECT * FROM TBL_Folio WHERE ' + #Column1 + ' LIKE '+ #Value1 + ' AND ' + #Column2 + ' LIKE '+ #Value2 + ' AND ' + #Column3 + ' LIKE '+ #Value3 + ' ORDER BY 1 DESC';
exec sp_executesql #query, N' '
END
END
END
The user can send 1 to 3 values, for that I have the parameter #cant, this code works but I want to know if there is a better way to do this or how can I improve this stored procedure.
I think what you have is fine if you need to do it in an SP rather than client side. I would probabably initialize the query to the 'select * from TBL_Folio" and then append the LIKES after each if. I would also caution against using SELECT * so your client side doesn't blow up if a field gets added to the table.
If you have a need to check a variable number of fields rather than just up to 3, you can do a table-valued parameter and build up your query by looping through. Here is an example:
ALTER PROCEDURE [dbo].[GetFilteredInvoices]
#FilterColumns ColumnValueType READONLY
AS
BEGIN
SET NOCOUNT ON;
declare #columnName varchar(50), #columnValue varchar(MAX), #query nvarchar(MAX), #count int
set #query='SELECT InvoiceNumber, InvoiceDate, Customer from Invoices '
set #count=0
set #columnName=''
while exists(select * from #FilterColumns where ColumnName>#ColumnName)
begin
set #columnName=(select min(ColumnName) from #FilterColumns where ColumnName>#columnName)
if #count=0
set #query=#query+'WHERE '
else
set #query=#query+'AND '
set #query=#query+ (select ColumnName+' Like ''%'+ColumnValue+'%'' ' from #filterColumns where ColumnName=#columnName)
set #count=#count+1
end
exec sp_executesql #query
END
Here is the table valued type I used:
CREATE TYPE [dbo].[ColumnValueType] AS TABLE(
[ColumnName] [varchar](50) NULL,
[ColumnValue] [varchar](max) NULL
)
GO
Now this will take any number of columns and values to apply the filter.
Here is an example call to the procedure:
DECLARE #RC int
DECLARE #FilterColumns [dbo].[ColumnValueType]
insert into #filterColumns
Values('InvoiceNumber','345')
,('Customer','67')
EXECUTE #RC = [dbo].[GetFilteredInvoices]
#FilterColumns
I think you can perhaps improve the way that you handle your input parameters by getting rid of the #cant parameter. You can also improve the way that you build up the conditions, at the moment you are not handling the situations where only #Column2 and #Value2 or only #Column3 and #Value3 is set (perhaps it is not needed in your case, but it is still good practice to handle these types of scenarios)
CREATE PROCEDURE SP_Select_TBL_Folio
#Column1 NVARCHAR(50) = NULL,
#Value1 NVARCHAR(50) = NULL,
#Column2 NVARCHAR(50) = NULL,
#Value2 NVARCHAR(50) = NULL,
#Column3 NVARCHAR(50) = NULL,
#Value3 NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#P1 NVARCHAR(500),
#P2 NVARCHAR(500),
#P3 NVARCHAR(500),
#SQL NVARCHAR(MAX)
IF (#Column1 IS NULL OR #Column1 = '') AND (#Value1 IS NULL OR #Value1 = '')
BEGIN
-- This will build up dynamic SQL to always select records even if #Column1
-- and #Value1 is not set. This obvisously all depends on your requirements
-- and if you still need to select records if the parameters are not set, otherwise
-- it can be changed to ' WHERE ThePrimaryKeyColumn = 0'
SET #P1 = ' WHERE ThePrimaryKeyColumn > 0'
END
ELSE
BEGIN
SET #P1 = 'WHERE ' + #Column1 + ' LIKE ' + '''' + #Value1 + ''''
END
IF (#Column2 IS NULL OR #Column2 = '') AND (#Value2 IS NULL OR #Value2 = '')
BEGIN
SET #P2 = ''
END
ELSE
BEGIN
SET #P2 = ' AND ' + #Column2 + ' LIKE ' + '''' + #Value2 + ''''
END
IF (#Column3 IS NULL OR #Column3 = '') AND (#Value3 IS NULL OR #Value3 = '')
BEGIN
SET #P3 = ''
END
ELSE
BEGIN
SET #P3 = ' AND ' + #Column3 + ' LIKE ' + '''' + #Value3 + ''''
END
SET #SQL = 'SELECT * FROM TBL_Folio
[P1]
[P2]
[P3]'
-- Here we set all the conditions
SET #SQL = REPLACE(#SQL, '[P1]', #P1);
SET #SQL = REPLACE(#SQL, '[P2]', #P2);
SET #SQL = REPLACE(#SQL, '[P3]', #P3);
-- This will be replaced by EXEC(#SQL)
PRINT #SQL
END
So now you can for instance execute
EXEC SP_Select_TBL_Folio
which will give you
SELECT * FROM TBL_Folio
WHERE ThePrimaryKeyColumn > 0
or you can execute
EXEC SP_Select_TBL_Folio 'Column1','Value1'
which will give you
SELECT * FROM TBL_Folio
WHERE Column1 LIKE 'Value1'
or you can execute
EXEC SP_Select_TBL_Folio NULL,NULL,'Column2','Value2'
which will give you
SELECT * FROM TBL_Folio
WHERE ThePrimaryKeyColumn > 0
AND Column2 LIKE 'Value2'
I'm not going to list all the permutations, I'm sure you get my point.

WHERE clause is not filtering in logical order

I'm writing a general search Stored Procedure to search in a table based on many filters which user can select in the UI (using MS-SQL 2008).
Here is ther simplified version:
CREATE PROCEDURE SearchAll
#FirstName NVARCHAR(MAX) = NULL,
#LastName NVARCHAR(MAX) = NULL,
#Age INT = NULL
AS
SELECT *
FROM persons
WHERE
(#FirstName IS NULL OR FirstName = #firstname)
AND (#LastName IS NULL OR LastName = #LastName)
AND (#Age IS NULL OR Age = #Age)
It seems that if I pass NULL to #Age there'll be no performance cost. But, when I'm testing with huge amount of data, I have a great perfomance lost!
Here is the queries which are the same logicaly but VERY different practically:
DECLARE #FirstName NVARCHAR(MAX) = NULL
DECLARE #Age INT = 23
------------First slow------------
SELECT *
FROM persons
WHERE
(#FirstName IS NULL OR FirstName = #firstname)
AND (#Age IS NULL OR Age = #Age)
------------Very fast------------
SELECT *
FROM persons
WHERE
Age = #Age
Did is miss a point?
I know that SQL engine finds the best match for indexes, and ... (before running the query),but it's obvious that: #FirstName IS NULL and there's no need to analyse anything.
I've also tested ISNULL function in the query (the same result).
Queries that contain this construct of #variable is null or #variable = column are a performance disaster. This is because SQL plans are created so they work for any value of the variable. For a lengthy discussion of the topic, the problems and possible solutions see Dynamic Search Conditions in T-SQL
The problem, as has already been mentioned, is that the query plan will be built to work with any value of the variables. You can circumvent this by building the query with only the paremeter required, as follows:
CREATE PROCEDURE SearchAll
#FirstName NVARCHAR(MAX) = NULL,
#LastName NVARCHAR(MAX) = NULL,
#Age INT = NULL
AS
BEGIN
DECLARE #sql NVARCHAR(MAX), #has_where BIT
SELECT #has_where = 0, #sql = 'SELECT * FROM persons '
IF #FirstName IS NOT NULL
SELECT #sql = #sql + 'WHERE FirstName = ''' + #FirstName + '''', #has_where = 1
IF #LastName IS NOT NULL
SELECT #sql = #sql + CASE WHEN #has_where = 0 THEN 'WHERE ' ELSE 'AND ' END + 'LastName = ''' + #LastName + '''', #has_where = 1
IF #Age IS NOT NULL
SELECT #sql = #sql + CASE WHEN #has_where = 0 THEN 'WHERE ' ELSE 'AND ' END + 'Age = ' + CAST(#Age AS VARCHAR), #has_where = 1
EXEC sp_executesql #sql
END

Resources