Stored Procedure with optional "WHERE" parameters - sql

I have a form where users can specify various parameters to dig through some data (status, date etc.).
I can produce a query that is:
SELECT * FROM table WHERE:
status_id = 3
date = <some date>
other_parameter = <value>
etc. Each WHERE is optional (I can select all the rows with status = 3, or all the rows with date = 10/10/1980, or all the rows with status = 3 AND date = 10/10/1980 etc.).
Given a large number of parameters, all optional, what is the best way to make up a dynamic stored procedure?
I'm working on various DB, such as:
MySQL, Oracle and SQLServer.

One of the easiest ways to accomplish this:
SELECT * FROM table
WHERE ((#status_id is null) or (status_id = #status_id))
and ((#date is null) or ([date] = #date))
and ((#other_parameter is null) or (other_parameter = #other_parameter))
etc.
This completely eliminates dynamic sql and allows you to search on one or more fields. By eliminating dynamic sql you remove yet another security concern regarding sql injection.

Create your procedure like this:
CREATE PROCEDURE [dbo].[spXXX]
#fromDate datetime = null,
#toDate datetime = null,
#subCode int = null
as
begin
set NOCOUNT ON
/* NOCOUNT limits the server feedback on select results record count */
SELECT
fields...
FROM
source
WHERE
1=1
--Dynamic where clause for various parameters which may or may not be passed in.
and ( #fromDate is null or [dateField] >= #fromDate)
and ( #toDate is null or [dateField] <= #toDate)
and ( #subCode is null or subCode= #leaveTypeSubCode)
order by fields...
This will allow you to execute the procedure with 0 params, all params, or any # of params.

This is the style I use:
t-sql
SELECT *
FROM table
WHERE
status_id = isnull(#status_id ,status_id)
and date = isnull(#date ,date )
and other_parameter = isnull(#other_parameter,other_parameter)
oracle
SELECT *
FROM table
WHERE
status_id = nval(p_status_id ,status_id)
and date = nval(p_date ,date )
and other_parameter = nval(p_other_parameter,other_parameter)

A readable and maintainable way to do it (even usable with JOIN/APPLY) :
where
(#parameter1 IS NULL OR your_condition1)
and (#parameter2 IS NULL OR your_condition2)
-- etc
However it's a bad idea on most big tables (even more using JOIN/APPLY), since your execution plan will not ignore NULL values and generates massive performance loophole (ex : scaning all a table searching for NULL values).
A roundabout way in SQL Server is to use WITH(RECOMPILE) options in your query (available since SQL 2008 SP1 CU5 (10.0.2746)).
The best way to implements this (performance wise) is to use IF ... ELSE block, one for each combination possible. Maybe it's exhausting but you will have the best performances and it doesn't matter your database settings.
If you need more details, you can look for KM. answer here.

You can do something like
WHERE
(
ParameterA == 4 OR ParameterA IS NULL
)
AND
(
ParameterB == 12 OR ParameterB IS NULL
)

If you want to avoid dynamically building up SQL strings (which is often best avoided), you can do this in stored procs by comparing each critera in your where claused with a default value, which equates to "ignore". E.g.:
select * from Table where
(#Col1 IS NULL OR Col1 = #Col1) /*If you don't want to filter in #col, pass in NULL*/
AND
(#Col2 IS NULL OR Col2 = #Col2)

Related

Make SQL SERVER evaluate clauses in a certain order

Take the following table as an instance:
CREATE TABLE TBL_Names(Name VARCHAR(32))
INSERT INTO TBL_Names
VALUES ('Ken'),('1965'),('Karen'),('2541')
sqlfiddle
Executing following query throws an exception:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.tblNames
WHERE ISNUMERIC([name]) = 1 )
AND [name] = 2541
Msg 245, Level 16, State 1, Line 1 Conversion failed when converting
the varchar value 'Ken' to data type int.
While the following query executes without error:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE ISNUMERIC([name]) = 1
AND [name] = 2541
I know that this is because of SQL Server Query Optimizer's decision. but I am wondering if there is any way to make sql server evaluate clauses in a certain order. this way, in the first query,the first clause filters out those Names that are not numeric so that the second clause will not fail at converting to a number.
Update: As you may noticed, the above query is just an instance to exemplify the problem. I know the risks of that implicit conversion and appreciate those who tried to warn me of that. However my main question is how to change Optimizer's behavior of evaluating clauses in a certain order.
There is no "direct" way of telling the engine to perform operations in order. SQL isn't an imperative language where you have complete control of how to do things, you simply tell what you need and the server decides how to do it itself.
For this particular case, as long as you have [name] = 2541, you are risking a potential conversion failure since you are comparing a VARCHAR column against an INT. Even if you use a subquery/CTE there is still room for the optimizer to evaluate this expression first and try to convert all varchar values to int (thus failing).
You can evade this with workarounds:
Correctly comparing matching data types:
[name] = '2541'
Casting [name] to INT beforehand and only whenever possible and on a different statement, do the comparison.
DECLARE #tblNamesInt TABLE (nameInt INT)
INSERT INTO #tblNamesInt (
nameInt)
SELECT
[nameInt] = CONVERT(INT, [name])
FROM
dbo.tblNames
WHERE
TRY_CAST([name] AS INT) IS NOT NULL -- TRY_CAST better than ISNUMERIC for INT
SELECT
*
FROM
#tblNamesInt AS T
WHERE
T.nameInt = 2351 -- data types match
Even an index hint won't force the optimizer to use an index (that's why it's called a hint), so we have little control on how it gets stuff done.
There are a few mechanics that we know are evaluated in order and we can use to our advantage, such as the HAVING expressions will always be computed after grouping values, and the grouping always after WHERE conditions. So we can "safely" do the following grouping:
DECLARE #Table TABLE (IntsAsVarchar VARCHAR(100))
INSERT INTO #Table (IntsAsVarchar)
VALUES
('1'),
('2'),
('20'),
('25'),
('30'),
('A') -- Not an INT!
SELECT
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END,
COUNT(*)
FROM
#Table AS T
WHERE
TRY_CAST(T.IntsAsVarchar AS INT) IS NOT NULL -- Will filter out non-INT values first
GROUP BY
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END
But you should always avoid writing code that implies implicit conversions (like T.IntsAsVarchar < 15).
Try like this
SELECT [name]
FROM #TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM #TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
2)
AND [name] = convert(varchar,2541 )
Since You are storing name as varchar(32) varchar will accept integer datatype values also called precedence value
What about:
SELECT *
FROM dbo.tblNames AS tn
WHERE [name] = convert(varchar, 2541)
Why do you need ISNUMERIC([name]) = 1) since you only care about the value '2541'?
You can try this
SELECT [name]
FROM dbo.TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
You need to just [name] = 2541 to [name] = '2541'. You are missing ' (single quote) with name in where condition.
You can find the live demo Here.
Honestly, I wouldn't apply the implicit cast to your column [name], it'll make the query non-SARGable. Instead, convert the value of your input (or pass it as a string)
SELECT [name]
FROM dbo.TBL_Names tn
WHERE [name] = CONVERT(varchar(32),2541);
If you "must", however, wrap [name] (and suffer performance degradation) then use TRY_CONVERT:
SELECT [name]
FROM dbo.TBL_Names tn
WHERE TRY_CONVERT(int,[name]) = 2541;

WHERE clause in CASE

I want to write SQL that if field #tank_id is null then not include this field in WHERE clause. Else include.
So here my sql:
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2) FROM myTable
(CASE
WHEN #tank_id = 0 THEN (WHERE dt < #dateTo)
ELSE (WHERE dt < #dateTo AND tank_id = #tank_id)
END)
But I get error:
Error: Incorrect syntax near the keyword 'WHERE'.
SQLState: S1000
ErrorCode: 156
The code you provided does not match your text description.
In your description, you are asking how to not use the #tank_id if it's null, but in the code it looks like you want to not use it if it's 0 - so I'm going on the assumption that the text of the question is correct.
You can't use case as a flow control.
case is an expression that can only be used to return a single scalar value based on condition(s).
However, for this kind of query you don't need to use case, you can simply use a combination of and and or, like the following:
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2)
FROM myTable
WHERE dt < #dateTo
AND (
tank_id = #tank_id
OR #tank_id IS NULL
)
BTW, you should always specify the RDBMS you are working with (oracle, mySql, postgreSQL, SQL Server etc`) in the tags, as well as add a tag for the specific version.
Try the following
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2)
FROM myTable
WHERE dt < #dateTo
AND tank_id = (
case when #tank_id is null then tank_id else #tank_id end
)
No can do that.
A SELECT can be accompanied by at most one WHERE. That's how the syntax is defined.
That WHERE clause, if present, gets compiled (a.o. for the purpose of determining the physical data access strategy) so there is simply no means to make "what the WHERE clause consists of" in any way dependent upon information that can be known only at run-time (e.g. values in columns of the rows being processed).
So what you have to do is think about the cases you can run into and write your single WHERE clause such that it covers all cases. The comments and answers show you the correct way for the particular case you mentioned in your question.

How do I properly SELECT WHERE Effective_Date >= 'Given_Date' in a stored procedure?

I have this select statement that returns the results I'm looking for:
SELECT *
FROM Database.dbo.Table
WHERE Effective_Date >= '04/01/2014'
AND Chain = 'MCD'
I'm looking to turn this into a stored procedure with the following variables, #EffectiveDate and #Chain so that I can simply replace the date and chain to get different results. Here is the stored procedure I've made that doesn't work correctly:
CREATE PROCEDURE Database.dbo.StoredProc
#Chain VARCHAR(255),
#EffectiveDate VARCHAR(255)
AS
SELECT *
FROM Database.dbo.Table
WHERE Effective_Date >= '+#EffectiveDate+'
AND Chain = '+#Chain+'
GO
I'd like to execute this stored procedure like this:
EXEC Database.dbo.StoredProc
#PharmacyChain = N'MCD',
#EffectiveDate = N'04/01/2014'
;
GO
In this example, Table.Effective_Date is in datetime format. When I run the SELECT statement w/o the stored proc, the date comparison works fine to only select records with effective date after '04/01/2014'. However, when it's run using the variables int he stored proc, it doesn't convert the date correctly to compare. I've tried changing the EffectiveDate variable to datetime format, but still had no luck. Any help would be greatly appreciated. Thank you
Parameters should match the column datatype
#Chain VARCHAR(255) -- what is Chain?
#EffectiveDate datetime -- or date etc
And simply do this
SELECT *
FROM dbo.Table
WHERE Effective_Date >= #EffectiveDate
AND Chain = #Chain;
You don't need 3 part object names either

How to write filtered queries using SQL stored procedures?

How can I write a SQL stored procedure where I want the parameters to be optional in the select statement?
try this.. Make the SPs input parameters that control the filtering optional, witrh default values of null. In each select statement's Where clause, write the predicate like this:
Create procedure MyProcedure
#columnNameValue [datatype] = null
As
Select [stuff....]
From table
Where ColumnName = Coalesce(#columnNameValue , ColumnName)
this way if you do not include the parameter, or if you pass a null value for the parameter, the select statement will filter on where the column value is equal to itself, (effectively doing no filtering at all on that column.)
The only negative to this is that it prevents you from being able to pass a null as a meaningfull value to explicitly filter on only the nulls.... (i.e., Select only the rows where the value is null) Once the above technique has been adopted, you would need to add another parameter to implement that type of requirement. ( say, #GetOnlyNulls TinyInt = 0, or something similar)
Create procedure MyProcedure
#columnNameValue [datatype] = null,
#GetOnlyNulls Tinyint = 0
As
Select [stuff....]
From table
Where (ColumnName Is Null And #GetOnlyNulls = 1)
Or ColumnName = Coalesce(#columnNameValue , ColumnName)

Why does my sql date comparison return 0 results

I apologize in advance if this question is too long but I wanted to make sure I included all the steps I followed to get to this point.
I have the following table in my SQL Server 2008 database:
CREATE TABLE [VSPRRecalc](
[VSPRDate] [datetimeoffset](7) NOT NULL,
[CalcType] [int] NOT NULL,
CONSTRAINT [PK_VSPRRecalc] PRIMARY KEY CLUSTERED ([VSPRDate] ASC)
It has some rows in it that look like this:
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-15 10:17:49.5780000 -05:00','3')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-16 07:44:03.3750000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-17 07:40:40.1090000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-18 16:29:02.2203744 -05:00','2')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-20 09:58:50.1250000 -05:00','1')
INSERT [vsprrecalc](VSPRDate,CalcType) VALUES('2010-12-29 19:21:26.8120000 -05:00','1')
I'm using linq to check and see if a given date already exists in this table:
var recalc = (from re in VSPRRecalcs
where re.VSPRDate.Date == oDate.Value.Date
select re).SingleOrDefault();
Currently recalc returns null whenever the date is within 5 hours of midnight (like the 12-29 case in the insert statements above). I checked and the following sql is being executed:
exec sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE
CONVERT(DATE, [t0].[VSPRDate]) = #p0',N'#p0 datetime',#p0='2010-12-29'
Which returns 0 records. I modified the query to make the test easier to play with and came up with the following:
declare #t as date
set #t = '2010-12-29'
select *,
case when CONVERT(DATE, [VSPRDate]) = #t then 'true' else 'false' end
from VSPRRecalc where
CONVERT(DATE, [VSPRDate]) = #t
That query works for any other date in the table but not for any date that is within 5 hours of midnight (again see 12-29 above). If I run the above query without the where clause the 12-29 row does have 'true' displayed so clearly the boolean is evaluating the way I expect in the select statement but not in the where clause. Why does that happen?
I would say that's a bug on SQL Server, regarding conversion between the DATETIMEOFFSET time and the more "standard" types DATETIME and DATE...
What I have find out is the following:
This works:
EXEC sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE [t0].[VSPRDate] = #p0',
N'#p0 DATETIMEOFFSET(7)',
#p0 = '2010-12-29 19:21:26.8120000 -05:00'
Which means that when we keep using DATETIMEOFFSET, there is no problem whatsoever... Still, you seem to need to find all records in a given day, not search for an exact DATETIMEOFFSET, right?
So, probably a little bit more useful, this works also:
EXEC sp_executesql N'SELECT [t0].[VSPRDate], [t0].[CalcType]
FROM [dbo].[VSPRRecalc] AS [t0]
WHERE [t0].[VSPRDate] BETWEEN #p0 AND #p1',
N'#p0 DATETIMEOFFSET, #p1 DATETIMEOFFSET',
#p0 = '2010-12-29 00:00:00.0000000 -05:00',
#p1 = '2010-12-30 00:00:00.0000000 -05:00'
I guess the secret here is keep using the DATETIMEOFFSET data type (and it's CLR equivalent, System.DateTimeOffset). That way you will not get into this conversion issue...
(And, by the way, you should use a BETWEEN for searching records based on a date anyway. This allows the DBMS to use an index over that column, which is not possible when your WHERE clause is making a function call or a hard-coded conversion).
Edit I forgot there is no BETWEEN operator available for Linq for SQL - but that's easy to fix, just use something like WHERE [t0].[VSPRDate] >= #p0 AND [t0].[VSPRDate] <= #p1'... Also, this SO question is about declaring an extension method in order to implement it, but I don't know if it works...