This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Parameterizing an SQL IN clause?
I have a SQL function whereby I need to pass a list of IDs in, as a string, into:
WHERE ID IN (#MyList)
I have looked around and most of the answers are either where the SQL is built within C# and they loop through and call AddParameter, or the SQL is built dynamically.
My SQL function is fairly large and so building the query dynamically would be rather tedious.
Is there really no way to pass in a string of comma-separated values into the IN clause?
My variable being passed in is representing a list of integers so it would be:
"1,2,3,4,5,6,7" etc
Here is a slightly more efficient way to split a list of integers. First, create a numbers table, if you don't already have one. This will create a table with 100,000 unique integers (you may need more or less):
;WITH x AS
(
SELECT TOP (1000000) Number = ROW_NUMBER() OVER
(ORDER BY s1.[object_id])
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
ORDER BY s1.[object_id]
)
SELECT Number INTO dbo.Numbers FROM x;
CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);
Then a function:
CREATE FUNCTION [dbo].[SplitInts_Numbers]
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = CONVERT(INT, SUBSTRING(#List, Number,
CHARINDEX(#Delimiter, #List + #Delimiter, Number) - Number))
FROM dbo.Numbers
WHERE Number <= CONVERT(INT, LEN(#List))
AND SUBSTRING(#Delimiter + #List, Number, 1) = #Delimiter
);
You can compare the performance to an iterative approach here:
http://sqlfiddle.com/#!3/960d2/1
To avoid the numbers table, you can also try an XML-based version of the function - it is more compact but less efficient:
CREATE FUNCTION [dbo].[SplitInts_XML]
(
#List VARCHAR(MAX),
#Delimiter CHAR(1)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN ( SELECT Item = CONVERT(INT, Item) FROM (
SELECT Item = x.i.value('(./text())[1]', 'int') FROM (
SELECT [XML] = CONVERT(XML, '<i>' + REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.') ) AS a CROSS APPLY [XML].nodes('i') AS x(i)) AS y
WHERE Item IS NOT NULL
);
Anyway once you have a function you can simply say:
WHERE ID IN (SELECT Item FROM dbo.SplitInts_Numbers(#MyList, ','));
Passing a string directly into the IN clause is not possible. However, if you are providing the list as a string to a stored procedure, for example, you can use the following dirty method.
First, create this function:
CREATE FUNCTION [dbo].[fnNTextToIntTable] (#Data NTEXT)
RETURNS
#IntTable TABLE ([Value] INT NULL)
AS
BEGIN
DECLARE #Ptr int, #Length int, #v nchar, #vv nvarchar(10)
SELECT #Length = (DATALENGTH(#Data) / 2) + 1, #Ptr = 1
WHILE (#Ptr < #Length)
BEGIN
SET #v = SUBSTRING(#Data, #Ptr, 1)
IF #v = ','
BEGIN
INSERT INTO #IntTable (Value) VALUES (CAST(#vv AS int))
SET #vv = NULL
END
ELSE
BEGIN
SET #vv = ISNULL(#vv, '') + #v
END
SET #Ptr = #Ptr + 1
END
-- If the last number was not followed by a comma, add it to the result set
IF #vv IS NOT NULL
INSERT INTO #IntTable (Value) VALUES (CAST(#vv AS int))
RETURN
END
(Note: this is not my original code, but thanks to versioning systems here at my place of work, I have lost the header comment linking to the source.)
Then use it like so:
SELECT *
FROM tblMyTable
INNER JOIN fnNTextToIntTable(#MyList) AS List ON tblMyTable.ID = List.Value
Or, as in your question:
SELECT *
FROM tblMyTable
WHERE ID IN ( SELECT Value FROM fnNTextToIntTable(#MyList) )
Related
I have the following Split function which was used for a while now in conjunction with multi-valued parameters used for the purpose of SSRS Reports.
CREATE FUNCTION [dbo].[FnSplit]
(#List NVARCHAR(MAX),
#SplitOn NVARCHAR(5))
RETURNS #RtnValue TABLE
(
Id INT IDENTITY(1,1),
Value NVARCHAR(100)
)
AS
BEGIN
WHILE (CHARINDEX(#SplitOn, #List) > 0)
BEGIN
INSERT INTO #RtnValue (value)
SELECT
Value = LTRIM(RTRIM(SUBSTRING(#List, 1, CHARINDEX(#SplitOn, #List) - 1)))
SET #List = SUBSTRING(#List, CHARINDEX(#SplitOn, #List) + LEN(#SplitOn), LEN(#List))
END
INSERT INTO #RtnValue (Value)
SELECT Value = LTRIM(RTRIM(#List))
RETURN
END
This method is somewhat cumbersome, but I noticed that since we had an upgrade of our version of SQL Server we are on it now supports the String_Split which is a built-in function.
My question is for anyone in the know who has used this: can this be used to replace the function shown above entirely for multi-value parameters?
Thank you in advance
If 2016+, I like the JSON approach. If not 2016+ ... there is an XML approach as well.
Example
Select * from [dbo].[tvf-Str-Parse-JSON]('Dog,Cat,House,Car',',')
Returns
RetSeq RetVal
1 Dog
2 Cat
3 House
4 Car
The TVF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Parse-JSON] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = [Key]+1
,RetVal = Value
From OpenJSON( '["'+replace(replace(#String,'"','\"'),#Delimiter,'","')+'"]' )
);
Simple but should do the job
DECLARE #list varchar(max) = 'john, bob, dave'
SELECT txt
FROM (SELECT ltrim(rtrim([Value])) as txt from string_split(#list, ',')) s
ORDER BY txt
returns
Let's say we have a 12-digit numbers in a given row.
AccountNumber
=============
136854775807
293910210121
763781239182
Is it possible to shuffle the numbers of a single row solely based on the numbers of that row? e.g. 136854775807 would become 573145887067
I have created a user-defined function to shuffle the numbers.
What I have done is, taken out each character and stored it into a table variable along with a random number. Then at last concatenated each character in the ascending order of the random number.
It is not possible to use RAND function inside a user-defined function. So created a VIEW for taking a random number.
View : random_num
create view dbo.[random_num]
as
select floor(rand()* 12) as [rnd];
It's not necessary that the random number should be between 0 and 12. We can give a larger number instead of 12.
User-defined function : fn_shuffle
create function dbo.[fn_shuffle](
#acc varchar(12)
)
returns varchar(12)
as begin
declare #tbl as table([a] varchar(1), [b] int);
declare #i as int = 1;
declare #l as int;
set #l = (select len(#acc));
while(#i <= #l)
begin
insert into #tbl([a], [b])
select substring(#acc, #i, 1), [rnd] from [random_num]
set #i += 1;
end
declare #res as varchar(12);
select #res = stuff((
select '' + [a]
from #tbl
order by [b], [a]
for xml path('')
)
, 1, 0, ''
);
return #res;
end
Then, you would be able to use the function like below.
select [acc_no],
dbo.[fn_shuffle]([acc_no]) as [shuffled]
from dbo.[your_table_name];
Find a demo here
I don't really see the utility, but you can. Here is one way:
select t.accountnumber, x.shuffled
from t cross apply
(select digit
from (values (substring(accountnumber, 1, 1)),
substring(accountnumber, 2, 1)),
. . .
substring(accountnumber, 12, 1))
)
) v(digit)
order by newid()
for xml path ('')
) x(shuffled);
I need to apply a procedure on every record's NVARCHAR(MAX) field in a table. The procedure will receive a large string and split it into several shorter strings (less than 100 chars). The procedure will return a result set of smaller string. These strings will be inserted into a different table (each in its own row).
How can I apply this procedure in a set-based fashion to the whole table, so that I can insert the results into another table?
I've found some similar questions on SO, however they didn't need to use the INSERT INTO construct. This means UDF and TVF functions are off the table. EDIT: functions do not support DML statements. I wanted to use INSERT INTO inside the function.
Alternatively, is there a set-based way of using a stored procedure? SELECT sproc(Text) FROM Table didn't work.
I am not sure of your exact logic to split the string, but if possible you can make your split function an inline TVF (Heres one I made earlier):
CREATE FUNCTION dbo.Split(#StringToSplit NVARCHAR(MAX), #Delimiter NCHAR(1))
RETURNS TABLE
AS
RETURN
(
SELECT Position = Number,
Value = SUBSTRING(#StringToSplit, Number, CHARINDEX(#Delimiter, #StringToSplit + #Delimiter, Number) - Number)
FROM ( SELECT TOP (LEN(#StringToSplit) + 1) Number = ROW_NUMBER() OVER(ORDER BY a.object_id)
FROM sys.all_objects a
) n
WHERE SUBSTRING(#Delimiter + #StringToSplit + #Delimiter, n.Number, 1) = #Delimiter
);
Then you can simply use this in your insert statement by using cross apply with the TVF:
DECLARE #T1 TABLE (ID INT IDENTITY, TextToSplit NVARCHAR(MAX) NOT NULL);
DECLARE #T2 TABLE (T1ID INT NOT NULL, Position INT NOT NULL, SplitText NVARCHAR(MAX) NOT NULL);
INSERT #T1 (TextToSplit)
VALUES ('This is a test'), ('This is Another Test');
INSERT #T2 (T1ID, Position, SplitText)
SELECT t1.ID, s.Position, s.Value
FROM #T1 t1
CROSS APPLY dbo.Split(t1.TextToSplit, N' ') s;
SELECT *
FROM #T2;
I have 2 string in input for example '1,5,6' and '2,89,9' with same number of element (3 or plus).
Those 2 string i want made a "ordinate join" as
1 2
5 89
6 9
i have think to assign a rownumber and made a join between 2 result set as
SELECT a.item, b.item FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rownumber,
* FROM dbo.Split('1,5,6',',')
) AS a
INNER JOIN
(
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rownumber,
* FROM dbo.Split('2,89,9',',')
) AS b ON a.rownumber = b.rownumber
is that a best practice ever?
When dbo.Split() returns the data-set, nothing you do can assign the row_number you want (based on their order in the string) with absolute certainty. SQL never guarantees an ordering without an ORDER BY that actually relates to the data.
With you trick of using (SELECT 0) to order by you may often get the right values. Probably very often. But this is never guaranteed. Once in a while you will get the wrong order.
Your best option is to recode dbo.Split() to assign a row_number as the string is parsed. Only then can you know with 100% certainty that the row_number really does correspond to the item's position in the list.
Then you join them as you suggest, and get the results you want.
Other than that, the idea does seem fine to me. Though you may wish to consider a FULL OUTER JOIN if one list can be longer than the other.
You can do it like this as well
Consider your split function like this:
CREATE FUNCTION Split
(
#delimited nvarchar(max),
#delimiter nvarchar(100)
) RETURNS #t TABLE
(
id int identity(1,1),
val nvarchar(max)
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val)
select
r.value('.','varchar(5)') as item
from #xml.nodes('//root/r') as records(r)
RETURN
END
GO
The it will be a simple task to JOIN them together. Like this:
SELECT
*
FROM
dbo.Split('1,5,6',',') AS a
JOIN dbo.Split('2,89,9',',') AS b
ON a.id=b.id
The upside of this is that you do not need any ROW_NUMBER() OVER(ORDER BY SELECT 0)
Edit
As in the comment the performance is better with a recursive split function. So maybe something like this:
CREATE FUNCTION dbo.Split (#s varchar(512),#sep char(1))
RETURNS table
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(#sep, #s)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(#sep, #s, stop + 1)
FROM Pieces
WHERE stop > 0
)
SELECT pn,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
FROM Pieces
)
GO
And then the select is like this:
SELECT
*
FROM
dbo.Split('1,5,6',',') AS a
JOIN dbo.Split('2,89,9',',') AS b
ON a.pn=b.pn
Thanks to Arion's suggestion. It's very useful for me. I modified the function a little bit to support varchar(max) type of input string, and max length of 1000 for the delimiter string. Also, added a parameter to indicate if you need the empty string in the final return.
For MatBailie's question, because this is an inline function, you can include the pn column in you outer query which is calling this function.
CREATE FUNCTION dbo.Split (#s nvarchar(max),#sep nvarchar(1000), #IncludeEmpty bit)
RETURNS table
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT convert(bigint, 1) , convert(bigint, 1), convert(bigint,CHARINDEX(#sep, #s))
UNION ALL
SELECT pn + 1, stop + LEN(#sep), CHARINDEX(#sep, #s, stop + LEN(#sep))
FROM Pieces
WHERE stop > 0
)
SELECT pn,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE LEN(#s) END) AS s
FROM Pieces
where start< CASE WHEN stop > 0 THEN stop ELSE LEN(#s) END + #IncludeEmpty
)
But I ran into a bit issue with this function when the list intended to return had more than 100 records. So, I created another function purely using string parsing functions:
Create function [dbo].[udf_split] (
#ListString nvarchar(max),
#Delimiter nvarchar(1000),
#IncludeEmpty bit)
Returns #ListTable TABLE (ID int, ListValue varchar(max))
AS
BEGIN
Declare #CurrentPosition int, #NextPosition int, #Item nvarchar(max), #ID int
Select #ID = 1,
#ListString = #Delimiter+ #ListString + #Delimiter,
#CurrentPosition = 1+LEN(#Delimiter)
Select #NextPosition = Charindex(#Delimiter, #ListString, #CurrentPosition)
While #NextPosition > 0 Begin
Select #Item = Substring(#ListString, #CurrentPosition, #NextPosition-#CurrentPosition)
If #IncludeEmpty=1 or Len(LTrim(RTrim(#Item)))>0 Begin
Insert Into #ListTable (ID, ListValue) Values (#ID, LTrim(RTrim(#Item)))
Set #ID = #ID+1
End
Select #CurrentPosition = #NextPosition+LEN(#Delimiter),
#NextPosition = Charindex(#Delimiter, #ListString, #CurrentPosition)
End
RETURN
END
Hope this could help.
I'm working on a report in reporting services that has the user select a number of items from a multivalue list. The query for the report uses the resulting list in a simple
SELECT foo FROM bar WHERE foobar IN (#SelectedItemsFromMultiValueList)
I'm now altering the report and need to iterate over the items in #SelectedItemsFromMultiValueList using a cursor. I've looked around but can't figure out how to do this - made even more difficult by the fact that I'm not sure what to call a list of values used in an IN or even declare one manually (eg. DECLARE #SelectedItemsFromMultiValueList ???)
Does anybody know how to cursor over a multivalue list parameter or how to call something like that in SQL so I can search more effectively for a solution?
Your multi-value list is going to come in to sql as a list of comma separated values (i.e. "31,26,17")
To iterate through these values you need a way to split the values into a table. This is a function I have used, that I believe was originally coded by Jens Suessmeyer:
CREATE FUNCTION [dbo].[ufn_split]
( #Delimiter varchar(5),
#List nvarchar(max)
)
RETURNS #TableOfValues table
( RowID smallint IDENTITY(1,1),
[Value] NVARCHAR(max)
)
AS
BEGIN
DECLARE #LenString int
WHILE len( #List ) > 0
BEGIN
SELECT #LenString =
(CASE charindex( #Delimiter, #List )
WHEN 0 THEN len( #List )
ELSE ( charindex( #Delimiter, #List ) -1 )
END
)
INSERT INTO #TableOfValues
SELECT substring( #List, 1, #LenString )
SELECT #List =
(CASE ( len( #List ) - #LenString )
WHEN 0 THEN ''
ELSE right( #List, len( #List ) - #LenString - 1 )
END
)
END
RETURN
END
So you call this function, passing it #SelectedItemsFromMultiValueList and it will return to you a table of values that you can then do with what you want.
For example:
SELECT * FROM Foo WHERE X IN (SELECT [value] FROM dbo.ufn_split(',', #SelectedItemsFromMultiValueList)
i prefer my set-based solution using a recursive cte
declare #delim varchar(max),
#string varchar(max)
set #delim=','
set #string='1,2,3,4,5,6,7,8,9,10'
;with c as
(
select
CHARINDEX(#delim,#string,1) as Pos,
case when CHARINDEX(#delim,#string,1)>0 then SUBSTRING(#string,1,CHARINDEX(#delim,#string,1)-1) else #string end as Value,
case when CHARINDEX(#delim,#string,1)>0 then SUBSTRING(#string,CHARINDEX(#delim,#string,1)+1,LEN(#string)-CHARINDEX(#delim,#string,1)) else '' end as String
union all
select
CHARINDEX(#delim,String,1) as Pos,
case when CHARINDEX(#delim,String,1)>0 then SUBSTRING(String,1,CHARINDEX(#delim,String,1)-1) else String end as Value,
case when CHARINDEX(#delim,String,1)>0 then SUBSTRING(String,CHARINDEX(#delim,String,1)+1,LEN(String)-CHARINDEX(#delim,String,1)) else '' end as String
from c
where LEN(String)>0
)
select
Value
from c
option (maxrecursion 10000)
you then take whatever your query is and do an inner join with c.