Parametrize the WHERE clause? - sql

I'm need to write a stored procedure for SQL Server 2008 for performing a large select query and I need it to filter results with specifying filtering type via procedure's parameters. I found some solutions like this:
create table Foo(
id bigint, code char, name nvarchar(max))
go
insert into Foo values
(1,'a','aaa'),
(2,'b','bbb'),
(3,'c','ccc')
go
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
select * from Foo as f
where case #FilterType
when 'by_id' then f.id
when 'by_code' then f.code
when 'by_name' then f.name end
=
case #FilterType
when 'by_id' then cast(#FilterValue as bigint)
when 'by_code' then cast(#FilterValue as char)
when 'by_name' then #FilterValue end
end
go
exec Bar 'by_id', '1';
exec Bar 'by_code', 'b';
exec Bar 'by_name', 'ccc';
I'm finding that this approach doesn't work. It's possible to cast all the columns to nvarchar(max) and compare them as strings, but I think it will cause performance degradation.
Is it possible to parametrize the where clause in stored procedure without using constructs like EXEC sp_executesql?

This may become a little more long winded, for large filter requirements, but I think it probably more performant/easier to read/maintain:
create procedure Bar
#Id int,
#Code nvarchar,
#name nvarchar
begin
select * from Foo as f
where (#id = -1 or f.ID = #id)
and (#Code = '' or f.Code = #Code)
and (#Name = '' or f.Name = #Name)
end
go
exec Bar 1, '', ''
exec Bar -1, 'code', ''
exec Bar -1, '', 'name'
This also allows you to filter by more than one item at the same time.

Try this
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
select * from Foo as f
where
(#FilterType ='by_id' and f.id =cast(#FilterValue as bigint) )
OR
(#FilterType ='by_code' and f.code =cast(#FilterValue as char)
OR
(#FilterType ='by_name' and f.name =#FilterValue
end
go

declare #field varchar(20)
declare #value varchar(100)
set #field = 'customerid'
set #value = 'ALFKI'
set #field = 'Country'
set #value = 'Germany'
set #field = 'ContactTitle'
set #value = 'Owner'
select * from customers
where (customerid = (case when #field = 'customerid' then #value else customerid end)
and ContactTitle = (case when #field = 'ContactTitle' then #value else ContactTitle end)
and country = (case when #field = 'Country' then #value else country end))
Example is adapted for Northwind database.
Hope this helps.
EDIT: Comment any 2 out of the 3 values for #field and #value for above.

If you don't have many filtering criteria, you could consider creating delegate functions and dispatching the request to the appropriate one. E.g.,
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
case #FilterType
when 'by_id' then FilterById(#FilterValue)
when 'by_code' then FilterByCode(#FilterValue)
when 'by_name' then FilterByName(#FilterValue)
end
end
go

Related

Stored procedure table-valued variable without aliases in query string must declare scalar variable

I will pass a table-valued input parameter into a stored procedure, and also a variable that contains query string, so I made my sproc like this.
CREATE PROCEDURE [dbo].[SP_SelectData_View]
(
#Sort VARCHAR(MAX),
#CONDITION VARCHAR(MAX) = ''
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
DECLARE #STRQUERY NVARCHAR(MAX)
IF #CONDITION IS NOT NULL AND #CONDITION != ''
BEGIN
SET #CONDITION = 'WHERE ' + #CONDITION
END
ELSE
BEGIN
SET #CONDITION = ''
END
IF #Sort IS NULL OR #Sort = ''
BEGIN
SET #Sort = 'Id Desc'
END
BEGIN
SET #STRQUERY = 'SELECT A.*
FROM ' + #Values + ' as FlowStatus'
JOIN Tbl_A as A
ON A.status = FlowStatus.StatusNowId AND B.flow = FlowStatus.FlowNowId
' + #CONDITION + '
Order By ' + #Sort
EXEC(#STRQUERY)
END
END
But in the code above, I got an error
must declare scalar variable #Values
I've searched for it and I think it is because the aliases is not detected because it's inside a string. But if I didn't put it in a string query, the #condition and #sort variable will be error. Is there a solution where I can do both calling the table-valued variable and query string variable together?
There are several things wrong with the approach you currently have, as I and others have commented, Brent Ozar has a good reference on dynamic SQL https://www.brentozar.com/sql/dynamic/
I would say don't pass in some SQL, construct it in the stored proc; passing in parameters such as name which is used in the where, hence I have put a full working example. This also shows how to pass the user defined table type into the stored proc and then also pass it into the dynamic SQL.
I hope this is a good enough example of the techniques, I had a bit of time so thought I would try and help as much as possible :)
/*
--------------------------------------------
Create a test table to run the stored proc against
*/
IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'MyTestTable'))
BEGIN
PRINT 'Creating table MyTestTable'
CREATE TABLE [dbo].[MyTestTable](
Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] NVARCHAR(50) NOT NULL
)
INSERT INTO dbo.MyTestTable ([Name])
VALUES ('Andrew'),
('Bob'),
('john')
-- SELECT * FROM MyTestTable
END
GO
/*
--------------------------------------------
Create the table type that we pass into the store proc
*/
IF NOT EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = 'FlowStatus')
BEGIN
PRINT 'Creating type [dbo].[FlowStatus]'
CREATE TYPE [dbo].FlowStatus AS TABLE (
MyId BIGINT PRIMARY KEY,
SomeText NVARCHAR(200)
)
END
GO
/*
--------------------------------------------
Create the stored proc with the User Defined table type
*/
CREATE OR ALTER PROCEDURE [dbo].[MyStoredProc]
(
#SortBy VARCHAR(50),
#SearchName VARCHAR(50),
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
-- As your SQL gets more complex it is an idea to create seperate parts of the SQL
DECLARE #SqlToExecute NVARCHAR(MAX)
-- The primary data you want to get
SET #SqlToExecute = N'
SELECT T.Id, T.[Name], V.SomeText
FROM MyTestTable AS T
LEFT JOIN #Values AS V ON V.MyId = T.Id
WHERE 1 = 1' -- We do this so that we can have many AND statements which could be expanded upon
IF #SearchName IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
AND T.[Name] LIKE ''%' + #SearchName + ''''
END
IF #SortBy IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
ORDER BY ' +
CASE WHEN #SortBy LIKE 'Name%' THEN N'T.[Name]'
ELSE N'T.[Id]'
END
END
-- Print out the script that will be run, useful for debugging you code
PRINT #SqlToExecute
EXEC sp_executesql #SqlToExecute,
N'#Values dbo.FlowStatus READONLY', #Values
END
GO
/*
--------------------------------------------
Now lets test it
-- Test Andrew
*/
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Andrew', #SortBy = 'Name', #Values = #flowStatusType
GO
-- Test Bob
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Bob', #SortBy = 'Name', #Values = #flowStatusType
GO
Its also worth noting that if you can just join on the #Values without needing dynamic SQL then that is sure to be less work.

Writing a Stored procedure to search for matching values in columns with optional Input parameters

Requirement: To write stored Procedure(s) such that the values passed in stored procedures are matched against the values in columns in the table and then arranged with highest to lowest matching number of attributes.Then are inserted in a dynamically created temporary table inside the stored procedure.
Problem:
I have say 15-20 attributes that are matched against to confirm the suggestions made in response to record search. Basically, There is a table that stores Patients information and multiple parameters may be passed into the stored procedure to search through so that a Temporary table is created that suggests records in decreasing order of matching attributes.
To frame the basic structure, I tried with 3 attributes and respective stored procedures to match them which in turn are collectively called from a calling procedure based on the input parameter list which in turn creates the required temporary table.
Here is the SQL code(Just to give the gist of what I have tried so far):
But as a matter of fact it is, I realize this is way too naive to be used in a real time application which may require 80-90% of accuracy.So, what exactly can replace this technique for better efficiency?
Create Procedure NameMatch
(
#Name nvarchar(20),
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name = #Name)
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name =' + #Name
set #temp = 0.1*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
Create Procedure AgeMatch
(
#Name nvarchar(20),
#Age int,
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name =#Name and Age = + #Age)
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name = ' + #Name + ' and Age = '+ #Age
set #temp = 0.2*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
Create Procedure Nationality
(
#Name nvarchar(20),
#Age int,
#Nation nvarchar(10),
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name = #Name and Age = #Age and Nationality = #Nation )
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name = ' + #Name + ' and Age = '+ #Age + ' and Nationality = ' + #Nation
set #temp = 0.3*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
create procedure CallingProcedure
(
#Name nvarchar(20),
#Age int = null,
#Nation nvarchar(10)= null
)
As
declare #PercentMatch nvarchar(4)
Begin
create table #results(PatientName nvarchar(30), PercentMatch nvarchar(4))
if(#Nation IS NOT NULL)
Insert into #results exec Nationality #Nation, #Name output, #PercentMatch output
else if(#Age is not Null)
Insert into #results exec AgeMatch #Age, #Name output, #PercentMatch output
else
Insert into #results exec NameMatch #Name, #Name output, #PercentMatch output
End
Setting aside nuances of stored procedure syntax, given parameters 1-n that if not null should match columns 1-n and the results sorted by highest number of matches first, a dynamic query is not needed - plain SQL can do it.
select *
from patient
where #param1 is null or column1 = #param1
or #param2 is null or column2 = #param2
...
or #paramN is null or columnN = #paramN
order by if(column1 = #param1, 1, 0)
+ if(column2 = #param2, 1, 0)
...
+ if(columnN = #paramN, 1, 0) desc

Put Where Clause in a parameter

I would like to use a where clause in a parameter. I already tried the following code but it doesnt work. the last line us red underlined, i reckon that i have to bind the parameter to the select command somehow. Would be great if anyone could help me with it.
Begin
declare #name varchar(MAX)
declare #x int
Set #x = 1
If #x = 1
BEGIN
SET #name = 'WHERE Username = Frank'
END
ELSE
BEGIN
SET #name = ''
END
END
now use it in:
SELECT * FROM dbo.person #name
Here is a typical way to have optional parameters in a query:
declare #UserName varchar(255) = 'Frank'
select *
from person p
where (#UserName is null or UserName = #UserName)
If you set the variable to NULL then all users are chosen. If you set it to a value, then only that user is chosen.
Looks like you want an optional Stored Procedure parameter to either pull rows with the parameter value or pull all rows:
CREATE PROCEDURE foo
#name AS VARCHAR(MAX) = NULL
AS
BEGIN
SELECT * FROM dbo.person WHERE Username = #name OR #name IS NULL
END
GO

SQL SP: Dynamic Query

I am trying get a col and value as a function parameter and planning to use them in a query.
Unfortunately, my value for #Col is being treated like a string and not a column identifier.
ie, if I specify name as a value for the #Col parameter, it will be treated like 'name' instead of name and hence, the call to the function always returns NULL as a result
Have you came across similar situations? Could you please recommand me a better way to deal with this situation?
Here is my Function for your reference:
CREATE FUNCTION [dbo].[FN_FindIdBy]
(
#Col NVARCHAR(255),
#Value NVARCHAR(255)
)
RETURNS INT
AS
BEGIN
DECLARE #Id INT = NULL
SET #Id = (SELECT ID FROM dbo.MYWORK WHERE (CONVERT(NVARCHAR(255), #Col) = #Value))
IF #Id IS NOT NULL RETURN #Id
RETURN NULL
END
Thanks a lot for looking into this.
The following works, but you have to use it as a procedure and create dynamic sql.
create table MYWORK (ID int identity, Name nvarchar(255))
insert into MYWORK(Name)
select 'a'
union select 'b'
union select 'c'
union select 'd'
union select 'e'
union select 'f'
CREATE procedure [dbo].[EPG_FN_FindIdBy]
#Col NVARCHAR(255),
#Value NVARCHAR(255)
AS
BEGIN
DECLARE #Id nvarchar(255)
, #ParmDefinition nvarchar(255)
, #sql nvarchar(max)
set #sql = N'SELECT #IdOUT = ID FROM dbo.MYWORK WHERE '+ #Col +' = ''' + #Value + ''''
set #ParmDefinition = N'#IdOUT nvarchar(255) OUTPUT'
PRINT #sql
EXECUTE sp_executesql #sql,#ParmDefinition, #IdOUT = #Id OUTPUT
SELECT #Id as ID
END
Run this and it'll return the matching row
Exec dbo.EPG_FN_FindIdBy #Col = 'Name', #Value = 'a'
And for a NULL
Exec dbo.EPG_FN_FindIdBy #Col = 'Name', #Value = 'g'
Yeah, there's almost always a better way to query than using dynamic SQL.
Check out this usage of the CASE operator.
SELECT id
FROM dbo.MYWORK
WHERE CASE #Col
WHEN 'a' THEN [a]
WHEN 'b' THEN [b]
WHEN 'c' THEN [c]
END = #Value
Where the table has columns [a], [b] and [c].

sql server procedure error

CREATE PROCEDURE USP_SEARCH_HOTELS
(
#Text varchar(50),
#Type varchar(40)
)
AS
BEGIN
Declare #Query VARCHAR(60)
IF #Type = 'By Country'
BEGIN
SET #Query = 'Hotel.countryName like '+ #Text+'%'
END
ELSE IF #Type = 'By State'
BEGIN
SET #Query = 'HOTEL.stateName like '+ #Text+'%'
END
ELSE IF #Type='By Property Name'
BEGIN
SET #Query='hotel.propertyname like'+ #Text+'%'
End
ELSE IF #Type='By Rating'
BEGIN
SET #Query='hotel.starRating='+ Cast(#Text as INT)
END
ELSE IF #Type='By City'
BEGIN
SET #Query='hotel.cityName like '+ #Text+'%'
END
begin
select * from hotel,tbl_cust_info
where
hotel.agentID=Tbl_Cust_Info.Cust_ID
and
(#Query)
end
END
WHAT IS THE ERROR IN THIS PROCEDURE PLEASE HELP.
DECLARE #Final nvarchar(1000) -- Separate partial and final
DECLARE #Partial nvarchar(100) -- let's you maintain and debug better
SET #Final = 'select * from hotel
join tbl_cust_info
on hotel.agentID=Tbl_Cust_Info.Cust_ID
where' + #Partial
Assumiung that you are invioking this via .NET run a Regexp on the Text to eliminate all chars that are not letters or space. Like [!##$%^&*()?;:'"\|].
Consider rewriting as 5 sprocs (HotelsByCountry, HotelsByState, HotelsByCity, HotelsByName, HotelsByRating). That will increase you perf and let you do orderby and paging (like Row_number() OVER(order by StartDate)). It will also make them totally safe.