参数化SQL IN子句

我如何参数化一个查询包含一个可变数量的参数的IN子句,就像这个?

 SELECT * FROM Tags WHERE Name IN ('ruby','rails','scruffy','rubyonrails') ORDER BY Count DESC 

在这个查询中,参数的个数可以是1到5之间的任意值。

我不希望为这个(或XML)使用一个专用的存储过程,但是如果有一些针对SQL Server 2008的优雅方式,我可以这么做。

这是我用过的一个快速和肮脏的技术:

 SELECT * FROM Tags WHERE '|ruby|rails|scruffy|rubyonrails|' LIKE '%|' + Name + '|%' 

所以这里是C#代码:

 string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" }; const string cmdText = "select * from tags where '|' + @tags + '|' like '%|' + Name + '|%'"; using (SqlCommand cmd = new SqlCommand(cmdText)) { cmd.Parameters.AddWithValue("@tags", string.Join("|", tags); } 

两个警告:

  • 表演很糟糕。 LIKE "%...%"查询没有编入索引。
  • 确保你没有任何| ,空白或空标签或这将无法正常工作

还有其他方法可以做到这一点,有些人可能会认为更清洁,所以请继续阅读。

你可以参数化每个值,例如:

 string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" }; string cmdText = "SELECT * FROM Tags WHERE Name IN ({0})"; string[] paramNames = tags.Select( (s, i) => "@tag" + i.ToString() ).ToArray(); string inClause = string.Join(", ", paramNames); using (SqlCommand cmd = new SqlCommand(string.Format(cmdText, inClause))) { for(int i = 0; i < paramNames.Length; i++) { cmd.Parameters.AddWithValue(paramNames[i], tags[i]); } } 

哪个会给你:

 cmd.CommandText = "SELECT * FROM Tags WHERE Name IN (@tag0, @tag1, @tag2, @tag3)" cmd.Parameters["@tag0"] = "ruby" cmd.Parameters["@tag1"] = "rails" cmd.Parameters["@tag2"] = "scruffy" cmd.Parameters["@tag3"] = "rubyonrails" 

不,这不是对SQL注入开放。 CommandText中唯一注入的文本不基于用户input。 它完全基于硬编码的“@tag”前缀和数组的索引。 索引总是一个整数,不是用户生成的,是安全的。

用户input的值仍然填入参数中,因此没有任何漏洞。

编辑:

除了注入问题之外,需要注意的是构build命令文本以容纳可变数量的参数(如上所述)阻碍了SQL服务器利用caching查询的能力。 最终的结果是,你几乎可以肯定会失去首先使用参数的价值(而不是仅仅将谓词string插入SQL本身)。

不是说caching的查询计划是没有价值的,但IMO这个查询几乎没有复杂到从中看到很多好处。 虽然编译成本可能接近(甚至超过)执行成本,但你仍然在说毫秒。

如果你有足够的内存,我希望SQL Server可能会caching一个计划的常见计数的参数。 我想你总是可以添加五个参数,并且让未指定的标签为NULL – 查询计划应该是相同的,但是对我来说这看起来相当丑陋,我不确定它是否值得微型优化(尽pipe,堆栈溢出 – 这可能是非常值得的)。

此外,SQL Server 7及更高版本将自动参数化查询 ,因此,从性能的angular度来看,使用参数并不是真正必要的,但从安全angular度来看,这一点至关重要 ,特别是对于像这样的用户input数据。

对于SQL Server 2008,您可以使用表值参数 。 这是一个工作,但它可以说是比我的其他方法更干净。

首先,你必须创build一个types

 CREATE TYPE dbo.TagNamesTableType AS TABLE ( Name nvarchar(50) ) 

然后,你的ADO.NET代码如下所示:

 string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" }; cmd.CommandText = "SELECT Tags.* FROM Tags JOIN @tagNames as P ON Tags.Name = P.Name"; // value must be IEnumerable<SqlDataRecord> cmd.Parameters.AddWithValue("@tagNames", tags.AsSqlDataRecord("Name")).SqlDbType = SqlDbType.Structured; cmd.Parameters["@tagNames"].TypeName = "dbo.TagNamesTableType"; // Extension method for converting IEnumerable<string> to IEnumerable<SqlDataRecord> public static IEnumerable<SqlDataRecord> AsSqlDataRecord(this IEnumerable<string> values, string columnName) { if (values == null || !values.Any()) return null; // Annoying, but SqlClient wants null instead of 0 rows var firstRecord = values.First(); var metadata = SqlMetaData.InferFromValue(firstRecord, columnName); return values.Select(v => { var r = new SqlDataRecord(metadata); r.SetValues(v); return r; }); } 

原来的问题是“我如何参数化查询…”

让我在这里说,这不是对原来的问题的答案 。 其他好的答案已经有一些示范。

有了这个说法,请继续,并标记这个答案,downvote它,标记它不是一个答案…做任何你认为是正确的。

请参阅马克·布拉克特(Mark Brackett)的回答,我(和其他231人)赞成的首选答案。 他的答案中给出的方法允许1)有效地使用绑定variables,以及2)对于可靠的谓词。

选定的答案

我想在这里解决的是乔尔·斯波尔斯基的回答中给出的方法,“select”的答案是正确的答案。

Joel Spolsky的方法很聪明。 它的工作原理是合理的,它将呈现出可预测的行为和可预测的性能,给定“正常”的值以及规范的边缘情况,例如NULL和空string。 对于特定的应用来说可能就足够了。

但是在总结这种方法的时候,我们还要考虑一些比较晦涩的angular落案例,比如Name列包含一个通配符(被LIKE谓词识别)。我最常用的通配符是% (百分号)。 。 那么现在我们来处理这个问题,然后继续处理其他案例。

%字符的一些问题

考虑'pe%ter'的名称值。 (对于这里的示例,我使用文本string值来代替列名称。)具有“pe%ter”名称值的行将通过以下格式的查询返回:

 select ... where '|peanut|butter|' like '%|' + 'pe%ter' + '|%' 

但是,如果search条件的顺序颠倒,那么同一行将不会被返回:

 select ... where '|butter|peanut|' like '%|' + 'pe%ter' + '|%' 

我们观察到的行为有点奇怪。 更改列表中search项的顺序会更改结果集。

无论他喜欢多less,我们可能不想让pe%ter匹配花生酱。

模糊的angular落案件

(是的,我会同意这是一个不明确的情况,可能是一个不太可能被testing的情况,我们不希望在列值中使用通配符,我们可以假设应用程序阻止存储这样的值。根据我的经验,我很less见到一个数据库约束,它明确禁止LIKE比较运算符右侧的字符或模式被视为通配符。

修补一个洞

修补此漏洞的一种方法是转义%通配符。 (对于不熟悉运算符上的escape子句的人,这里是一个指向SQL Server文档的链接。

 select ... where '|peanut|butter|' like '%|' + 'pe\%ter' + '|%' escape '\' 

现在我们可以匹配文字%了。 当然,当我们有一个列名时,我们需要dynamic地转义通配符。 我们可以使用REPLACE函数来查找%字符的出现,并在每个字符的前面插入一个反斜杠字符,如下所示:

 select ... where '|pe%ter|' like '%|' + REPLACE( 'pe%ter' ,'%','\%') + '|%' escape '\' 

这样可以解决%通配符的问题。 几乎。

逃避逃跑

我们认识到我们的解决scheme引入了另一个问题 转义字符。 我们看到,我们也将需要逃避任何逃跑angular色本身的事件。 这一次,我们使用! 作为转义字符:

 select ... where '|pe%t!r|' like '%|' + REPLACE(REPLACE( 'pe%t!r' ,'!','!!'),'%','!%') + '|%' escape '!' 

下划线也是

现在我们已经开始了,我们可以添加另一个REPLACE处理下划线通配符。 为了好玩,这一次,我们将使用$作为转义字符。

 select ... where '|p_%t!r|' like '%|' + REPLACE(REPLACE(REPLACE( 'p_%t!r' ,'$','$$'),'%','$%'),'_','$_') + '|%' escape '$' 

我更喜欢这种方法来逃避,因为它可以在Oracle和MySQL以及SQL Server中工作。 (我通常使用\反斜杠作为转义字符,因为这是我们在正则expression式中使用的字符,但为什么会被约定所约束!

那些讨厌的括号

SQL Server还允许将通配符字符视为文字,方法是将它们括在括号[] 。 所以我们还没有完成修复,至less对于SQL Server来说。 由于括号对有特殊含义,我们也需要逃避这些。 如果我们设法妥善地摆脱了方括号,那么至less我们不必再用括号内的连字符和克拉来打扰了。 我们可以在括号内留下任何%_字符,因为我们基本上禁用了括号的特殊含义。

find匹配的括号对应该不那么困难。 这比处理单身%和_事件要困难得多。 (请注意,只是转义所有的括号是不够的,因为一个单独的括号被认为是一个文字,并且不需要转义。逻辑变得比我能处理的模糊一点,而不需要运行更多的testing用例。)

内联expression式变得混乱

SQL中的内联expression式越来越长,越来越丑。 我们可以让它工作,但是天堂帮助那些落后的可怜人,并且必须破译它。 对于内联expression式来说,我是一个粉丝,我倾向于在这里不使用,主要是因为我不想留下评论来解释混乱的原因,并为此道歉。

一个函数在哪里?

好的,如果我们不把它作为SQL中的内联expression式来处理,那么我们最接近的替代方法就是用户定义的函数。 而且我们知道这不会加快速度(除非我们可以像在Oracle上那样定义一个索引)。如果我们需要创build一个函数,那么我们最好在调用SQL的代码中这样做声明。

而且这个函数在行为上可能有一些差异,依赖于DBMS和版本。 (对于Java开发人员如此热衷于可交换地使用任何数据库引擎,大声疾呼。)

领域知识

我们可能对列的领域有专门的知识(也就是为该列强制执行的一组允许值),我们可能知道列中存储的值绝不会包含百分号,下划线或括号在这种情况下,我们只是简单地说一下这些案例。

存储在列中的值可以允许%或_字符,但是约束可能要求这些值被转义,或许使用定义的字符,使得LIKE比较“安全”。 同样,关于允许的一组值的快速评论,特别是哪个字符被用作转义字符,并且与Joel Spolsky的方法一致。

但是,缺乏专业知识和保证,对于我们来说,至less要考虑处理那些晦涩难懂的案例,并考虑行为是否合理和“按规范”。


其他问题重演

我相信其他人已经充分指出了其他一些通常考虑的领域:

  • SQL注入 (看上去是用户提供的信息,并将其包含在SQL文本中,而不是通过绑定variables提供),使用绑定variables不是必需的,这只是一个方便的方法来阻止SQL注入。处理方法:

  • 优化器计划使用索引扫描而不是索引查找,可能需要用于转义通配符的expression式或函数(expression式或函数的可能索引)

  • 使用文字值代替绑定variables会影响可伸缩性


结论

我喜欢Joel Spolsky的方法。 这很聪明。 它工作。

但是一看到它,我立刻就看到了一个潜在的问题,让它滑下来不是我的天性。 我不是要批评别人的努力。 我知道很多开发者都是非常个人的,因为他们投入很多,而且非常关心。 所以请理解,这不是个人攻击。 我在这里确定的是在生产中出现而不是在testing中出现的问题types。

是的,我已经远离了原来的问题。 但是还有什么地方可以留下这个笔记,关于我认为是“select”问题的一个重要问题呢?

您可以将该参数作为string传递

所以你有string

 DECLARE @tags SET @tags = 'ruby|rails|scruffy|rubyonrails' select * from Tags where Name in (SELECT item from fnSplit(@tags, '|')) order by Count desc 

然后你所要做的就是把string作为一个parameter passing。

这是我使用的分割function。

 CREATE FUNCTION [dbo].[fnSplit]( @sInputList VARCHAR(8000) -- List of delimited items , @sDelimiter VARCHAR(8000) = ',' -- delimiter that separates items ) RETURNS @List TABLE (item VARCHAR(8000)) BEGIN DECLARE @sItem VARCHAR(8000) WHILE CHARINDEX(@sDelimiter,@sInputList,0) <> 0 BEGIN SELECT @sItem=RTRIM(LTRIM(SUBSTRING(@sInputList,1,CHARINDEX(@sDelimiter,@sInputList,0)-1))), @sInputList=RTRIM(LTRIM(SUBSTRING(@sInputList,CHARINDEX(@sDelimiter,@sInputList,0)+LEN(@sDelimiter),LEN(@sInputList)))) IF LEN(@sItem) > 0 INSERT INTO @List SELECT @sItem END IF LEN(@sInputList) > 0 INSERT INTO @List SELECT @sInputList -- Put the last item in RETURN END 

我听说杰夫/乔尔在今天播客( 第34集 ,2008-12-16(MP3,31 MB),1小时03分38秒 – 1小时06分45秒)讨论了这个问题,我还以为我记得Stack Overflow正在使用LINQ to SQL ,但可能会被丢弃。 这在LINQ to SQL中也是一样的。

 var inValues = new [] { "ruby","rails","scruffy","rubyonrails" }; var results = from tag in Tags where inValues.Contains(tag.Name) select tag; 

而已。 而且,是的,LINQ已经看起来倒退了,但是Contains子句似乎倒退了。 当我不得不在工作中做一个类似的查询项目时,我自然而然地试图通过在本地数组和SQL Server表之间进行连接来做到这一点,并认为LINQ to SQL转换器足够聪明,可以处理翻译不知何故。 它没有,但它提供了一个描述性的错误信息,并指出我使用Contains

无论如何,如果您在强烈推荐的LINQPad中运行这个查询,并运行这个查询,您可以查看SQL LINQ提供程序生成的实际SQL。 它会告诉你每个参数化的值到一个IN子句中。

如果您从.NET调用,则可以使用Dapper dot net :

 string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"}; var tags = dataContext.Query<Tags>(@" select * from Tags where Name in @names order by Count desc", new {names}); 

这里Dapper是这样想的,所以你不必这样做。 当然, LINQ to SQL也有类似的可能:

 string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"}; var tags = from tag in dataContext.Tags where names.Contains(tag.Name) orderby tag.Count descending select tag; 

这可能是一个半恶劣的做法,我曾经使用过,效果相当好。

根据你的目标,它可能是有用的。

  1. 用一列创build一个临时表
  2. INSERT每个查找值插入到该列中。
  3. 您可以使用标准的JOIN规则来代替使用IN 。 (灵活性++)

这在你可以做的事情上增加了一些额外的灵活性,但是它更适合于你有一个大表查询的情况,索引很好,并且你想多次使用参数化列表。 保存必须执行两次,并手动完成所有卫生。

我从来没有到处分析究竟有多 ,但在我的情况下,它是需要的。

我们有一个函数可以创build一个你可以join的表variables:

 ALTER FUNCTION [dbo].[Fn_sqllist_to_table](@list AS VARCHAR(8000), @delim AS VARCHAR(10)) RETURNS @listTable TABLE( Position INT, Value VARCHAR(8000)) AS BEGIN DECLARE @myPos INT SET @myPos = 1 WHILE Charindex(@delim, @list) > 0 BEGIN INSERT INTO @listTable (Position,Value) VALUES (@myPos,LEFT(@list, Charindex(@delim, @list) - 1)) SET @myPos = @myPos + 1 IF Charindex(@delim, @list) = Len(@list) INSERT INTO @listTable (Position,Value) VALUES (@myPos,'') SET @list = RIGHT(@list, Len(@list) - Charindex(@delim, @list)) END IF Len(@list) > 0 INSERT INTO @listTable (Position,Value) VALUES (@myPos,@list) RETURN END 

所以:

 @Name varchar(8000) = null // parameter for search values select * from Tags where Name in (SELECT value From fn_sqllist_to_table(@Name,','))) order by Count desc 

这是严重的,但如果你保证至less有一个,你可以这样做:

 SELECT ... ... WHERE tag IN( @tag1, ISNULL( @tag2, @tag1 ), ISNULL( @tag3, @tag1 ), etc. ) 

IN('tag1','tag2','tag1','tag1','tag1')将被SQL Server轻松优化。 另外,你可以得到直接的索引

在我看来,解决这个问题最好的来源是这个网站上发布的内容:

SYSCOMMENTS。 Dinakar Nethi

 CREATE FUNCTION dbo.fnParseArray (@Array VARCHAR(1000),@separator CHAR(1)) RETURNS @T Table (col1 varchar(50)) AS BEGIN --DECLARE @T Table (col1 varchar(50)) -- @Array is the array we wish to parse -- @Separator is the separator charactor such as a comma DECLARE @separator_position INT -- This is used to locate each separator character DECLARE @array_value VARCHAR(1000) -- this holds each array value as it is returned -- For my loop to work I need an extra separator at the end. I always look to the -- left of the separator character for each array value SET @array = @array + @separator -- Loop through the string searching for separtor characters WHILE PATINDEX('%' + @separator + '%', @array) <> 0 BEGIN -- patindex matches the a pattern against a string SELECT @separator_position = PATINDEX('%' + @separator + '%',@array) SELECT @array_value = LEFT(@array, @separator_position - 1) -- This is where you process the values passed. INSERT into @T VALUES (@array_value) -- Replace this select statement with your processing -- @array_value holds the value of this element of the array -- This replaces what we just processed with and empty string SELECT @array = STUFF(@array, 1, @separator_position, '') END RETURN END 

使用:

 SELECT * FROM dbo.fnParseArray('a,b,c,d,e,f', ',') 

信誉:Dinakar Nethi

我会传递一个表types参数(因为它是SQL Server 2008 ),并做一个where exists ,或内部联接 。 您也可以使用XML,使用sp_xml_preparedocument ,然后甚至索引该临时表。

SQL Server 2016+您可以使用SPLIT_STRING函数:

 DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails'; SELECT * FROM Tags WHERE Name IN (SELECT [value] FROM STRING_SPLIT(@names, ',')) ORDER BY Count DESC; 

要么:

 DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails'; SELECT t.* FROM Tags t JOIN STRING_SPLIT(@names,',') ON t.Name = [value] ORDER BY Count DESC; 

LiveDemo

被接受的答案当然是可行的,这是一种方式,但它是反模式的。

E.按值列表查找行

这是replace常见的反模式,如在应用程序层或Transact-SQL中创builddynamicSQLstring,或使用LIKE运算符:

 SELECT ProductId, Name, Tags FROM Product WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%'; 

原来的问题有要求SQL Server 2008 由于这个问题经常被用作重复,我已经添加了这个答案作为参考。

恕我直言,正确的方法是将列表存储在一个string中(由DBMS支持的长度限制); 唯一的技巧是(为了简化处理)我在string的开头和结尾都有一个分隔符(在我的例子中是一个逗号)。 这个想法是“实时正常化”,将列表变成一列表,每列包含一行。 这可以让你转

在(ct1,ct2,ct3 … ctn)

变成一个

在(select…)

或者(我可能更喜欢的解决scheme)定期join,如果你只是添加一个“不同”,以避免列表中的重复值的问题。

不幸的是,分割string的技术是相当特定于产品的。 这里是SQL Server版本:

  with qry(n, names) as (select len(list.names) - len(replace(list.names, ',', '')) - 1 as n, substring(list.names, 2, len(list.names)) as names from (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' names) as list union all select (n - 1) as n, substring(names, 1 + charindex(',', names), len(names)) as names from qry where n > 1) select n, substring(names, 1, charindex(',', names) - 1) dwarf from qry; 

Oracle版本:

  select n, substr(name, 1, instr(name, ',') - 1) dwarf from (select n, substr(val, 1 + instr(val, ',', 1, n)) name from (select rownum as n, list.val from (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val from dual) list connect by level < length(list.val) - length(replace(list.val, ',', '')))); 

和MySQL版本:

 select pivot.n, substring_index(substring_index(list.val, ',', 1 + pivot.n), ',', -1) from (select 1 as n union all select 2 as n union all select 3 as n union all select 4 as n union all select 5 as n union all select 6 as n union all select 7 as n union all select 8 as n union all select 9 as n union all select 10 as n) pivot, (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val) as list where pivot.n < length(list.val) - length(replace(list.val, ',', '')); 

(当然,“pivot”必须返回与我们在列表中可以find的项目的最大数量一样多的行)

如果你有SQL Server 2008或更高版本,我会使用表值参数 。

如果你不幸被困在SQL Server 2005上,你可以添加一个像这样的CLR函数,

 [SqlFunction( DataAccessKind.None, IsDeterministic = true, SystemDataAccess = SystemDataAccessKind.None, IsPrecise = true, FillRowMethodName = "SplitFillRow", TableDefinintion = "s NVARCHAR(MAX)"] public static IEnumerable Split(SqlChars seperator, SqlString s) { if (s.IsNull) return new string[0]; return s.ToString().Split(seperator.Buffer); } public static void SplitFillRow(object row, out SqlString s) { s = new SqlString(row.ToString()); } 

你可以这样使用,

 declare @desiredTags nvarchar(MAX); set @desiredTags = 'ruby,rails,scruffy,rubyonrails'; select * from Tags where Name in [dbo].[Split] (',', @desiredTags) order by Count desc 

我认为这是一个情况下,静态查询是不是要走的路。 dynamic构build您的in子句列表,转义您的单引号,并dynamic构buildSQL。 在这种情况下,由于小列表的存在,您可能不会看到与任何方法有很大差异,但最有效的方法实际上是按照您的post中写入的方式发送SQL。 我认为把它写成最有效的方式是一个好习惯,而不是做最漂亮的代码,或者认为dynamic构buildSQL是不好的做法。

我已经看到,在参数变大的许多情况下,拆分函数比查询本身花费的时间更长。 SQL 2008中的表值参数的存储过程是我会考虑的唯一的其他选项,虽然这可能会比较慢。 如果你正在searchTVP的主键,那么TVP对于大型列表可能只会更快,因为无论如何,SQL将为列表build立一个临时表(如果列表很大的话)。 除非您testing,否则您将无法确定。

I have also seen stored procedures that had 500 parameters with default values of null, and having WHERE Column1 IN (@Param1, @Param2, @Param3, …, @Param500). This caused SQL to build a temp table, do a sort/distinct, and then do a table scan instead of an index seek. That is essentially what you would be doing by parameterizing that query, although on a small enough scale that it won't make a noticeable difference. I highly recommend against having NULL in your IN lists, as if that gets changed to a NOT IN it will not act as intended. You could dynamically build the parameter list, but the only obvious thing that you would gain is that the objects would escape the single quotes for you. That approach is also slightly slower on the application end since the objects have to parse the query to find the parameters. It may or may not be faster on SQL, as parameterized queries call sp_prepare, sp_execute for as many times you execute the query, followed by sp_unprepare.

The reuse of execution plans for stored procedures or parameterized queries may give you a performance gain, but it will lock you in to one execution plan determined by the first query that is executed. That may be less than ideal for subsequent queries in many cases. In your case, reuse of execution plans will probably be a plus, but it might not make any difference at all as the example is a really simple query.

Cliffs notes:

For your case anything you do, be it parameterization with a fixed number of items in the list (null if not used), dynamically building the query with or without parameters, or using stored procedures with table valued parameters will not make much of a difference. However, my general recommendations are as follows:

Your case/simple queries with few parameters:

Dynamic SQL, maybe with parameters if testing shows better performance.

Queries with reusable execution plans, called multiple times by simply changing the parameters or if the query is complicated:

SQL with dynamic parameters.

Queries with large lists:

Stored procedure with table valued parameters. If the list can vary by a large amount use WITH RECOMPILE on the stored procedure, or simply use dynamic SQL without parameters to generate a new execution plan for each query.

May be we can use XML here:

  declare @x xml set @x='<items> <item myvalue="29790" /> <item myvalue="31250" /> </items> '; With CTE AS ( SELECT x.item.value('@myvalue[1]', 'decimal') AS myvalue FROM @x.nodes('//items/item') AS x(item) ) select * from YourTable where tableColumnName in (select myvalue from cte) 

Use the following stored procedure. It uses a custom split function, which can be found here .

  create stored procedure GetSearchMachingTagNames @PipeDelimitedTagNames varchar(max), @delimiter char(1) as begin select * from Tags where Name in (select data from [dbo].[Split](@PipeDelimitedTagNames,@delimiter) end 

I'd approach this by default with passing a table valued function (that returns a table from a string) to the IN condition.

Here is the code for the UDF (I got it from Stack Overflow somewhere, i can't find the source right now)

 CREATE FUNCTION [dbo].[Split] (@sep char(1), @s varchar(8000)) 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 SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s FROM Pieces ) 

Once you got this your code would be as simple as this:

 select * from Tags where Name in (select s from dbo.split(';','ruby;rails;scruffy;rubyonrails')) order by Count desc 

Unless you have a ridiculously long string, this should work well with the table index.

If needed you can insert it into a temp table, index it, then run a join…

Another possible solution is instead of passing a variable number of arguments to a stored procedure, pass a single string containing the names you're after, but make them unique by surrounding them with '<>'. Then use PATINDEX to find the names:

 SELECT * FROM Tags WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0 

For a variable number of arguments like this the only way I'm aware of is to either generate the SQL explicitly or do something that involves populating a temporary table with the items you want and joining against the temp table.

In ColdFusion we just do:

 <cfset myvalues = "ruby|rails|scruffy|rubyonrails"> <cfquery name="q"> select * from sometable where values in <cfqueryparam value="#myvalues#" list="true"> </cfquery> 

Here's a technique that recreates a local table to be used in a query string. Doing it this way eliminates all parsing problems.

The string can be built in any language. In this example I used SQL since that was the original problem I was trying to solve. I needed a clean way to pass in table data on the fly in a string to be executed later.

Using a user defined type is optional. Creating the type is only created once and can be done ahead of time. Otherwise just add a full table type to the declaration in the string.

The general pattern is easy to extend and can be used for passing more complex tables.

 -- Create a user defined type for the list. CREATE TYPE [dbo].[StringList] AS TABLE( [StringValue] [nvarchar](max) NOT NULL ) -- Create a sample list using the list table type. DECLARE @list [dbo].[StringList]; INSERT INTO @list VALUES ('one'), ('two'), ('three'), ('four') -- Build a string in which we recreate the list so we can pass it to exec -- This can be done in any language since we're just building a string. DECLARE @str nvarchar(max); SET @str = 'DECLARE @list [dbo].[StringList]; INSERT INTO @list VALUES ' -- Add all the values we want to the string. This would be a loop in C++. SELECT @str = @str + '(''' + StringValue + '''),' FROM @list -- Remove the trailing comma so the query is valid sql. SET @str = substring(@str, 1, len(@str)-1) -- Add a select to test the string. SET @str = @str + '; SELECT * FROM @list;' -- Execute the string and see we've pass the table correctly. EXEC(@str) 

I use a more concise version of the top voted answer :

 List<SqlParameter> parameters = tags.Select((s, i) => new SqlParameter("@tag" + i.ToString(), SqlDbType.NVarChar(50)) { Value = s}).ToList(); var whereCondition = string.Format("tags in ({0})", String.Join(",",parameters.Select(s => s.ParameterName))); 

It does loop through the tag parameters twice; but that doesn't matter most of the time (it won't be your bottleneck; if it is, unroll the loop).

If you're really interested in performance and don't want to iterate through the loop twice, here's a less beautiful version:

 var parameters = new List<SqlParameter>(); var paramNames = new List<string>(); for (var i = 0; i < tags.Length; i++) { var paramName = "@tag" + i; //Include size and set value explicitly (not AddWithValue) //Because SQL Server may use an implicit conversion if it doesn't know //the actual size. var p = new SqlParameter(paramName, SqlDbType.NVarChar(50) { Value = tags[i]; } paramNames.Add(paramName); parameters.Add(p); } var inClause = string.Join(",", paramNames); 

In SQL Server 2016+ another possibility is to use the OPENJSON function.

This approach is blogged about in OPENJSON – one of best ways to select rows by list of ids .

A full worked example below

 CREATE TABLE dbo.Tags ( Name VARCHAR(50), Count INT ) INSERT INTO dbo.Tags VALUES ('VB',982), ('ruby',1306), ('rails',1478), ('scruffy',1), ('C#',1784) GO CREATE PROC dbo.SomeProc @Tags VARCHAR(MAX) AS SELECT T.* FROM dbo.Tags T WHERE T.Name IN (SELECT J.Value COLLATE Latin1_General_CI_AS FROM OPENJSON(CONCAT('[', @Tags, ']')) J) ORDER BY T.Count DESC GO EXEC dbo.SomeProc @Tags = '"ruby","rails","scruffy","rubyonrails"' DROP TABLE dbo.Tags 

Here is another alternative. Just pass a comma-delimited list as a string parameter to the stored procedure and:

 CREATE PROCEDURE [dbo].[sp_myproc] @UnitList varchar(MAX) = '1,2,3' AS select column from table where ph.UnitID in (select * from CsvToInt(@UnitList)) 

And the function:

 CREATE Function [dbo].[CsvToInt] ( @Array varchar(MAX)) returns @IntTable table (IntValue int) AS begin declare @separator char(1) set @separator = ',' declare @separator_position int declare @array_value varchar(MAX) set @array = @array + ',' while patindex('%,%' , @array) <> 0 begin select @separator_position = patindex('%,%' , @array) select @array_value = left(@array, @separator_position - 1) Insert @IntTable Values (Cast(@array_value as int)) select @array = stuff(@array, 1, @separator_position, '') end return end 

I have an answer that doesn't require a UDF, XML Because IN accepts a select statement eg SELECT * FROM Test where Data IN (SELECT Value FROM TABLE)

You really only need a way to convert the string into a table.

This can be done with a recursive CTE, or a query with a number table (or Master..spt_value)

Here's the CTE version.

 DECLARE @InputString varchar(8000) = 'ruby,rails,scruffy,rubyonrails' SELECT @InputString = @InputString + ',' ;WITH RecursiveCSV(x,y) AS ( SELECT x = SUBSTRING(@InputString,0,CHARINDEX(',',@InputString,0)), y = SUBSTRING(@InputString,CHARINDEX(',',@InputString,0)+1,LEN(@InputString)) UNION ALL SELECT x = SUBSTRING(y,0,CHARINDEX(',',y,0)), y = SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y)) FROM RecursiveCSV WHERE SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y)) <> '' OR SUBSTRING(y,0,CHARINDEX(',',y,0)) <> '' ) SELECT * FROM Tags WHERE Name IN (select x FROM RecursiveCSV) OPTION (MAXRECURSION 32767); 

Here is another answer to this problem.

(new version posted on 6/4/13).

  private static DataSet GetDataSet(SqlConnectionStringBuilder scsb, string strSql, params object[] pars) { var ds = new DataSet(); using (var sqlConn = new SqlConnection(scsb.ConnectionString)) { var sqlParameters = new List<SqlParameter>(); var replacementStrings = new Dictionary<string, string>(); if (pars != null) { for (int i = 0; i < pars.Length; i++) { if (pars[i] is IEnumerable<object>) { List<object> enumerable = (pars[i] as IEnumerable<object>).ToList(); replacementStrings.Add("@" + i, String.Join(",", enumerable.Select((value, pos) => String.Format("@_{0}_{1}", i, pos)))); sqlParameters.AddRange(enumerable.Select((value, pos) => new SqlParameter(String.Format("@_{0}_{1}", i, pos), value ?? DBNull.Value)).ToArray()); } else { sqlParameters.Add(new SqlParameter(String.Format("@{0}", i), pars[i] ?? DBNull.Value)); } } } strSql = replacementStrings.Aggregate(strSql, (current, replacementString) => current.Replace(replacementString.Key, replacementString.Value)); using (var sqlCommand = new SqlCommand(strSql, sqlConn)) { if (pars != null) { sqlCommand.Parameters.AddRange(sqlParameters.ToArray()); } else { //Fail-safe, just in case a user intends to pass a single null parameter sqlCommand.Parameters.Add(new SqlParameter("@0", DBNull.Value)); } using (var sqlDataAdapter = new SqlDataAdapter(sqlCommand)) { sqlDataAdapter.Fill(ds); } } } return ds; } 

干杯。

Here's a cross-post to a solution to the same problem. More robust than reserved delimiters – includes escaping and nested arrays, and understands NULLs and empty arrays.

C# & T-SQL string[] Pack/Unpack utility functions

You can then join to the table-valued function.

  create FUNCTION [dbo].[ConvertStringToList] (@str VARCHAR (MAX), @delimeter CHAR (1)) RETURNS @result TABLE ( [ID] INT NULL) AS BEG IN DECLARE @x XML SET @x = '<t>' + REPLACE(@str, @delimeter, '</t><t>') + '</t>' INSERT INTO @result SELECT DISTINCT xivalue('.', 'int') AS token FROM @x.nodes('//t') x(i) ORDER BY 1 RETURN END 

–YOUR QUERY

 select * from table where id in ([dbo].[ConvertStringToList(YOUR comma separated string ,',')])