Passing multiple values for one SQL parameter - sql-server-2005

I have a CheckBoxList where users can select multiple items from the list. I then need to be able to pass these values to my Stored Procedure so they can be used in a WHERE condition like:
WHERE ID IN (1,2,3)
I tried doing this so that its a nvarchar parameter and i pass the string 1,2,3 with:
WHERE ID IN (#IDs)
But this returned the following error:
Conversion failed when converting the nvarchar value '1,2,3' to data type int
Any help would be much appreciated!

There's a few ways of doing it.
You could pass in the parameter as an XML blob like this example:
CREATE PROCEDURE [dbo].[uspGetCustomersXML]
#CustomerIDs XML
AS
BEGIN
SELECT c.ID, c.Name
FROM [dbo].[Customer] c
JOIN #CustomerIDs.nodes('IDList/ID') AS x(Item) ON c.ID = Item.value('.', 'int' )
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersXML] '<IDList><ID>1</ID><ID>10</ID><ID>100</ID></IDList>'
Or pass in the values as CSV and use a split function to split the values out into a table variable (there's a lot of split functions out there, quick search will throw one up).
CREATE PROCEDURE [dbo].[uspGetCustomersCSV]
#CustomerIDs VARCHAR(8000)
AS
BEGIN
SELECT c.Id, c.Name
FROM [dbo].[Customer] c
JOIN dbo.fnSplit(#CustomerIDs, ',') t ON c.Id = t.item
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersCSV] '1,10,100'
If you were using SQL 2008 or later, you could have used Table Valued Parameters which allow you to pass a TABLE variable in as a parameter. I blogged about these 3 approaches a while back, with a quick performance comparison.

alter procedure c2
(#i varchar(5))
as
begin
declare #sq nvarchar(4000)
set #sq= 'select * from test where id in (<has_i>) '
SET #sq= REPLACE(#sq, '<has_i>', #i)
EXECUTE sp_executesql #sq
end
exec c2 '1,3'

I did find a solution for a similar problem.
It is used for a data driven subscription, but can be easily altered for use in a parameter.
check my blog post here with a detailed description
If you are having problem converting it to a stored procedure call, just let me know.

Related

Make value of a parameter the column name without dynamic SQL

Is there a way to name a column the value of what's in a parameter, without using dynamic SQL?
I need to somehow output the value of what is in the #input parameter as the name of the column in the SQL statement. I need to avoid using dynamic SQL.
DECLARE #input VARCHAR(10)=' Person ';
SELECT count(*) AS #input + ' Open Data'
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid
One ugly way, without Dynamic-SQL is using temporary table and rename column:
DECLARE #input VARCHAR(10) = ' Person ';
DECLARE #new_name VARCHAR(100) = #input + ' Open Data';
SELECT [rename_me] = COUNT(*)
INTO #temp
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid;
EXEC tempdb..sp_rename '#temp.rename_me', #new_name, 'COLUMN';
SELECT *
FROM #temp;
You need to use dynamic SQL:
DECLARE #input VARCHAR(10) = ' Person ';
DECLARE #sql NVARCHAR(MAX) = '
SELECT count(*) AS [#Input Open Data]
FROM #Accounts a JOIN
dbo.FileFeed t
On t.ID = a.AccountID
GROUP BY a.accountid';
SET #sql = REPLACE(#sql, '#Input', #Input);
exec sp_executesql #sql;
However, I don't really think this is a good idea. If you need to rename a column, do it in the application code.
No. Databases use a process similar to compiling to turn your query into an execution plan. Part of this process involves determining whether the user running the query has permissions to access the tables and columns used by the query. If those tables and columns are not determined until execution time, the compilation step can't finish. It seems strange, but the same thing applies to the result set.
Dynamic SQL (which creates a new query, with a new compilation step where the tables and column names are known up front) will be the only way to accomplish this.
The answer is "No". Dynamic SQL, by definition, means that the code can alter itself. The only alternative to Dynamic SQL is Hard-Coded SQL, which is when you specifically write every piece of it yourself.
You're asking if you can change a field name based on a variable, which can only be done with Dynamic SQL.
Unless you rename the columns in the application, this is the way i would do it.
DECLARE #input VARCHAR(10)=' Person ';
Declare #sql varchar(max);
set #sql'SELECT count(*) as ['+ #input +' Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid';
EXEC(#sql);
IF it is the case that the values of what can be in the #input variable / parameter are limited (i.e. not open-ended user input or something of that nature), then you can do this without DynamicSQL. You just need to have multiple copies of the query in this Stored Procedure, each being exactly the same except for the alias of the column, and pick the one you want based on the value of the variable. For example:
DECLARE #input VARCHAR(10)=' Person ';
IF (#input = 'Person')
BEGIN
SELECT COUNT(*) AS [Person Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t
ON t.ID = a.AccountID
GROUP BY a.accountid;
END;
IF (#input = 'Account')
BEGIN
SELECT COUNT(*) AS [Account Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t
ON t.ID = a.AccountID
GROUP BY a.accountid;
END;
...
If you've got a later version of SQL Server then you might look at this option:
EXEC <stored_procedure> WITH RESULT SETS (...)
I imagine you're already using a stored procedure. This way you'd at least be able to keep the main logic completely static in a second procedure and just interface to a procedure with the dynamic exec call to accomplish the renaming of column(s).

Using single quote in replace?

I am passing the following string into my Stored Procedure
'1,2,3,4,5,6'
I am trying to replace the , with ',' so that my string is translated to:
'1','2','3','4','5','6'
Is this possible? Here is my attempt:
Declare #Var2 varchar(250)
SET #Var2 = Replace(#Var1, ',', "','")
If you want to pass a list of values as a parameter use a table valued parameter.
The first step would be to create your type (I tend to go for a generic name so they can be reused):
CREATE TYPE dbo.ListOfInt AS TABLE (Value INT);
Then you can pass as a parameter fairly easily:
DECLARE #T dbo.ListOfInt;
INSERT #T (Value) VALUES (1), (2), (3);
DECLARE #SQL NVARCHAR(MAX) = 'SELECT * FROM dbo.T WHERE ID IN (SELECT Value FROM #T)';
EXECUTE sp_executesql #sql, N'#T dbo.ListOfInt READONLY', #T;
Or create a procedure that takes the parameter:
CREATE PROCEDURE dbo.SomeProcName #IDs dbo.ListOfInt READONLY
AS
BEGIN
-- DO SOMETHING
END
If this isn't an option, then pretty much everything you ever need to know about splitting strings in SQL Server is covered in the article Split strings the right way – or the next best way by Aaron Bertrand. He has done lots of testing so you can pick the best approach for your needs.
It should be like below, you need to escape extra '
select replace(#Var1,',',''',''')
But doing that will result in something which you can't use in INLIST. So try like
select ''''+ replace('1,2,3,4,5,6',',',''',''') + ''''
Which will result in '1','2','3','4','5','6'

Iterate through XML variable in SQL Server

I have a XML variable in a stored procedure (SQL Server 2008), its sample value is
<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>
I have to take each category and insert into table as a separate record. How to iterate in XML and take individual node value?
If I want to call a stored procedure and send each category as input parameter, how we can do that? The stored procedure is legacy one, which accept only one category at at time. I am trying to do invoke procedure in this way.
loop fetch single category from xml variable.
invoke stored procedure with current category.
move to next category.
loop until list contain value.
Any help will be appreciated.
Something like this?
DECLARE #XmlVariable XML = '<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>'
INSERT INTO dbo.YourTargetTable(CategoryColumn)
SELECT
XTbl.Cats.value('.', 'varchar(50)')
FROM
#XmlVariable.nodes('/parent_node/category') AS XTbl(Cats)
Update: if you must use the old legacy stored procedure and cannot change it (that would be my preferred way of doing this), then you would have to do the row-by-agonizing-row (RBAR) looping yourself, e.g. by using a table variable:
-- declare temporary work table
DECLARE #RbarTable TABLE (CategoryName VARCHAR(50))
-- insert values into temporary work table
INSERT INTO #RbarTable(CategoryName)
SELECT
XTbl.Cats.value('.', 'varchar(50)')
FROM
#XmlVariable.nodes('/parent_node/category') AS XTbl(Cats)
-- declare a single category
DECLARE #CategoryNameToBeInserted VARCHAR(50)
-- get the first category
SELECT TOP 1 #CategoryNameToBeInserted = CategoryName FROM #RbarTable
-- as long as we have data
WHILE #CategoryNameToBeInserted IS NOT NULL
BEGIN
-- execute your stored procedure here.....
EXEC sp_executesql N'dbo.YourStoredProcedure #CategoryName',
N'#CategoryName VARCHAR(50)',
#CategoryName = #CategoryNameToBeInserted
-- delete the category we just inserted from the temporary work table
DELETE FROM #RbarTable WHERE CategoryName = #CategoryNameToBeInserted
-- see if we still have more categories to insert
SET #CategoryNameToBeInserted = NULL
SELECT TOP 1 #CategoryNameToBeInserted = CategoryName FROM #RbarTable ORDER BY CategoryName
END
With XML in SQL Server there's always more than one way to do it. Depending on the size of your XML doc and the number of times you're querying it, you could be best off using sp_xml_preparedocument which parses the document, gives you a handle to reference it, and then you can query it as many times and ways as you want to. Here's how you do that:
declare #xml xml = '
<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>'
declare #xml_handle int
exec sp_xml_preparedocument #xml_handle output, #xml
select value from openxml(#xml_handle, '/parent_node/category', 2) with (value varchar(100) 'text()') x
exec sp_xml_removedocument #xml_handle

Execute Stored Procedure for List of Parameters in SQL

I have an Stored Procedure that have an argument named Id:
CREATE PROCEDURE [TargetSp](
#Id [bigint]
)
AS
BEGIN
Update [ATable]
SET [AColumn] =
(
Select [ACalculatedValue] From [AnotherTable]
)
Where [ATable].[Member_Id] = #Id
END
So I need to use it for a list of Id's not for one Id like :
Exec [TargetSp]
#Id IN (Select [M].[Id] From [Member] AS [M] Where [M].[Title] = 'Example');
First: How can I Execute it for a list?
Second: Is there any Performance difference between I execute the sp many times or rewrite it in target script?
You could use a table-valued parameter (see http://msdn.microsoft.com/en-us/library/bb510489.aspx). Generally, if you send only one request to the server instead of a list of requests you will see a shorter execution time.
I normally pass in the information like that as XML, then you can use it just like it's a table... selecting, inserting, updating as necessary
DECLARE #IDS NVARCHAR(MAX), #IDOC INT
SET #IDS = N'<ROOT><ID>1</ID><ID>2<ID></ROOT>'
EXEC sp_xml_preparedocument #IDOC OUTPUT, #IDS
SELECT [ID] FROM OPENXML (#IDOC, '/ROOT/ID', 2) WITH ([ID] INT '.') AS XMLDOC
EXEC sp_xml_removedocument #IDOC
Similar to freefaller's example, but using xml type instead and inserting into a table variable #ParsedIds
DECLARE #IdXml XML = N'<root><id value="1"/><id value="2"/></root>'
DECLARE #ParsedIds TABLE (parsedId int not null)
INSERT INTO #ParsedIds (parsedId)
SELECT v.parsedId.value('#value', 'int')
FROM #IdXml.nodes('/root/id') as v(parsedId)
SELECT * FROM #ParsedIds
Interestingly I've worked on an large scale system with 1000's of users and we found that using this method out performed the table-valued parameter approach for small lists of id's (no more than say 5 id's). The table-valued parameter approach was faster for larger lists of Id's.
EDIT following edited question:
Looking at your example it looks like you want to update ATable based on the Title parameter. If you can you'd benefit from rewriting your stored procedure to instead except the title parameter.
create procedure [TargetSP](
#title varchar(50)
)
as
begin
update [ATable]
set [AColumn] =
(
select [ACalculatedValue] from [AnotherTable]
)
where [ATable].[Member_Id] in (select [M].[Id] from [Member] as [M] where [M].[Title] = #title);
end
Since you only care about all the rows with a title of 'Example', you shouldn't need to determine the list first and then tell SQL Server the list you want to update, since you can already identify those with a query. So why not do this instead (I'm guessing at some data types here):
ALTER PROCEDURE dbo.TargetSP
#title VARCHAR(255)
AS
BEGIN
SET NOCOUNT ON;
-- only do this once instead of as a subquery:
DECLARE #v VARCHAR(255) = (SELECT [ACalculatedValue] From [AnotherTable]);
UPDATE a
SET AColumn = #v
FROM dbo.ATable AS a
INNER JOIN dbo.Member AS m
ON a.Member_Id = m.Id
WHERE m.Title = #title;
END
GO
Now call it as:
EXEC dbo.TargetSP #title = 'Example';
DECLARE #VId BIGINT;
DECLARE [My_Cursor] CURSOR FAST_FORWARD READ_ONLY FOR
Select [M].[Id] From [Member] AS [M] Where [M].[Title] = 'Example'
OPEN [My_Cursor]
FETCH NEXT FROM [My_Cursor] INTO #VId
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC [TargetSp]
#Id = #VId
FETCH NEXT FROM [My_Cursor] INTO #VId
END
CLOSE [My_Cursor]
DEALLOCATE [My_Cursor];
GO
if the parameter is integer, you can only pass one value at a time.
Your options are:
call the proc several times, one for each parameter
Change the proc to accept a structure where you can pass more than
one id like a varchar where you pass a coma separated list of values
(not so good) or a table-value parameter
About the performance question, it would be faster to re-write the proc to iterate through a list of ids than call it several times, once per id, BUT unless you are dealing with a HUGE list of ids, I dont think you will see much of a difference

TSQL Statement IN

I am having a small problem with the IN SQL statement. I was just wondering if anyone could help me?
#Ids = "1,2,3,4,5"
SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (CONVERT(VARCHAR,#Ids))
This is coming back with the error below, I am sure this is pretty simple!
Conversion failed when converting the varchar value '1,' to data type int.
The SQL IN clause does not accept a single variable to represent a list of values -- no database does, without using dynamic SQL. Otherwise, you could use a Table Valued Function (SQL Server 2000+) to pull the values out of the list & return them as a table that you can join against.
Dynamic SQL example:
EXEC('SELECT *
FROM Nav
WHERE NavigationID IN ('+ #Ids +')')
I recommend reading The curse and blessings of dynamic SQL before using dynamic SQL on SQL Server.
Jason:
First create a function like this
Create FUNCTION [dbo].[ftDelimitedAsTable](#dlm char, #string varchar(8000))
RETURNS
--------------------------------------------------------------------------*/
/*------------------------------------------------------------------------
declare #dlm char, #string varchar(1000)
set #dlm=','; set #string='t1,t2,t3';
-- tHIS FUNCION RETUNRS IN THE ASCENDING ORDER
-- 19TH Apr 06
------------------------------------------------------------------------*/
--declare
#table_var TABLE
(id int identity(1,1),
r varchar(1000)
)
AS
BEGIN
declare #n int,#i int
set #n=dbo.fnCountChars(#dlm,#string)+1
SET #I =1
while #I <= #N
begin
insert #table_var
select dbo.fsDelimitedString(#dlm,#string,#i)
set #I= #I+1
end
if #n =1 insert #TABLE_VAR VALUES(#STRING)
delete from #table_var where r=''
return
END
And then
set quoted_identifier off
declare #ids varchar(max)
select #Ids = "1,2,3,4,5"
declare #nav table ( navigationid int identity(1,1),theother bigint)
insert #nav(theother) select 10 union select 11 union select 15
SELECT * FROM #Nav WHERE CONVERT(VARCHAR,NavigationID) IN (select id from dbo.ftDelimitedAsTable(',',#Ids))
select * from dbo.ftDelimitedAsTable(',',#Ids)
What you're doing is not possible with the SQL IN statement. You cannot pass a string to it and expect that string to be parsed. IN is for specific, hard-coded values.
There are two ways to do what you want to do here.
One is to create a 'dynamic sql' query and execute it, after substituting in your IN list.
DECLARE #query varchar(max);
SET #query = 'SELECT * FROM Nav WHERE CONVERT(VARCHAR,NavigationID) IN (' + #Ids + ')'
exec (#query)
This can have performance impacts and other complications. Generally I'd try to avoid it.
The other method is to use a User Defined Function (UDF) to split the string into its component parts and then query against that.
There's a post detailing how to create that function here
Once the function exists, it's trivial to join onto it
SELECT * FROM Nav
CROSS APPLY dbo.StringSplit(#Ids) a
WHERE a.s = CONVERT(varchar, Nav.NavigationId)
NB- the 'a.s' field reference is based on the linked function, which stores the split value in a column named 's'. This may differ based on the implementation of your string split function
This is nice because it uses a set based approach to the query rather than an IN subquery, but a CROSS JOIN may be a little complex for the moment, so if you want to maintain the IN syntax then the following should work:
SELECT * FROM Nav
WHERE Nav.NavigationId IN
(SELECT CONVERT(int, a.s) AS Value
FROM dbo.StringSplit(#Ids) a