I'm new here, and relatively new to stored procedures, so please bear with me! I've checked related questions on here and can't find anything that works in this instance.
I am trying to build a stored procedure (MS SQL Server 2005) that takes a number of passed in values and in effect dynamically builds up the SQL as you would with inline SQL.
This is where I've come unstuck.
We have (somewhat simplified for clarity):
#searchf1 varchar(100), -- search filter 1
#searchr1 varchar(100), -- search result 1
#searchf2 varchar(100), -- search filter 2
#searchr2 varchar(100), -- search result 2
#direction char(1), -- direction to order results in
AS
set nocount on
set dateformat dmy
SELECT *
FROM database.dbo.table T
WHERE T.deleted = 'n'
ORDER BY CASE #direction
WHEN 'A' THEN T.id
WHEN 'D' THEN T.id DESC
END
END
set nocount off
I have also tried the lines from ORDER BY as:
IF #direction = 'N' THEN
ORDER BY
T.id
ELSE
ORDER BY
T.id DESC
Both approaches give me an error along the lines:
"Incorrect syntax near the keyword 'DESC'." (which references the line id DESC following the final ORDER BY
As part of this stored procedure I also want to try to feed in matched pairs of values which reference a field to look up and a field to match it to, these could either be present or ''. To do that I need to add into the SELECT section code similar to:
WHERE
deleted = 'n'
IF #searchf1 <> '' THEN
AND fieldf1 = #searchf1 AND fieldr1 = #searchr1
This however generates errors like:
Incorrect syntax near the keyword 'IF'.
I know dynamic SQL of this type isn't the most elegant. And I know that I could do it with glocal IF ELSE statements, but if I did the SP would be thousands of lines long; there are going to up to 15 pairs of these search fields, together with the direction and field to order that direction on.
(the current version of this SP uses a passed in list of IDs to return generated by some inline dynamic SQL, through doing this I'm trying to reduce it to one hit to generate the recordset)
Any help greatly appreciated. I've hugely simplified the code in the above example for clarity, since it's the general concept of a nested IF statement with SELECT and ORDER BY that I'm inquiring about.
For this I would try to go with a more formal Dynamic SQL solution, something like the following, given your defined input parameters
DECLARE #SQL VARCHAR(MAX)
SET #SQL = '
SELECT
FROM
database.dbo.table T
WHERE
T.deleted = ''n'' '
--Do your conditional stuff here
IF #searchf1 <> '' THEN
SET #SQL = #SQL + ' AND fieldf1 = ' + #searchf1 + ' AND fieldr1 = ' + #searchr1 + ''' '
--Finish the query
SET #SQL = #SQL + ' ORDER BY xxx'
EXEC(#SQL)
DISCLAIMER: The use of Dynamic SQL is NOT something that should be taken lightly, and proper consideration should be taken in ALL circumstances to ensure that you are not open to SQL injection attacks, however, for some dynamic search type operations it is one of the most elegant route.
Try it this way:
SELECT * FROM database.dbo.table T WHERE T.deleted = 'n'
ORDER BY
CASE WHEN #direction='A' THEN T.id END ASC,
CASE WHEN #direction='D' THEN T.id END DESC
Source Article:
http://blog.sqlauthority.com/2007/07/17/sql-server-case-statement-in-order-by-clause-order-by-using-variable/
Another option that you might have, depending on the data type of your field, if nulls are NOT allowed, would be to do something like this.
SELECT *
FROM database.dbo.table T
WHERE T.deleted = 'n'
AND fieldf1 = COALESCE(#searchf1, fieldf1)
AND fieldr1 = COALESCE(#searchr1, fieldr1)
--ETC
ORDER BY fieldf1
This way you are not using dynamic SQL and it is fairly readable, just have the variable be null when you are looking to omit the data.
NOTE: As I mentioned this route will NOT work if any of the COALESCE columns contain null values.
Related
** edited to invert logic in EXISTS test
I need to select a field from a table that may not exist. I also need to do this in a sub query.
There is a code release coming from engineering that will add a table to a database I scrape for reporting. If the table exists, select the field. If it does not, give a generic failure.
** Yes, there are tons of examples on how to use EXISTS. This goes outside of those as it deals with something that may not exist.
The new code release will rollout to several sites, some may not have the table immediately where others will. My stuff has to ready for Day Zero - aka: rolled out before hand (day -1?).
Here is my code (tables and columns renamed) that I am trying to make work in SQL Server Management Studio (SQL 2016) (SSMS 17.4)
select 'Cern' as SiteName,
(
select
case when exists (select 1 from sys.tables where name like 'ProtonAcceleratorSecurity')
then
(select top 1 isnull(SecurityID,'Not on file') from [ProtonAcceleratorSecurity] with (nolock) )
else
(select 'No Security Table Found')
end
as SecurityID
It looks contrived, but it is very near what I am trying to do, using that exact query (but nothing to do with security...)
The goal is to get back the site name, and the first security ID (at random - doesn't matter - no consistency required) - but only if the table actually exists.
The problem is that SSMS throws an error telling me that the table is an invalid object which I already know.
"Invalid object name 'ProtonAcceleratorSecurity' "
Final answer from #TabAlleman, with a minor correction:
SET #sql = '
select ''Cern'' as SiteName, ' +
( select
case when exists (select 1 from sys.tables where name like 'ProtonAcceleratorSecurity')
then
'(select top 1 isnull(SecurityID,''Not on file'') from [ProtonAcceleratorSecurity] with (nolock) )'
else
'''No Security Table Found'''
end )
+ ' as SecurityID ';
EXEC (#sql); ```
The reason for your error is that SQL Server will parse and compile your entire statement before it executes it. So your query cannot contain references to invalid objects, even though the query itself checks to make sure the object is valid before referencing it. The parser isn't smart enough to know that the subquery will not be called if the object doesn't exist, so it will prevent you from running the query at all.
One way you can trick the parser is through dynamic sql, which doesn't get pre-parsed:
DECLARE #sql varchar(max);
SET #sql = '
select ''Cern'' as SiteName, '
+
select
case when exists (select 1 from sys.tables where name like 'ProtonAcceleratorSecurity')
then
'(select top 1 isnull(SecurityID,''Not on file'') from [ProtonAcceleratorSecurity] with (nolock) )'
else
'''No Security Table Found'''
end
+
' as SecurityID
';
EXEC (#sql);
EDIT because I'm not testing as I go, but just to make the idea clear:
You need to check for the existence of the table in the outer query, but then only construct a dynamic query that uses the table if it exists. To make it really simple, ignoring your original CASE structure, you want to logically do this:
IF EXISTS({SELECT query to test for MyTable})
#SQL = 'query that references MyTable';
ELSE
#SQL = 'query that doesn't reference MyTable';
EXECUTE (#SQL);
PS: I think I just fixed the syntax in my first example.
I don't think there is any way to do this in a single query. One suggestion is to hide this in a view:
IF OBJECT_ID('ProtonAcceleratorSecurity') IS NULL
BEGIN
CREATE VIEW ProtonAcceleratorSecurity
SELECT 'Not on file' as SecurityID
END;
Then your query can work simply as:
select 'Cern' as SiteName,
(select top 1 coalesce(SecurityID, 'Not on file')
from ProtonAcceleratorSecurity
) as SecurityID
You are getting the problem because the table must exist when the statement is executed.
If you divide it into two statements then there is no problem. The one referencing the non existent table is marked for deferred compilation and as it is never executed the full compilation is never needed and no error.
IF OBJECT_ID('ProtonAcceleratorSecurity', 'U') IS NULL
SELECT 'Cern' AS SiteName,
'No Security Table Found' AS SecurityID
ELSE
SELECT TOP 1 'Cern' AS SiteName,
isnull(SecurityID, 'Not on file') AS SecurityID
FROM [ProtonAcceleratorSecurity] WITH (nolock)
ORDER BY ....
Is it possible to test for a column before selecting it within a select statement?
This may be rough for me to explain, I have actually had to teach myself dynamic SQL over the past 4 months. I am using a dynamically generated parameter (#TableName) to store individual tables within a loop (apologize for the vagueness, but the details aren't relevant).
I then want to be able to be able to conditionally select a column from the table (I will not know if each table has certain columns). I have figured out how to check for a column outside of a select statement...
SET #SQLQuery2 = 'Select #OPFolderIDColumnCheck = Column_Name From INFORMATION_SCHEMA.COLUMNS Where Table_Name = #TABLENAME And Column_Name = ''OP__FolderID'''
SET #ParameterDefinition2 = N'#TABLENAME VARCHAR(100), #OPFolderIDColumnCheck VARCHAR(100) OUTPUT'
EXECUTE SP_EXECUTESQL #SQLQuery2, #ParameterDefinition2, #TABLENAME, #OPFolderIDColumnCheck OUTPUT
IF #OPFolderIDColumnCheck IS NULL
BEGIN
SET #OP__FOLDERID = NULL
END
ELSE
IF #OPFolderIDColumnCheck IS NOT NULL
BEGIN
...etc
but id like to be able to do it inside of a select statement. Is there a way to check and see if OP__FOLDERID exists in the table?
Id like to be able to do something like this:
SELECT IF 'OP__FOLDERID' EXISTS IN [TABLE] THEN 'OP__FOLDERID' FROM [TABLE]
Thank you for any help or direction you can offer.
I'm afraid there isn't any direct way to do this within a SELECT statement at all. You can determine if a column exists in a table, however, and construct your dynamic SQL accordingly. To do this, use something like this:
IF COL_LENGTH('schemaName.tableName', 'columnName') IS NOT NULL
BEGIN
-- Column Exists
END
You could then set a variable as a flag, and the code to construct the dynamic SQL would construct the expression with/without the column, as desired. Another approach would be to use a string value, and set it to the column name if it is present (perhaps with a prefix or suffix comma, as appropriate to the expression). This would allow you to save writing conditionals in the expression building, and would be particularly helpful where you have more than one or two of these maybe-columns in a dynamic expression.
I have a very complicated stored procedure that repeats a very complicated query with different where clauses based on certain values passed in. The stored procedure takes up over 500 lines of code, with the common part of the query taking up just over 100 lines. That common part is repeated 3 times.
I originally thought to use CTE (Common Table Expressions) except in T-SQL you can't define the common part, do your IF statement and then apply the WHERE clause. That's essentially what I need.
As a workaround I created a view for the common code, but it's only used in one stored procedure.
Is there any way to do this without creating a full view or temp tables?
Ideally I would like to do something like this:
WITH SummaryCTE (col1, col2, col3...)
AS
(
SELECT concat("Pending Attachments - ", ifnull(countCol1, 0)) col1
-- all the rest of the stuff
FROM x as y
LEFT JOIN attachments z on z.attachmentId = x.attachmentId
-- and more complicated stuff
)
IF (#originatorId = #userId)
BEGIN
SELECT * FROM SummaryCTE
WHERE
-- handle this security case
END
ELSE IF (#anotherCondition = 1)
BEGIN
SELECT * FROM SummaryCTE
WHERE
-- a different where clause
END
ELSE
BEGIN
SELECT * FROM SummaryCTE
WHERE
-- the generic case
END
Hopefully the pseudo code gives you an idea of what I would like. Right now my workaround is to create a view for the contents of what I defined SummaryCTE as, and then handle the IF/ELSE IF/ELSE clause. Executing this structure will throw an error at the first IF statement because the next command is supposed to be a SELECT instead. At least in T-SQL.
Maybe this doesn't exist in any other way, but I wanted to know for sure.
Well, aside from the temp tables and views that you've identified, you could go with dynamic SQL to build the code then execute it. This keeps you from having to repeat code, but makes it a bit hard to just deal with. Like this:
declare #sql varchar(max) = 'with myCTE (col1, col2) as ( select * from myTable) select * from myCTE'
if (#myVar = 1)
begin
#sql = #sql + ' where col1 = 2'
end
else if (#myVar = 2)
begin
#sql = #sql + ' where col2 = 4'
end
-- ...
exec #sql
Another option would be to incorporate your different where clauses into the original query.
WITH SummaryCTE (col1, col2, col3...)
AS
(
SELECT concat("Pending Attachments - ", ifnull(countCol1, 0)) col1
-- all the rest of the stuff
FROM x as y
LEFT JOIN attachments z on z.attachmentId = x.attachmentId
-- and more complicated stuff
)
select *
from SummaryCTE
where
(
-- this was your first if
#originatorId = #userId
and ( whatever you do for your security case )
)
or
(
-- this was your second branch
#anotherCondition = 1
and ( handle another case here )
)
or
-- etc. etc.
This eliminates the if/else chain but makes the query more complicated. It also can cause some bad cached query plans because of parameter sniffing, but that may not matter much depending on your data. Test that before making a decision. (You can also add optimizer hints to not cache the query plan. You won't get a bad one, but you also take a hit on every execution to create the query plan again. Test to find out, don't guess. Also, a solution with a view and the if/else chain will suffer from the same parameter sniffing/cached query plan problem.)
I have a statement like this below in one of my big stored proc, and I am wondering if this is valid to write this way.
SELECT #PVDate = pv.Date,
#PVdMBeginDate = dbo.fPVoidDate(pv.myID)
FROM PVMeter pv (NOLOCK)
WHERE pv.PR_ID = #PR_ID
AND #VCommen BETWEEN pv.PVDMDate AND dbo.fPVoidDate(pv.myID)
Now, here, my question is, #VCommen is a declared date variable with a value set on it. It is not at all a column of PVMeter, while PVDMDate is a column in PVMeter and fpVoidDate returns datetime
While I debug SP, I do not see the value on #PVDate and #PVDMBeginDate
The original query is equivalent to:
SELECT
#PVDate = pv.Date,
#PVdMBeginDate = dbo.fPVoidDate(pv.myID)
FROM PVMeter pv (NOLOCK)
WHERE
pv.PR_ID = #PR_ID
AND pv.PVDMDate <= #VCommen
AND #VCommen <= dbo.fPVoidDate(pv.myID)
This style should be more familiar. In general, you can put any expression in the WHERE clause, it can be made of variables or constants without referring to table columns at all.
Examples
Classic example: WHERE 1=1 ... when the query text is generated dynamically. It is easy to add as many expressions as needed in no particular order and prepend all of them with AND.
DECLARE #VarSQL nvarchar(max);
SET #VarSQL = 'SELECT ... FROM ... WHERE 1=1 ';
IF ... THEN SET #VarSQL = #VarSQL + ' AND expression1';
IF ... THEN SET #VarSQL = #VarSQL + ' AND expression2';
IF ... THEN SET #VarSQL = #VarSQL + ' AND expression3';
EXEC #VarSQL;
Thus you don't need to have complex logic determining whether you need to add an AND before each expression or not.
Another example.
You have a stored procedure with parameter #ParamID int.
You have a complex query in the procedure that usually returns many rows and one column of the result set is some unique ID.
SELECT ID, ...
FROM ...
WHERE
expression1
AND expression2
AND expression3
...
You want to return all rows if #ParamID is NULL and only one row with the given ID if #ParamID is not NULL. I personally use this approach. When I open the screen with the results of a query for the first time I want to show all rows to the user, so I pass NULL as a parameter. Then user makes changes to a selected row, which is done through a separate UPDATE statement. Then I want to refresh results that user sees on the screen. I know ID of the row that was just changed, so I need to requery just this row, so I pass this ID to procedure and fetch only one row instead of the whole table again.
The final query would look like this:
SELECT ID, ...
FROM ...
WHERE
(#ParamID IS NULL OR ID = #ParamID)
AND expression1
AND expression2
AND expression3
...
OPTION (RECOMPILE);
Thus I don't have to repeat the complex code of the query twice.
I want to build a single select stored procedure for SQL 2005 that is universal for any select query on that table.
**Columns**
LocationServiceID
LocationID
LocationServiceTypeID
ServiceName
ServiceCode
FlagActive
For this table I may need to select by LocationServiceID, or LocationID, or LocationServiceTypeID or ServiceName or a combination of the above.
I'd rather not have a separate stored procedure for each of them.
I assume the best way to do it would be to build the 'WHERE' statement on NOT NULL. Something like
SELECT * FROM LocationServiceType WHERE
IF #LocationID IS NOT NULL (LocationID = #LocationID)
IF #LocationServiceID IS NOT NULL (LocationServiceID = #LocationServiceID)
IF #LocationServiceTypeID IS NOT NULL (LocationServiceTypeID = #LocationServiceTypeID)
IF #ServiceName IS NOT NULL (ServiceName = #ServiceName)
IF #ServiceCode IS NOT NULL (ServiceCode = #ServiceCode)
IF #FlagActive IS NOT NULL (FlagActive = #FlagActive)
Does that make sense?
here is the most extensive article I've ever seen on the subject:
Dynamic Search Conditions in T-SQL by Erland Sommarskog
here is an outline of the article:
Introduction
The Case Study: Searching Orders
The Northgale Database
Dynamic SQL
Introduction
Using sp_executesql
Using the CLR
Using EXEC()
When Caching Is Not Really What You Want
Static SQL
Introduction
x = #x OR #x IS NULL
Using IF statements
Umachandar's Bag of Tricks
Using Temp Tables
x = #x AND #x IS NOT NULL
Handling Complex Conditions
Hybrid Solutions – Using both Static and Dynamic SQL
Using Views
Using Inline Table Functions
Conclusion
Feedback and Acknowledgements
Revision History
First of all, your code will not work. It should look like this:
SELECT * FROM LocationServiceType WHERE
(#LocationID IS NULL OR (LocationID = #LocationID)
... -- all other fields here
This is totally valid and known as 'all-in-one query'. But from a performance point of view this is not a perfect solution as soon as you don't allow SQL Server to select optimal plan. You can see more details here.
Bottom line: if your top priority is 'single SP', then use this approach. In case you care about the performance, look for a different solution.
SELECT *
FROM LocationServiceType
WHERE LocationServiceID = ISNULL(#LocationServiceID,LocationServiceID)
AND LocationID = ISNULL(#LocationID,LocationID)
AND LocationServiceTypeID = ISNULL(#LocationServiceTypeID,LocationServiceTypeID)
AND ServiceName = ISNULL(#ServiceName,ServiceName)
AND ServiceCode = ISNULL(#ServiceCode,ServiceCode)
AND FlagActive = ISNULL(#FlagActive,FlagActive)
If a null value is sent in it will cancel out that line of the where clause, otherwise it will return rows that match the value sent in.
What I've always done is is set the incoming parameters to null if should be ignored in query
then check variable for null first, so if variable is null condition short circuits and filter is not applied. If variable has value then 'or' causes filter to be used. Has worked for me so far.
SET #LocationID = NULLIF(#LocationID, 0)
SET #LocationServiceID = NULLIF(#LocationServiceID, 0)
SET #LocationServiceTypeID = NULLIF(#LocationServiceTypeID, 0)
SELECT * FROM LocationServiceType WHERE
(#LocationID IS NULL OR LocationID = #LocationID)
AND (#LocationServiceID IS NULL OR LocationServiceID = #LocationServiceID)
AND (#LocationServiceTypeID IS NULL OR #LocationServiceTypeID = #LocationServiceTypeID)
etc...