I am working in SQL Server 2008. I have a stored proc that takes a parameter, called #test. This parameter is varchar(255). In this stored proc, I need to parse this string, convert each value into a string itself (there will be a variable number of values), and build a list to use in a NOT IN statement.
For example, suppose #test = 'a, b, c, d'. I need to send this parameter into my stored proc. There is a SELECT query in my stored proc that uses a NOT IN statement. For this example, I need this NOT IN statement to read NOT IN('a', 'b', 'c', 'd').
How do I accomplish this? Or, is this a bad practice?
Use Split function something along with NOT EXISTS operator almost always faster than NOT IN operator
Split Function
CREATE FUNCTION [dbo].[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'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
Test Data
DECLARE #Table TABLE (Vals INT)
INSERT INTO #Table VALUES (1), (2), (3), (4)
DECLARE #test VARCHAR(256) = '3,4,5,6'
SELECT * FROM #Table
WHERE NOT EXISTS (SELECT 1
FROM [dbo].[split](#test , ',')
WHERE val = Vals)
Result
Vals
1
2
You can use dynamic sql for this. Use the replace function to get the correct NOT IN() argument:
DECLARE #test VARCHAR(10) = 'a,b,c,d'
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = 'SELECT *
FROM #temp
WHERE label NOT IN (''' + REPLACE( #test ,',',''',''') + ''')'
EXEC sp_executesql #sql
you can find the char or string in the whole string having delimiter as comma (,)
DECLARE #code VARCHAR(50) = 'c', #text varchar(100) = 'a,b,c,d,e'
SELECT CHARINDEX(',' + #code + ',', ',' + #text + ',') > 0
Related
How to simulate string_agg function ?
I need get this
[value]
1
2
3
into this
1,2,3
I tried following
CREATE TYPE stringArray AS TABLE ([value] nvarchar(255))
GO
CREATE FUNCTION dbo.ufn_join
(
#table stringArray readonly,
#separator nvarchar(5) = ','
)
RETURNS nvarchar(max)
AS
BEGIN
RETURN stuff((select #separator + value from #table for xml path('')), 1, 1, '')
END
GO
SELECT dbo.ufn_join(
(
SELECT cast(1 as nvarchar(255)) as value
UNION
SELECT cast(2 as nvarchar(255)) as value
UNION
SELECT cast(3 as nvarchar(255)) as value
)
, DEFAULT
)
but I am getting an error
-- Error: Operand type clash: nvarchar is incompatible with stringArray
Only condition is that i do not want to use any kind of variables. CLR function is also totally fine, but there i have the same issue, how to insert return of select as a parameter to the function.
Normally I use this link when I want to concat rows. There are several options how to do it, so here you can find inspiration on which approach you like the most. Be aware of XML PATH since it uses all of your CPU Processes and can max out your CPU to 100%.
Different concat approaches
Example from the link:
CREATE FUNCTION dbo.udf_select_concat ( #c INT )
RETURNS VARCHAR(MAX) AS BEGIN
DECLARE #p VARCHAR(MAX) ;
SET #p = '' ;
SELECT #p = #p + ProductName + ','
FROM Northwind..Products
WHERE CategoryId = #c ;
RETURN #p
END
SELECT CategoryId, dbo.udf_select_concat( CategoryId )
FROM Northwind..Products
GROUP BY CategoryId ;
TVP issue aside, your function will be profoundly faster and more efficient by turning it into an inline table valued function (commonly referred as an inline scalar function (iSF)). This article explains what I'm saying in detail:
How to Make Scalar UDFs Run Faster (SQL Spackle)
CREATE FUNCTION dbo.ufn_join (#separator nvarchar(5))
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT concatinatedTxt =
stuff((select #separator + someTxt from dbo.someTable for xml path('')), 1, 1, '');
It's because you declared a type, set that type to a parameter and the tried to insert a table into this parameter (a different type).
Try this:
CREATE TYPE stringArray AS TABLE ([value] nvarchar(255))
GO
CREATE FUNCTION dbo.ufn_join
(
#table stringArray readonly,
#separator nvarchar(5) = ','
)
RETURNS nvarchar(max)
AS
BEGIN
RETURN stuff((select #separator + value from #table for xml path('')), 1, 1, '')
END
GO
DECLARE #table stringArray
INSERT INTO #Table
SELECT cast(1 as nvarchar(255)) as value
UNION
SELECT cast(2 as nvarchar(255)) as value
UNION
SELECT cast(3 as nvarchar(255)) as value
SELECT dbo.ufn_join(
#Table
, DEFAULT
)
CREATE FUNCTION [dbo].[func_1]
(
#ListNum AS nvarchar(MAX)
)
RETURNS #t TABLE
(
col_1 nvarchar(MAX)
)
AS
BEGIN
INSERT #t
SELECT col_1
FROM table_name
WHERE col_2 IN (#ListNum)
RETURN
END
When I pass only one value in paramater (for example : 1) the function correctly works but how can I pass multiple value (for example : 1,2,3,4,5). I get the following error :
Procedure execution failed
42000 - [SQL Server]Error converting data type nvarchar to bigint.
Is there a simple way to solve this?
Hi you can try like this,
CREATE FUNCTION Splitstring (#Input NVARCHAR(MAX),
#Character CHAR(1))
RETURNS #Output TABLE (
Item NVARCHAR(1000))
AS
BEGIN
DECLARE #StartIndex INT,
#EndIndex INT
SET #StartIndex = 1
IF Substring(#Input, Len(#Input) - 1, Len(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE Charindex(#Character, #Input) > 0
BEGIN
SET #EndIndex = Charindex(#Character, #Input)
INSERT INTO #Output
(Item)
SELECT Substring(#Input, #StartIndex, #EndIndex - 1)
SET #Input = Substring(#Input, #EndIndex + 1, Len(#Input))
END
RETURN
END
GO
CREATE FUNCTION [dbo].[Func_1] (#ListNum AS NVARCHAR(MAX))
RETURNS #t TABLE (
col_1 NVARCHAR(MAX))
AS
BEGIN
INSERT #t
SELECT p.col1
FROM dbo.Splitstring(#ListNum, ',') s
JOIN Table_Name t
ON t.col2 = s.Item
RETURN
END
DECLARE #var VARCHAR(100)='1,2,3,4'
SELECT *
FROM dbo.Func_1(#var)
Introduce one more function called split string. It will return the comma separated list as a table. Join the comma separated table with your actual table. This will gives the result.
For versions 2008+ using table separated values can assist where the calling procedure can construct the table and you are able to create a table type. If you must pass comma (or other character separated values) in a single string then you will need to separate the comma delimited string into a result set of its own.
The XML method works well when your string doesn't contain any special XML characters such as angle brackets <> - how-to-split-a-comma-separated-value-to-columns
I think this will work for your adjusted function;
CREATE FUNCTION [dbo].[func_1]
(
#ListNum AS nvarchar(MAX)
)
RETURNS #t TABLE
(
col_1 nvarchar(MAX)
)
AS
BEGIN
DECLARE #S varchar(max),
#Split char(1),
#X xml
SELECT #S = #ListNum,
#Split = ','
SELECT #X = CONVERT(xml,' <root> <s>' + REPLACE(#S,#Split,'</s> <s>') + '</s> </root> ')
INSERT #t
SELECT col_1
FROM table_name
WHERE col_2 IN (SELECT [Value] = T.c.value('.','varchar(20)')
FROM #X.nodes('/root/s') T(c))
RETURN
END
I have the following code to create a SQL function that will parse an XML string and create a table of key value pairs that represent the nodes and values. This works fine for me in my use cases.
CREATE FUNCTION XmlToKeyValue
(
#rootName AS varchar(256),
#xml AS Xml
)
RETURNS #keyval TABLE ([key] varchar(max), [value] varchar(max))
AS
BEGIN
DECLARE #input TABLE (XmlData XML NOT NULL)
INSERT INTO #input VALUES(#xml)
INSERT #keyval ([key], [value])
SELECT
XC.value('local-name(.)', 'varchar(max)') AS [key],
XC.value('(.)[1]', 'varchar(max)') AS [value]
FROM
#input
CROSS APPLY
XmlData.nodes('/*[local-name()=sql:variable("#rootName")]/*') AS XT(XC)
RETURN
END
What I am trying to do is have a stored procedure in my main database that will create another database with all the appropriate functions/procedures/etc. So in that stored procedure I am trying to do something like this:
SET #cmd = '
CREATE FUNCTION XmlToKeyValue
(
#rootName AS varchar(256),
#xml AS Xml
)
RETURNS #keyval TABLE ([key] varchar(max), [value] varchar(max))
AS
BEGIN
DECLARE #input TABLE (XmlData XML NOT NULL)
INSERT INTO #input VALUES(#xml)
INSERT #keyval ([key], [value])
SELECT
XC.value(''local-name(.)'', ''varchar(max)'') AS [key],
XC.value(''(.)[1]'', ''varchar(max)'') AS [value]
FROM
#input
CROSS APPLY
XmlData.nodes(''/*[local-name()=sql:variable("#rootName")]/*'') AS XT(XC)
RETURN
END
'
BEGIN TRY
EXEC(N'USE '+#dbName+'; EXEC sp_executesql N''' + #cmd + '''; USE master')
END TRY
BEGIN CATCH
PRINT 'Error creating XmlToKeyValue'
Print Error_Message();
RETURN
END CATCH
However, I am getting the following error that I can't figure out how to resolve.
Error creating XmlToKeyValue
Incorrect syntax near 'local'.
Can I use local-name in a dynamic sql statement? If not, how can I achieve my goal? Thank you.
The problem is not the local-name function. It is entirely the fact that you are concatenating in the #cmd variable into your Dynamic SQL without properly escaping the embedded single-quotes.
This line:
EXEC(N'USE '+#dbName+'; EXEC sp_executesql N''' + #cmd + '''; USE master')
should be:
SET #cmd = REPLACE(#cmd, N'''', N'''''');
EXEC(N'USE ' + #dbName + N'; EXEC sp_executesql N''' + #cmd + N''';');
Else you are embedding:
XC.value(''local-name(
into the string, but using the same number of escape sequences, hence the XC.value( now becomes the end of the string and the local-name(.) is technically unescaped SQL and not part of a string.
Also:
You don't need the USE master at the end of the Dynamic SQL (so I removed it).
You prefixed the first string literal with N but none of the others (I added the N in for the others so that they all have that prefix).
I run several queries that use a list of character values in the where clause, e.g.,
select *
from table1
where col1 in ('a','b','c')
The character list changes frequently, so I want to store the string in a variable and reference the variable in all of the queries instead of maintaining several copies of the string. I've tried the following but the query returns zero rows.
declare #str varchar(50)
select #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
select *
from table1
where col1 in (#str)
#str has the value 'a','b','c' but for some reason, SQL Server doesn't recognize it. How do I build a string and store it in a variable that works with the in keyword?
The IN construct in SQL as a set lookup, not a string lookup. Your single string value of "'a','b','c'" is exactly what it's looking for when you say where col1 in (#str)... as Fredou mentioned in comments.
Instead you want to pass in a set of values by using a table variable (or a temp table):
declare #tabIn table ( val varchar(10) )
insert #tabIn
(val) values
('a'), ('b'), ('c')
select *
from table1
where
col1 in (select val from #tabIn)
or, alternatively, just do a straight join:
declare #tabIn table ( val varchar(10) )
insert #tabIn
(val) values
('a'), ('b'), ('c')
select *
from table1 t1
join #tabIn t2 on
t1.col1 = t2.val
It is possible to create a string with embedded quotes. As Fredou and ChrisS mentioned, #str is considered a single string. If the #str value is concatenated with the rest of your select statement and then executed, you will achieve the your desired results. SQL Fiddle example.
declare #str varchar(50)
declare #sql varchar(MAX)
select #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
Select #sql = 'SELECT * FROM table1 WHERE col1 IN (' + #str + ')'
Exec(#sql)
Results using #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
Results using #str = '''a''' + ',' + '''b'''
I need to create a stored procedure that can check for all records matching an indefinite number of values.
So for example take the following simple statement;
SELECT *
FROM some_table
WHERE ID = #param1
However, #param1 should be able to take a string of comma-delimited values and check against all of them eg:
#param1 = '1,2,45,16,476,324,'
I imagine this would need to take the values and then turn them into a temporary table, and then somehow create a sub query to check against all the values in the temporary table. However my T-SQL skills aren't quite up to tackling that on my own just yet.
How can I do this, or is there a better way to do it?
There are many split function version online, if you just google SQL Server Split function you will get 100s of results back.
A quick fix without a function would look something like......
DECLARE #param1 VARCHAR(100) = '1,2,45,16,476,324'
DECLARE #param1XML xml;
SELECT #param1XML = CONVERT(xml,' <root> <s>'
+ REPLACE(#param1, ',','</s> <s>')
+ '</s> </root> ')
SELECT *
FROM some_table
WHERE ID IN (
SELECT T.c.value('.','varchar(20)') AS Value
FROM #param1XML.nodes('/root/s') T(c)
)
Procedure
A proc would look something like...
CREATE PROCEDURE dbo.usp_SomeProc
#param1 VARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #param1XML xml;
SELECT #param1XML = CONVERT(xml,' <root> <s>'
+ REPLACE(#param1, ',','</s> <s>')
+ '</s> </root> ')
SELECT *
FROM some_table
WHERE ID IN (
SELECT T.c.value('.','varchar(20)') AS Value
FROM #param1XML.nodes('/root/s') T(c))
END
What about this solution - using XML query.
Table variable in procedure is created for test only.
CREATE PROCEDURE dbo.spTestDelimitedParam
(
#param1 as varchar(max)
)
AS
BEGIN
DECLARE #xml as xml
DECLARE #delimiter as char(1)
DECLARE #test table (id int, description varchar(20))
insert into #test
values (1, 'Row ID = 1'), (11, 'Row ID = 11'), (3, 'Row ID = 3')
SET #delimiter =','
SET #xml = cast(('<X>'+replace(#param1,#delimiter ,'</X><X>')+'</X>') as xml)
SELECT *
FROM #test
WHERE ID IN (SELECT N.value('.', 'int') as value FROM #xml.nodes('X') as T(N))
END
How it works:
exec dbo.spTestDelimitedParam '1,23,4,11,24456'