How to create a stored procedure with optional parameters? [duplicate] - sql-server

I am creating a stored procedure to do a search through a table. I have many different search fields, all of which are optional. Is there a way to create a stored procedure that will handle this? Let's say I have a table with four fields: ID, FirstName, LastName and Title. I could do something like this:
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = ISNULL(#FirstName, FirstName) AND
LastName = ISNULL(#LastName, LastName) AND
Title = ISNULL(#Title, Title)
END
This sort of works. However it ignores records where FirstName, LastName or Title are NULL. If Title is not specified in the search parameters I want to include records where Title is NULL - same for FirstName and LastName. I know I could probably do this with dynamic SQL but I would like to avoid that.

Dynamically changing searches based on the given parameters is a complicated subject and doing it one way over another, even with only a very slight difference, can have massive performance implications. The key is to use an index, ignore compact code, ignore worrying about repeating code, you must make a good query execution plan (use an index).
Read this and consider all the methods. Your best method will depend on your parameters, your data, your schema, and your actual usage:
Dynamic Search Conditions in T-SQL by by Erland Sommarskog
The Curse and Blessings of Dynamic SQL by Erland Sommarskog
If you have the proper SQL Server 2008 version (SQL 2008 SP1 CU5 (10.0.2746) and later), you can use this little trick to actually use an index:
Add OPTION (RECOMPILE) onto your query, see Erland's article, and SQL Server will resolve the OR from within (#LastName IS NULL OR LastName= #LastName) before the query plan is created based on the runtime values of the local variables, and an index can be used.
This will work for any SQL Server version (return proper results), but only include the OPTION(RECOMPILE) if you are on SQL 2008 SP1 CU5 (10.0.2746) and later. The OPTION(RECOMPILE) will recompile your query, only the verison listed will recompile it based on the current run time values of the local variables, which will give you the best performance. If not on that version of SQL Server 2008, just leave that line off.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
OPTION (RECOMPILE) ---<<<<use if on for SQL 2008 SP1 CU5 (10.0.2746) and later
END

The answer from #KM is good as far as it goes but fails to fully follow up on one of his early bits of advice;
..., ignore compact code, ignore worrying about repeating code, ...
If you are looking to achieve the best performance then you should write a bespoke query for each possible combination of optional criteria. This might sound extreme, and if you have a lot of optional criteria then it might be, but performance is often a trade-off between effort and results. In practice, there might be a common set of parameter combinations that can be targeted with bespoke queries, then a generic query (as per the other answers) for all other combinations.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
IF (#FirstName IS NOT NULL AND #LastName IS NULL AND #Title IS NULL)
-- Search by first name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
ELSE IF (#FirstName IS NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by last name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
LastName = #LastName
ELSE IF (#FirstName IS NULL AND #LastName IS NULL AND #Title IS NOT NULL)
-- Search by title only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
Title = #Title
ELSE IF (#FirstName IS NOT NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by first and last name
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
AND LastName = #LastName
ELSE
-- Search by any other combination
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
END
The advantage of this approach is that in the common cases handled by bespoke queries the query is as efficient as it can be - there's no impact by the unsupplied criteria. Also, indexes and other performance enhancements can be targeted at specific bespoke queries rather than trying to satisfy all possible situations.

You can do in the following case,
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR FirstName = #FirstName) AND
(#LastNameName IS NULL OR LastName = #LastName) AND
(#Title IS NULL OR Title = #Title)
END
however depend on data sometimes better create dynamic query and execute them.

Five years late to the party.
It is mentioned in the provided links of the accepted answer, but I think it deserves an explicit answer on SO - dynamically building the query based on provided parameters. E.g.:
Setup
-- drop table Person
create table Person
(
PersonId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Person PRIMARY KEY,
FirstName NVARCHAR(64) NOT NULL,
LastName NVARCHAR(64) NOT NULL,
Title NVARCHAR(64) NULL
)
GO
INSERT INTO Person (FirstName, LastName, Title)
VALUES ('Dick', 'Ormsby', 'Mr'), ('Serena', 'Kroeger', 'Ms'),
('Marina', 'Losoya', 'Mrs'), ('Shakita', 'Grate', 'Ms'),
('Bethann', 'Zellner', 'Ms'), ('Dexter', 'Shaw', 'Mr'),
('Zona', 'Halligan', 'Ms'), ('Fiona', 'Cassity', 'Ms'),
('Sherron', 'Janowski', 'Ms'), ('Melinda', 'Cormier', 'Ms')
GO
Procedure
ALTER PROCEDURE spDoSearch
#FirstName varchar(64) = null,
#LastName varchar(64) = null,
#Title varchar(64) = null,
#TopCount INT = 100
AS
BEGIN
DECLARE #SQL NVARCHAR(4000) = '
SELECT TOP ' + CAST(#TopCount AS VARCHAR) + ' *
FROM Person
WHERE 1 = 1'
PRINT #SQL
IF (#FirstName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #FirstName'
IF (#LastName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #LastName'
IF (#Title IS NOT NULL) SET #SQL = #SQL + ' AND Title = #Title'
EXEC sp_executesql #SQL, N'#TopCount INT, #FirstName varchar(25), #LastName varchar(25), #Title varchar(64)',
#TopCount, #FirstName, #LastName, #Title
END
GO
Usage
exec spDoSearch #TopCount = 3
exec spDoSearch #FirstName = 'Dick'
Pros:
easy to write and understand
flexibility - easily generate the query for trickier filterings (e.g. dynamic TOP)
Cons:
possible performance problems depending on provided parameters, indexes and data volume
Not direct answer, but related to the problem aka the big picture
Usually, these filtering stored procedures do not float around, but are being called from some service layer. This leaves the option of moving away business logic (filtering) from SQL to service layer.
One example is using LINQ2SQL to generate the query based on provided filters:
public IList<SomeServiceModel> GetServiceModels(CustomFilter filters)
{
var query = DataAccess.SomeRepository.AllNoTracking;
// partial and insensitive search
if (!string.IsNullOrWhiteSpace(filters.SomeName))
query = query.Where(item => item.SomeName.IndexOf(filters.SomeName, StringComparison.OrdinalIgnoreCase) != -1);
// filter by multiple selection
if ((filters.CreatedByList?.Count ?? 0) > 0)
query = query.Where(item => filters.CreatedByList.Contains(item.CreatedById));
if (filters.EnabledOnly)
query = query.Where(item => item.IsEnabled);
var modelList = query.ToList();
var serviceModelList = MappingService.MapEx<SomeDataModel, SomeServiceModel>(modelList);
return serviceModelList;
}
Pros:
dynamically generated query based on provided filters. No parameter sniffing or recompile hints needed
somewhat easier to write for those in the OOP world
typically performance friendly, since "simple" queries will be issued (appropriate indexes are still needed though)
Cons:
LINQ2QL limitations may be reached and forcing a downgrade to LINQ2Objects or going back to pure SQL solution depending on the case
careless writing of LINQ might generate awful queries (or many queries, if navigation properties loaded)

Extend your WHERE condition:
WHERE
(FirstName = ISNULL(#FirstName, FirstName)
OR COALESCE(#FirstName, FirstName, '') = '')
AND (LastName = ISNULL(#LastName, LastName)
OR COALESCE(#LastName, LastName, '') = '')
AND (Title = ISNULL(#Title, Title)
OR COALESCE(#Title, Title, '') = '')
i. e. combine different cases with boolean conditions.

This also works:
...
WHERE
(FirstName IS NULL OR FirstName = ISNULL(#FirstName, FirstName)) AND
(LastName IS NULL OR LastName = ISNULL(#LastName, LastName)) AND
(Title IS NULL OR Title = ISNULL(#Title, Title))

Related

T-SQL : how to select rows based off an AND filter, where many values won't be set [duplicate]

I am creating a stored procedure to do a search through a table. I have many different search fields, all of which are optional. Is there a way to create a stored procedure that will handle this? Let's say I have a table with four fields: ID, FirstName, LastName and Title. I could do something like this:
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = ISNULL(#FirstName, FirstName) AND
LastName = ISNULL(#LastName, LastName) AND
Title = ISNULL(#Title, Title)
END
This sort of works. However it ignores records where FirstName, LastName or Title are NULL. If Title is not specified in the search parameters I want to include records where Title is NULL - same for FirstName and LastName. I know I could probably do this with dynamic SQL but I would like to avoid that.
Dynamically changing searches based on the given parameters is a complicated subject and doing it one way over another, even with only a very slight difference, can have massive performance implications. The key is to use an index, ignore compact code, ignore worrying about repeating code, you must make a good query execution plan (use an index).
Read this and consider all the methods. Your best method will depend on your parameters, your data, your schema, and your actual usage:
Dynamic Search Conditions in T-SQL by by Erland Sommarskog
The Curse and Blessings of Dynamic SQL by Erland Sommarskog
If you have the proper SQL Server 2008 version (SQL 2008 SP1 CU5 (10.0.2746) and later), you can use this little trick to actually use an index:
Add OPTION (RECOMPILE) onto your query, see Erland's article, and SQL Server will resolve the OR from within (#LastName IS NULL OR LastName= #LastName) before the query plan is created based on the runtime values of the local variables, and an index can be used.
This will work for any SQL Server version (return proper results), but only include the OPTION(RECOMPILE) if you are on SQL 2008 SP1 CU5 (10.0.2746) and later. The OPTION(RECOMPILE) will recompile your query, only the verison listed will recompile it based on the current run time values of the local variables, which will give you the best performance. If not on that version of SQL Server 2008, just leave that line off.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
OPTION (RECOMPILE) ---<<<<use if on for SQL 2008 SP1 CU5 (10.0.2746) and later
END
The answer from #KM is good as far as it goes but fails to fully follow up on one of his early bits of advice;
..., ignore compact code, ignore worrying about repeating code, ...
If you are looking to achieve the best performance then you should write a bespoke query for each possible combination of optional criteria. This might sound extreme, and if you have a lot of optional criteria then it might be, but performance is often a trade-off between effort and results. In practice, there might be a common set of parameter combinations that can be targeted with bespoke queries, then a generic query (as per the other answers) for all other combinations.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
IF (#FirstName IS NOT NULL AND #LastName IS NULL AND #Title IS NULL)
-- Search by first name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
ELSE IF (#FirstName IS NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by last name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
LastName = #LastName
ELSE IF (#FirstName IS NULL AND #LastName IS NULL AND #Title IS NOT NULL)
-- Search by title only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
Title = #Title
ELSE IF (#FirstName IS NOT NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by first and last name
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
AND LastName = #LastName
ELSE
-- Search by any other combination
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
END
The advantage of this approach is that in the common cases handled by bespoke queries the query is as efficient as it can be - there's no impact by the unsupplied criteria. Also, indexes and other performance enhancements can be targeted at specific bespoke queries rather than trying to satisfy all possible situations.
You can do in the following case,
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR FirstName = #FirstName) AND
(#LastNameName IS NULL OR LastName = #LastName) AND
(#Title IS NULL OR Title = #Title)
END
however depend on data sometimes better create dynamic query and execute them.
Five years late to the party.
It is mentioned in the provided links of the accepted answer, but I think it deserves an explicit answer on SO - dynamically building the query based on provided parameters. E.g.:
Setup
-- drop table Person
create table Person
(
PersonId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Person PRIMARY KEY,
FirstName NVARCHAR(64) NOT NULL,
LastName NVARCHAR(64) NOT NULL,
Title NVARCHAR(64) NULL
)
GO
INSERT INTO Person (FirstName, LastName, Title)
VALUES ('Dick', 'Ormsby', 'Mr'), ('Serena', 'Kroeger', 'Ms'),
('Marina', 'Losoya', 'Mrs'), ('Shakita', 'Grate', 'Ms'),
('Bethann', 'Zellner', 'Ms'), ('Dexter', 'Shaw', 'Mr'),
('Zona', 'Halligan', 'Ms'), ('Fiona', 'Cassity', 'Ms'),
('Sherron', 'Janowski', 'Ms'), ('Melinda', 'Cormier', 'Ms')
GO
Procedure
ALTER PROCEDURE spDoSearch
#FirstName varchar(64) = null,
#LastName varchar(64) = null,
#Title varchar(64) = null,
#TopCount INT = 100
AS
BEGIN
DECLARE #SQL NVARCHAR(4000) = '
SELECT TOP ' + CAST(#TopCount AS VARCHAR) + ' *
FROM Person
WHERE 1 = 1'
PRINT #SQL
IF (#FirstName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #FirstName'
IF (#LastName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #LastName'
IF (#Title IS NOT NULL) SET #SQL = #SQL + ' AND Title = #Title'
EXEC sp_executesql #SQL, N'#TopCount INT, #FirstName varchar(25), #LastName varchar(25), #Title varchar(64)',
#TopCount, #FirstName, #LastName, #Title
END
GO
Usage
exec spDoSearch #TopCount = 3
exec spDoSearch #FirstName = 'Dick'
Pros:
easy to write and understand
flexibility - easily generate the query for trickier filterings (e.g. dynamic TOP)
Cons:
possible performance problems depending on provided parameters, indexes and data volume
Not direct answer, but related to the problem aka the big picture
Usually, these filtering stored procedures do not float around, but are being called from some service layer. This leaves the option of moving away business logic (filtering) from SQL to service layer.
One example is using LINQ2SQL to generate the query based on provided filters:
public IList<SomeServiceModel> GetServiceModels(CustomFilter filters)
{
var query = DataAccess.SomeRepository.AllNoTracking;
// partial and insensitive search
if (!string.IsNullOrWhiteSpace(filters.SomeName))
query = query.Where(item => item.SomeName.IndexOf(filters.SomeName, StringComparison.OrdinalIgnoreCase) != -1);
// filter by multiple selection
if ((filters.CreatedByList?.Count ?? 0) > 0)
query = query.Where(item => filters.CreatedByList.Contains(item.CreatedById));
if (filters.EnabledOnly)
query = query.Where(item => item.IsEnabled);
var modelList = query.ToList();
var serviceModelList = MappingService.MapEx<SomeDataModel, SomeServiceModel>(modelList);
return serviceModelList;
}
Pros:
dynamically generated query based on provided filters. No parameter sniffing or recompile hints needed
somewhat easier to write for those in the OOP world
typically performance friendly, since "simple" queries will be issued (appropriate indexes are still needed though)
Cons:
LINQ2QL limitations may be reached and forcing a downgrade to LINQ2Objects or going back to pure SQL solution depending on the case
careless writing of LINQ might generate awful queries (or many queries, if navigation properties loaded)
Extend your WHERE condition:
WHERE
(FirstName = ISNULL(#FirstName, FirstName)
OR COALESCE(#FirstName, FirstName, '') = '')
AND (LastName = ISNULL(#LastName, LastName)
OR COALESCE(#LastName, LastName, '') = '')
AND (Title = ISNULL(#Title, Title)
OR COALESCE(#Title, Title, '') = '')
i. e. combine different cases with boolean conditions.
This also works:
...
WHERE
(FirstName IS NULL OR FirstName = ISNULL(#FirstName, FirstName)) AND
(LastName IS NULL OR LastName = ISNULL(#LastName, LastName)) AND
(Title IS NULL OR Title = ISNULL(#Title, Title))

Table-valued function displaying only 1 result

I've got this table valued function and I want it to display all records with the same "PeopleID". How can I do it? It's currently displaying only 1 of the possible results.
ALTER FUNCTION dbo.ufnGetContactInformation(#FuncID int)
RETURNS #retContactInformation TABLE
(
-- Columns returned by the function
FuncID int PRIMARY KEY NOT NULL,
Full_Name nvarchar(75) NULL,
Phone nvarchar(100) NULL,
Email nvarchar(50) NULL,
City nvarchar(20) NULL
)
AS
BEGIN
DECLARE
#fullname nvarchar(75),
#phonenumber nvarchar(100),
#email nvarchar(50),
#city nvarchar(20);
-- > Get common contact information
SELECT
#fullname = p.Full_Name,
#phonenumber = c.Phone,
#email = c.Email,
#city = c.CityID
FROM d_People p, d_Contacts c
WHERE p.PeopleID=#FuncID;
-- < Get common contact information
IF #FuncID IS NOT NULL
BEGIN
INSERT #retContactInformation
SELECT #FuncID, #fullname, #phonenumber, #email, #city;
END;
RETURN;
END;
GO
You need a proper join for your two tables, otherwise your query makes no sense. What you have now will give you one person full_name, but every contact phone, email, and city.
Bad habits to kick : using old-style JOINs - Aaron Bertrand
When you select into the scalar variables, you will only get one result in each variable. Inserting the variables after setting them to only hold one row will only ever yield at most one row.
You can simplify your function as an in-line table-valued function like so:
alter function dbo.ufngetcontactinformation(#funcid int)
returns table as return (
select
funcid = p.peopleid
, fullname = p.full_name
, phonenumber = c.phone
, email = c.email
, city = c.cityid
from d_people p, d_contacts c
where p.peopleid = #funcid;
);
go
Reference
When is a SQL function not a function? "If it’s not inline, it’s rubbish." - Rob Farley
Inline Scalar Functions - Itzik Ben-Gan
Scalar functions, inlining, and performance: An entertaining title for a boring post - Adam Machanic
TSQL User-Defined Functions: Ten Questions You Were Too Shy To Ask - Robert Sheldon
If you have to have a multi-statement table valued function despite the perfomance hit:
alter function dbo.ufngetcontactinformation(#funcid int)
returns #retcontactinformation table
(
-- columns returned by the function
funcid int primary key not null,
full_name nvarchar(75) null,
phone nvarchar(100) null,
email nvarchar(50) null,
city nvarchar(20) null
)
as
begin
insert #retcontactinformation
select
#funcid
, p.full_name
, c.phone
, c.email
, c.cityid
from d_people p, d_contacts c
where p.peopleid = #funcid;
return;
end;
go

High performance optional where clause in SQL Server (without using dynamic query) [duplicate]

I am creating a stored procedure to do a search through a table. I have many different search fields, all of which are optional. Is there a way to create a stored procedure that will handle this? Let's say I have a table with four fields: ID, FirstName, LastName and Title. I could do something like this:
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = ISNULL(#FirstName, FirstName) AND
LastName = ISNULL(#LastName, LastName) AND
Title = ISNULL(#Title, Title)
END
This sort of works. However it ignores records where FirstName, LastName or Title are NULL. If Title is not specified in the search parameters I want to include records where Title is NULL - same for FirstName and LastName. I know I could probably do this with dynamic SQL but I would like to avoid that.
Dynamically changing searches based on the given parameters is a complicated subject and doing it one way over another, even with only a very slight difference, can have massive performance implications. The key is to use an index, ignore compact code, ignore worrying about repeating code, you must make a good query execution plan (use an index).
Read this and consider all the methods. Your best method will depend on your parameters, your data, your schema, and your actual usage:
Dynamic Search Conditions in T-SQL by by Erland Sommarskog
The Curse and Blessings of Dynamic SQL by Erland Sommarskog
If you have the proper SQL Server 2008 version (SQL 2008 SP1 CU5 (10.0.2746) and later), you can use this little trick to actually use an index:
Add OPTION (RECOMPILE) onto your query, see Erland's article, and SQL Server will resolve the OR from within (#LastName IS NULL OR LastName= #LastName) before the query plan is created based on the runtime values of the local variables, and an index can be used.
This will work for any SQL Server version (return proper results), but only include the OPTION(RECOMPILE) if you are on SQL 2008 SP1 CU5 (10.0.2746) and later. The OPTION(RECOMPILE) will recompile your query, only the verison listed will recompile it based on the current run time values of the local variables, which will give you the best performance. If not on that version of SQL Server 2008, just leave that line off.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
OPTION (RECOMPILE) ---<<<<use if on for SQL 2008 SP1 CU5 (10.0.2746) and later
END
The answer from #KM is good as far as it goes but fails to fully follow up on one of his early bits of advice;
..., ignore compact code, ignore worrying about repeating code, ...
If you are looking to achieve the best performance then you should write a bespoke query for each possible combination of optional criteria. This might sound extreme, and if you have a lot of optional criteria then it might be, but performance is often a trade-off between effort and results. In practice, there might be a common set of parameter combinations that can be targeted with bespoke queries, then a generic query (as per the other answers) for all other combinations.
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
IF (#FirstName IS NOT NULL AND #LastName IS NULL AND #Title IS NULL)
-- Search by first name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
ELSE IF (#FirstName IS NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by last name only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
LastName = #LastName
ELSE IF (#FirstName IS NULL AND #LastName IS NULL AND #Title IS NOT NULL)
-- Search by title only
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
Title = #Title
ELSE IF (#FirstName IS NOT NULL AND #LastName IS NOT NULL AND #Title IS NULL)
-- Search by first and last name
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
FirstName = #FirstName
AND LastName = #LastName
ELSE
-- Search by any other combination
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR (FirstName = #FirstName))
AND (#LastName IS NULL OR (LastName = #LastName ))
AND (#Title IS NULL OR (Title = #Title ))
END
The advantage of this approach is that in the common cases handled by bespoke queries the query is as efficient as it can be - there's no impact by the unsupplied criteria. Also, indexes and other performance enhancements can be targeted at specific bespoke queries rather than trying to satisfy all possible situations.
You can do in the following case,
CREATE PROCEDURE spDoSearch
#FirstName varchar(25) = null,
#LastName varchar(25) = null,
#Title varchar(25) = null
AS
BEGIN
SELECT ID, FirstName, LastName, Title
FROM tblUsers
WHERE
(#FirstName IS NULL OR FirstName = #FirstName) AND
(#LastNameName IS NULL OR LastName = #LastName) AND
(#Title IS NULL OR Title = #Title)
END
however depend on data sometimes better create dynamic query and execute them.
Five years late to the party.
It is mentioned in the provided links of the accepted answer, but I think it deserves an explicit answer on SO - dynamically building the query based on provided parameters. E.g.:
Setup
-- drop table Person
create table Person
(
PersonId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Person PRIMARY KEY,
FirstName NVARCHAR(64) NOT NULL,
LastName NVARCHAR(64) NOT NULL,
Title NVARCHAR(64) NULL
)
GO
INSERT INTO Person (FirstName, LastName, Title)
VALUES ('Dick', 'Ormsby', 'Mr'), ('Serena', 'Kroeger', 'Ms'),
('Marina', 'Losoya', 'Mrs'), ('Shakita', 'Grate', 'Ms'),
('Bethann', 'Zellner', 'Ms'), ('Dexter', 'Shaw', 'Mr'),
('Zona', 'Halligan', 'Ms'), ('Fiona', 'Cassity', 'Ms'),
('Sherron', 'Janowski', 'Ms'), ('Melinda', 'Cormier', 'Ms')
GO
Procedure
ALTER PROCEDURE spDoSearch
#FirstName varchar(64) = null,
#LastName varchar(64) = null,
#Title varchar(64) = null,
#TopCount INT = 100
AS
BEGIN
DECLARE #SQL NVARCHAR(4000) = '
SELECT TOP ' + CAST(#TopCount AS VARCHAR) + ' *
FROM Person
WHERE 1 = 1'
PRINT #SQL
IF (#FirstName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #FirstName'
IF (#LastName IS NOT NULL) SET #SQL = #SQL + ' AND FirstName = #LastName'
IF (#Title IS NOT NULL) SET #SQL = #SQL + ' AND Title = #Title'
EXEC sp_executesql #SQL, N'#TopCount INT, #FirstName varchar(25), #LastName varchar(25), #Title varchar(64)',
#TopCount, #FirstName, #LastName, #Title
END
GO
Usage
exec spDoSearch #TopCount = 3
exec spDoSearch #FirstName = 'Dick'
Pros:
easy to write and understand
flexibility - easily generate the query for trickier filterings (e.g. dynamic TOP)
Cons:
possible performance problems depending on provided parameters, indexes and data volume
Not direct answer, but related to the problem aka the big picture
Usually, these filtering stored procedures do not float around, but are being called from some service layer. This leaves the option of moving away business logic (filtering) from SQL to service layer.
One example is using LINQ2SQL to generate the query based on provided filters:
public IList<SomeServiceModel> GetServiceModels(CustomFilter filters)
{
var query = DataAccess.SomeRepository.AllNoTracking;
// partial and insensitive search
if (!string.IsNullOrWhiteSpace(filters.SomeName))
query = query.Where(item => item.SomeName.IndexOf(filters.SomeName, StringComparison.OrdinalIgnoreCase) != -1);
// filter by multiple selection
if ((filters.CreatedByList?.Count ?? 0) > 0)
query = query.Where(item => filters.CreatedByList.Contains(item.CreatedById));
if (filters.EnabledOnly)
query = query.Where(item => item.IsEnabled);
var modelList = query.ToList();
var serviceModelList = MappingService.MapEx<SomeDataModel, SomeServiceModel>(modelList);
return serviceModelList;
}
Pros:
dynamically generated query based on provided filters. No parameter sniffing or recompile hints needed
somewhat easier to write for those in the OOP world
typically performance friendly, since "simple" queries will be issued (appropriate indexes are still needed though)
Cons:
LINQ2QL limitations may be reached and forcing a downgrade to LINQ2Objects or going back to pure SQL solution depending on the case
careless writing of LINQ might generate awful queries (or many queries, if navigation properties loaded)
Extend your WHERE condition:
WHERE
(FirstName = ISNULL(#FirstName, FirstName)
OR COALESCE(#FirstName, FirstName, '') = '')
AND (LastName = ISNULL(#LastName, LastName)
OR COALESCE(#LastName, LastName, '') = '')
AND (Title = ISNULL(#Title, Title)
OR COALESCE(#Title, Title, '') = '')
i. e. combine different cases with boolean conditions.
This also works:
...
WHERE
(FirstName IS NULL OR FirstName = ISNULL(#FirstName, FirstName)) AND
(LastName IS NULL OR LastName = ISNULL(#LastName, LastName)) AND
(Title IS NULL OR Title = ISNULL(#Title, Title))

Query optimization not using index

I'm running into performance issues with Sql Server 2008 R2, which I've narrowed down to the query optimizer (I think!). I'm looking for a definitive "why does this happen, or is it a bug?".
For sake of discussion I'll use this example, but the same problem has been seen across multiple sprocs with the same scenario.
We have a table containing payment methods; key fields are PaymentMethodId and UserId.
PaymentMethodId is an int, and the PK; UserId is a nvarchar(255) with a non-clustered index.
The query is similar to the following:
Sproc params worth mentioning:
#id int = null
#userId nvarchar(255) = null
There is an if statement at the beginning of the sproc to disallow both parameters being null.
select * from PaymentMethods (nolock) pm
where (#userId is null or #userId = pm.UserId)
and (#id is null or #id = pm.PaymentMethodId)
In the case where #userId is null, I would expect the optimizer to detect the first where clause as always true; if #userId is NOT null, I would expect it to use the index on UserId.
I have the same expectations for #id.
What we're seeing, is that regardless of input values the database is electing to do a full table scan.
While this is concerning on its own, it gets more interesting.
When updating the query where clause to equivalent of below, it is using the indecies correctly.
select * from PaymentMethods (nolock) pm
where ((#userId is null and pm.UserId is null) OR #userId = pm.UserId)
and (#id is null or #id = pm.PaymentMethodId)
What is going on? Why is "#userId is null" being considered for every record, (or is it?) Or is the real issue sitting in front of they keyboard?!
There can be a number of reasons why your sp is slow. For instance, stored procedures create a plan depending on the values for the parameters when you first run that sp. This means that you get the same plan even when the new values may return a completely different result set, one that could benefit from another plan. You could try using dynamic SQL or run the sp with OPTION(RECOMPILE) so the optimizer can create another execution plan. This is one example:
CREATE STORED PROCEDURE dbo.Test #userid INT, #id INT
AS
DECLARE #sql NVARCHAR(4000)
SET #sql = N'SELECT *
FROM PaymentMethods pm
WHERE 1 = 1'
SET #sql = #sql +
CASE
WHEN #userid IS NOT NULL THEN N' AND pm.UserId = #userid '
ELSE N''
END +
CASE
WHEN #id IS NOT NULL THEN N' AND pm.PaymentMethodId = #id '
ELSE N''
END
EXEC sp_executesql #sql, N'#userid INT, #id INT', #userid, #id;

What is the simplest way to check if a procedure's parameters are all NULL?

Lets say I have a stored procedure like so:
CREATE PROCEDURE People_Select
#LastName nvarchar(25) = NULL,
#FirstName nvarchar(25) = NULL,
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM People
WHERE (#LastName IS NULL OR (LastName = #LastName))
AND (#FirstName IS NULL OR (FirstName = #FirstName))
OPTION (RECOMPILE)
END
When this gets executed and at least one (or both) of the parameters are set it works perfectly. However, if someone happens to call it without setting either parameter it will return all rows from the People table. This is problematic especially if my People table has millions of records.
The preferred functionality (in my case) would be for it to return nothing if NO paramters are set. I could easily check if lastName is null AND if firstname is null and return nothing but is there a more elegant way of doing this? What if my stored procedure had 20 optional parameters? I don't want to check if they are ALL null first and return nothing.. is there an easier way? Thanks.
try the following
CREATE PROCEDURE People_Select
#LastName nvarchar(25) = NULL,
#FirstName nvarchar(25) = NULL,
AS
BEGIN
IF #LastName is null and #FirstName is null
goto end_section:
SET NOCOUNT ON;
SELECT *
FROM People
WHERE (#LastName IS NULL OR (LastName = #LastName))
AND (#FirstName IS NULL OR (FirstName = #FirstName))
OPTION (RECOMPILE)
end_section:
END

Resources