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'''
Related
I am using SQL Server 2008 R2 version. I have a simple table which has multiple columns. One of the column in EmpId which is of type nvarchar(50)
I am writing a stored procedure in which I receive an input which can have one of the following values.
Single EmpId: '12345'
Multiple EmpId's comma separated: '12345, 56789, 98987'
null
What I want:
If empid is a single empId just return
select *
from table_name
where EmpId = #empId
If empid is multiple comma-separated values, just return
select *
from table_name
where EmpId in (select * from dbo.splitstring(#empId))
if empId is null just return
Select *
from table_name
No need for where clause.
To cover all the three condition this is what I am trying:
DECLARE #empId nvarchar(2000)
SET #empId = '97050001,97050003, 97050004'
SELECT TOP 10 empId
FROM Employee
WHERE empId in (COALESCE((select * from dbo.splitstring(#empId)),[empId]))
I am getting following error:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I understand the error. COALESCE() is expecting a single value but when I am getting comma separated values, splitstring function returns multiple values.
I do not want to build a dynamic query, so besides duplicating the code with if else block where I check if empId is null run select * from table_name else run select * from table name where empId in (). What options do I have?
To split comma-separated string into table, I am using this function:
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE CHARINDEX(',', #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
Try this with a bit more sophisticated version of split string fuction.
CREATE PROCEDURE myProc
#EmpId NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = N' SELECT * FROM Table_Name WHERE 1 = 1'
+ CASE WHEN #EmpId IS NOT NULL THEN
N' AND empId IN ( SELECT Split.a.value(''.'', ''VARCHAR(100)'') empId
FROM (
SELECT Cast (''<X>''
+ Replace(#EmpId, '','', ''</X><X>'')
+ ''</X>'' AS XML) AS Data
) AS t CROSS APPLY Data.nodes (''/X'') AS Split(a)
) ' ELSE N'' END
Exec sp_executesql #Sql
,N'#EmpId NVARCHAR(50)'
,#EmpId
END
This version will perform better because
More efficient string splitting.
Better execution plan because of the parameterised execution plan caching because of the sp_executesql .
Depends on what you need:-
I do not want to build dynamic query, so besides duplicating the code
with if else block where I check if empId is null run select * from
table_name else run select * from table name where empId in ()
and for avoiding the duplicate, use the next approach:-
DECLARE #empId nvarchar(2000)
set #empId = '97050001,97050003,97050004'
if CHARINDEX(',',#empId) > 0 -- multiple Values
begin
set #empId = '''' + replace (#empId,',',''',''') + ''''
end
else if #empId is null
begin
set #empId = 'select empId from Employee'
end
exec ('select top 10 empId from Employee where empId in (' + #empId + ')' )
This approach handles the three cases:-
Passing a single value.
Passing multiple Value.
Passing a Null.
Case 1 and 2 can both be handled for the code you have already written for Case 2. You just need to add an OR condition for Case 3.
select *
from table_name
where #empId is NULL
or EmpId in (select * from dbo.splitstring(#empId))
That being said, IN clauses wrapping select statements are generally bad practice for the following reasons
They are slower than joins when the select statement returns many rows
Joins are more idiomatic
In your case, if there are only a few rows returned by splitstring it likely won't make much difference, but the following would be a more general approach to this kind of query.
select *
from table_name t
left join dbo.splitstring(#empId) s
on t.EmpId = s.Name
where #empId is NULL
or s.Name is not NULL
Feel free to check the execution plan for your query and profile to see which one is quicker, though your initial implementation should be fine. To quote Donald Knuth, "premature optimization is the root of all evil."
Update
After double checking the execution plans used in the cases when #empId is null and non null, it looks like the query above will always use the same execution plan, i.e. to join against the contents of the in clause, regardless of whether #empId is null. This is probably not ideal, as pointed out by #m-ali.
To ensure the proper execution plan in each case, you can separate this into two queries:
IF #empId is NULL
select *
from table_name
ELSE
select *
from table_name
where EmpId in (select * from dbo.splitstring(#empId))
I've verified the proper execution plan in either case in SSMS.
Disclaimer: I haven't profiled it but the string splitting suggested by #m-ali is also likely faster.
This question already has answers here:
Passing a varchar full of comma delimited values to a SQL Server IN function
(27 answers)
Closed 7 years ago.
I would like to retrieve certain users from a full list of a temp table #temptable.
The query looks like this:
DECLARE #List varchar(max)
SELECT #List = coalesce(#List + ',','') + '''' + StaffCode + ''''
FROM tblStaffs
SELECT UserName
FROM #temptable
WHERE #temptable.StaffCode IN (#List)
I can tell #List is in a right format:
'AAA','ABB','BBB','CCC','DDD','MMM'
And if I change it to
WHERE #temptable.StaffCode IN ('AAA','ABB','BBB','CCC','DDD','MMM')
It certainly works, then why not IN (#List)?
Create some split string function and convert the comma separated values to rows then you can use the converted rows IN clause
DECLARE #List VARCHAR(max)
SELECT #List = COALESCE(#List + ',', '') +StaffCode
FROM tblStaffs
SELECT UserName
FROM #temptable
WHERE #temptable.StaffCode IN (SELECT split_values
FROM dbo.Splitstring_function(#list))
Check here for various Split String function
If you dont want to create functions then you can also directly use the code instead of creating a new function(M.Ali's answer).
Another way of doing it is using dynamic query.
Declare #List varchar(max), #sql nvarchar(max)
Select #List = coalesce(#List + ',','') + '''' + StaffCode + ''''
From tblStaffs
set #sql = '
Select UserName
From #temptable
Where #temptable.StaffCode IN ('+ #List + ')'
--print #sql
exec (#sql)
To debug the dynamic query always print the dynamic sql before executing.
Because the Variable has a string which IN operator reads as 'AAA'',''ABB'',''BBB' and it treats it as a single value.
In your query you should really use the query itself in the IN operator something like....
Select UserName
From #temptable
Where #temptable.StaffCode IN (SELECT StaffCode From tblStaffs)
Anyway if there is a need to use variable and then read values inside the IN operator from that variable you can do something like this....
DECLARE #List VARCHAR(1000);
Select #List = coalesce(#List + ',','') + StaffCode
From tblStaffs
SELECT *
From #temptable
Where #temptable.StaffCode IN (
SELECT t.c.value('.', 'VARCHAR(1000)')
FROM (
SELECT x = CAST('<t>' +
REPLACE(#List , ',', '</t><t>') + '</t>' AS XML)
) a
CROSS APPLY x.nodes('/t') t(c))
I would recommend that you not use a comma-delimited string at all. Consider a semi-join instead:
select [Temp].[UserName]
from
#temptable [Temp]
where
exists (select 1 from [tblStaffs] where [tblStaffs].[StaffCode] = [Temp].[StaffCode]);
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
I have the following case statement as shown below:
Example:
I have the case statement:
case cola
when cola between '2001-01-01' and '2001-01-05' then 'G1'
when cola between '2001-01-10' and '2001-01-15' then 'G2'
when cola between '2001-01-20' and '2001-01-25' then 'G3'
when cola between '2001-02-01' and '2001-02-05' then 'G4'
when cola between '2001-02-10' and '2001-02-15' then 'G5'
else ''
end
Note: Now I want to create dynamic case statement because of the values dates and name passing as a parameter and it may change.
Declare #dates varchar(max) = '2001-01-01to2001-01-05,2001-01-10to2001-01-15,
2001-01-20to2001-01-25,2001-02-01to2001-02-05,
2001-02-10to2001-02-15'
Declare #names varchar(max) = 'G1,G2,G3,G4,G5'
The values in the variables may change as per the requirements, it will be dynamic. So the case statement should be dynamic without using loop.
My bad try:
DECLARE #Name varchar(max)
DECLARE #Dates varchar(max)
DECLARE #SQL varchar(max)
DECLARE #SQL1 varchar(max)
SET #Name = 'G1,G2,G3,G4,G5'
SET #dates = '2001-01-01to2001-01-05,2001-01-10to2001-01-15,
2001-01-20to2001-01-25,2001-02-01to2001-02-05,
2001-02-10to2001-02-15'
SELECT #SQL = STUFF((SELECT ' ' + Value FROM
(
SELECT 'WHEN Cola Between '''' AND '''' THEN ''' + A.Value + '''' AS Value
FROM
(
SELECT
Split.a.value('.', 'VARCHAR(100)') AS Value
FROM
(
SELECT CAST ('<M>' + REPLACE(#Name, ',',
'</M><M>') + '</M>' AS XML) AS Value
) AS A
CROSS APPLY Value.nodes ('/M') AS Split(a)
) AS A
) AS B
FOR XML PATH (''), type).value('.', 'Varchar(max)'),1,1,'') + ''
SET #SQL1 = 'CASE Cola '+#SQL+' ELSE '''' END'
PRINT(#SQL1);
Stuck: But got stuck to split the #dates 2001-01-01to2001-01-05
into BETWEEN '2001-01-01' AND '2001-01-05'.
Just create a temp table (which can be inserted into dynamically) and use it in a LEFT JOIN. A LEFT JOIN (along with the COALESCE) accounts for the ELSE '' condition, but if there was no ELSE condition and all ranges were represented in the data, an INNER JOIN should be used (and no need for the COALESCE).
In order to dynamically populate the temp table from two separate variables that whose data is aligned only by position within the CSV list, and one of which is a two-dimensional array needing to be split on both comma and the string "to", I used a CTE (to make it easier to split the two-dimension #Dates variable) and a SQLCLR-based string splitter. The splitter I used is from the SQL# library (which I am the creator of but this function is in the Free version) but you can use any splitter you like (but please don't use a WHILE loop-based splitter as that is just silly).
CREATE TABLE #Cola
(
StartDate DATETIME NOT NULL,
EndDate DATETIME NOT NULL,
Name NVARCHAR(50) NOT NULL
);
DECLARE #Dates VARCHAR(MAX) = '2001-01-01to2001-01-05,2001-01-10to2001-01-15,
2001-01-20to2001-01-25,2001-02-01to2001-02-05,
2001-02-10to2001-02-15';
DECLARE #Names VARCHAR(MAX) = 'G1,G2,G3,G4,G5';
-- dynamic population of temp table from two variables (#Dates being 2 dimensional)
;WITH cte AS
(
SELECT vals.SplitNum,
vals.SplitVal,
CHARINDEX(N'to', vals.SplitVal) AS [WhereToSplit]
FROM SQL#.String_Split4k(#dates, ',', 1) vals
)
INSERT INTO #Cola (StartDate, EndDate, Name)
SELECT CONVERT(DATETIME, SUBSTRING(cte.SplitVal, (cte.WhereToSplit - 10), 10)),
CONVERT(DATETIME, SUBSTRING(cte.SplitVal, (cte.WhereToSplit + 2), 10)),
names.SplitVal
FROM cte
INNER JOIN SQL#.String_Split4k(#names, ',', 1) names
ON names.SplitNum = cte.SplitNum; -- keep the values aligned by position
SELECT tab.fields, COALESCE(cola.[Name], '') AS [Cola]
FROM SchemaName.TableName tab
LEFT JOIN #Cola cola
ON tab.cola BETWEEN cola.StartDate AND cola.EndDate
I have a table that contains many rows of SQL commands that make up a single SQL statement (to which I am grateful for this answer, step 5 here)
I have followed the example in this answer and now have a table of SQL - each row is a line of SQL that build a query. I can copy and paste the contents of this table into a new query window and get the results however due to my lack of SQL knowledge I am not sure how I go about copying the contents of the table into a string variable which I can then execute.
Edit: The SQL statement in my table comprises of 1 row per each line of the statement i.e.
Row1: SELECT * FROM myTable
Row2: WHERE
Row3: col = #value
This statement if copied into a VARCHAR(MAX) exceeds the MAX limit.
I look forward to your replies. in the mean time I will try myself.
Thank you
You can use coalesce to concatenate the contents of a column into a string, e.g.
create table foo (sql varchar (max));
insert foo (sql) values ('select name from sys.objects')
insert foo (sql) values ('select name from sys.indexes')
declare #sql_output varchar (max)
set #sql_output = '' -- NULL + '' = NULL, so we need to have a seed
select #sql_output = -- string to avoid losing the first line.
coalesce (#sql_output + sql + char (10), '')
from foo
print #sql_output
Note: untested, just off the top of my head, but a working example of this should produce the following output:
select name from sys.objects
select name from sys.indexes
You can then execute the contents of the string with exec (#sql_output) or sp_executesql.
You can try something like this
DECLARE #TABLE TABLE(
SqlString VARCHAR(MAX)
)
INSERT INTO #TABLE (SqlString) SELECT 'SELECT 1'
DECLARE #SqlString VARCHAR(MAX)
SELECT TOP 1 #SqlString = SqlString FROM #TABLE
EXEC (#SqlString)
Concatenate string from multiple rows
DECLARE #Table TABLE(
ID INT,
Val VARCHAR(50)
)
INSERT INTO #Table (ID,Val) SELECT 1, 'SELECT *'
INSERT INTO #Table (ID,Val) SELECT 2, 'FROM YourTable'
INSERT INTO #Table (ID,Val) SELECT 3, 'WHERE 1 = 1'
DECLARE #SqlString VARCHAR(MAX)
--Concat
SELECT DISTINCT
#SqlString =
(
SELECT tIn.Val + ' '
FROM #Table tIn
ORDER BY ID
FOR XML PATH('')
)
FROM #Table t
PRINT #SqlString
if you want to execute a string of sql then use Exec() or sp_executeSql