参数化SQL IN子句

时间:2008-12-03 16:16:43

标签: sql sql-server parameters

如何参数化包含带有可变数量参数的IN子句的查询,比如这个?

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

在此查询中,参数的数量可以是1到5之间的任何值。

我不希望为此(或XML)使用专用存储过程,但如果有一些特定于SQL Server 2008的优雅方式,我对此持开放态度。

40 个答案:

答案 0 :(得分:699)

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

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 injection开放。 CommandText中唯一注入的文本不是基于用户输入。它完全基于硬编码的“@tag”前缀和数组的索引。索引始终是一个整数,不是用户生成的,而且是安全的。

用户输入的值仍然填充到参数中,因此没有漏洞。

编辑:

  

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

并非缓存的查询计划没有价值,但IMO此查询的复杂程度不足以从中看到很多好处。虽然编译成本可能接近(甚至超过)执行成本,但您仍然在谈论毫秒。

如果你有足够的内存,我希望SQL Server可能会缓存一个常见的参数计数计划。我想你总是可以添加五个参数,让未指定的标签为NULL - 查询计划应该是相同的,但对我来说似乎很难看,我不确定它是否值得进行微优化(尽管如此,在Stack Overflow上 - 它可能非常值得。)

此外,SQL Server 7及更高版本将auto-parameterize queries,因此从性能角度来看,使用参数并不是必需的 - 但从安全角度来看,它是 critical - 尤其是用户输入这样的数据。

答案 1 :(得分:301)

这是我使用的一种快速而肮脏的技术:

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 "%...%"个查询未编入索引。
  • 确保您没有任何|,空白或空标记,否则无效

还有其他方法可以实现这一点,有些人可能会认为更干净,所以请继续阅读。

答案 2 :(得分:242)

对于SQL Server 2008,您可以使用table valued parameter。这有点工作,但可以说比my other method更清晰。

首先,您必须创建一个类型

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;
    });
}

答案 3 :(得分:182)

答案 4 :(得分:130)

您可以将参数作为字符串传递

所以你有字符串

DECLARE @tags

SET @tags = ‘ruby|rails|scruffy|rubyonrails’

select * from Tags 
where Name in (SELECT item from fnSplit(@tags, ‘|’))
order by Count desc

然后你要做的就是将字符串作为1参数传递。

这是我使用的分割功能。

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

答案 5 :(得分:65)

我听说Jeff / Joel今天在播客上谈论这个( episode 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子句的值。

答案 6 :(得分:47)

如果您使用.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;

答案 7 :(得分:27)

这可能是一种令人讨厌的方式,我用了一次,相当有效。

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

  1. 使用一列创建临时表
  2. INSERT每个查找值到该列。
  3. 您可以使用标准IN规则,而不是使用JOIN。 (灵活性++)
  4. 这有一些额外的灵活性,你可以做什么,但它更适合你有一个大表要查询,具有良好的索引,并且你想要多次使用参数化列表的情况。节省必须执行两次并手动完成所有卫生。

    我从来没有准确地分析它是如何快速,但在我的情况下它是必要的。

答案 8 :(得分:23)

我们有一个函数可以创建一个可以加入的表变量:

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

答案 9 :(得分:18)

这很糟糕,但如果保证至少有一个,你可以这样做:

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

IN('tag1','tag2','tag1','tag1','tag1')将很容易被SQL Server优化。另外,您可以获得直接索引搜索

答案 10 :(得分:17)

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

答案 11 :(得分:17)

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

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

答案 12 :(得分:14)

SQL Server 2016+中,您可以使用STRING_SPLIT功能:

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

accepted answer当然会起作用,这是其中一种方法,但它是反模式的。

  

<强>电子。按值列表查找行

     

这是常见反模式的替代,例如在应用程序层或Transact-SQL中创建动态SQL字符串,或者使用LIKE运算符:

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

<小时/> 原始问题有SQL Server 2008的要求。由于这个问题经常被用作副本,我已将此答案添加为参考。

答案 13 :(得分:11)

恕我直言的正确方法是将列表存储在字符串中(DBMS支持的长度有限);唯一的技巧是(为了简化处理)我在字符串的开头和结尾有一个分隔符(在我的例子中是一个逗号)。我们的想法是“动态规范化”,将列表转换为单列表,每个值包含一行。这允许你转

  

in(ct1,ct2,ct3 ... ctn)

进入

  

in(select ...)

或(我可能更喜欢的解决方案)常规联接,如果您只是添加“distinct”以避免列表中出现重复值的问题。

不幸的是,切片字符串的技术是特定于产品的。 这是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”必须返回与最大数量一样多的行 我们可以在列表中找到的项目)

答案 14 :(得分:10)

如果您有SQL Server 2008或更高版本,我会使用Table Valued Parameter

如果你不幸被卡在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

答案 15 :(得分:9)

我认为这是静态查询不可行的情况。动态构建in子句的列表,转义单引号,并动态构建SQL。在这种情况下,由于列表很小,您可能不会发现任何方法有太大差异,但最有效的方法实际上是发送SQL,就像在帖子中写的一样。我认为以最有效的方式编写它是一个好习惯,而不是做出最漂亮的代码,或者认为动态构建SQL是不好的做法。

我已经看到,在参数变大的许多情况下,分割函数的执行时间比查询本身要长。 SQL 2008中具有表值参数的存储过程是我考虑的唯一其他选项,尽管在您的情况下这可能会更慢。如果要搜索TVP的主键,TVP可能只会对大型列表更快,因为SQL无论如何都会为列表构建一个临时表(如果列表很大)。除非你测试它,否则你不会确定。

我还看到存储过程有500个参数,默认值为null,并且WHERE Column1 IN(@ Param1,@ Param2,@ Param3,...,@ Param500)。这导致SQL构建临时表,执行排序/分离,然后执行表扫描而不是索引搜索。这基本上就是你通过参数化查询来做的事情,虽然规模很小,但不会产生明显的差异。我强烈建议您不要在IN列表中使用NULL,就好像它被更改为NOT IN一样,它不会按预期运行。您可以动态构建参数列表,但唯一明显的事情是对象将为您转义单引号。这种方法在应用程序端也略慢,因为对象必须解析查询以查找参数。它可能会或可能不会更快SQL,因为参数化查询调用sp_prepare,sp_execute执行查询的次数,然后是sp_unprepare。

重用存储过程或参数化查询的执行计划可能会为您带来性能提升,但它会将您锁定到由执行的第一个查询确定的一个执行计划。在许多情况下,这可能不太适合后续查询。在您的情况下,重用执行计划可能是一个加分,但它可能没有任何区别,因为该示例是一个非常简单的查询。

Cliffs说明:

对于您所做的任何事情,无论是在列表中使用固定数量的项进行参数化(如果不使用,则为null),使用或不使用参数动态构建查询,或者使用具有表值参数的存储过程将不会产生太多差异。但是,我的一般建议如下:

您的案例/简单查询参数很少:

动态SQL,如果测试显示更好的性能,可能带参数。

使用可重复使用的执行计划进行查询,通过简单地更改参数或查询是否复杂来多次调用:

带动态参数的SQL。

查询大型列表:

具有表值参数的存储过程。如果列表变化很大,请在存储过程中使用WITH RECOMPILE,或者只使用不带参数的动态SQL为每个查询生成新的执行计划。

答案 16 :(得分:9)

我们可以在这里使用XML:

    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)

答案 17 :(得分:9)

默认情况下,我会将表值函数(从字符串返回一个表)传递给IN条件。

以下是UDF 的代码(我从某处获得Stack Overflow,我现在找不到源代码)

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
  )

一旦你得到了这个,你的代码就会这么简单:

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

除非你有一个可笑的长字符串,否则这应该适用于表索引。

如果需要,您可以将其插入临时表,索引它,然后运行连接...

答案 18 :(得分:8)

另一种可能的解决方案是,不是将可变数量的参数传递给存储过程,而是传递包含您所追求的名称的单个字符串,但通过用'&lt;&gt;'包围它们来使它们成为唯一的。然后使用PATINDEX查找名称:

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

答案 19 :(得分:8)

使用以下存储过程。它使用自定义拆分功能,可以找到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

答案 20 :(得分:7)

这是另一种选择。只需将逗号分隔的列表作为字符串参数传递给存储过程,然后:

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

功能:

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

答案 21 :(得分:7)

如果我们在IN子句中存储了以逗号(,)分隔的字符串,我们可以使用charindex函数来获取值。如果使用.NET,则可以使用SqlParameters进行映射。

DDL脚本:

CREATE TABLE Tags
    ([ID] int, [Name] varchar(20))
;

INSERT INTO Tags
    ([ID], [Name])
VALUES
    (1, 'ruby'),
    (2, 'rails'),
    (3, 'scruffy'),
    (4, 'rubyonrails')
;

<强> T-SQL:

DECLARE @Param nvarchar(max)

SET @Param = 'ruby,rails,scruffy,rubyonrails'

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

您可以在.NET代码中使用上述语句,并使用SqlParameter映射参数。

Fiddler demo

修改 使用以下脚本创建名为SelectedTags的表。

DDL脚本:

Create table SelectedTags
(Name nvarchar(20));

INSERT INTO SelectedTags values ('ruby'),('rails')

T-SQL:

DECLARE @list nvarchar(max)
SELECT @list=coalesce(@list+',','')+st.Name FROM SelectedTags st

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

答案 22 :(得分:7)

这是一种重新创建要在查询字符串中使用的本地表的技术。这样做可以消除所有解析问题。

该字符串可以用任何语言构建。在这个例子中,我使用SQL,因为那是我试图解决的原始问题。我需要一种干净的方法来动态传递表格数据,以便稍后执行。

使用用户定义的类型是可选的。创建类型只创建一次,可以提前完成。否则只需在字符串中的声明中添加完整的表类型。

一般模式易于扩展,可用于传递更复杂的表。

-- 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)

答案 23 :(得分:7)

对于像这样的可变数量的参数,我所知道的唯一方法是显式生成SQL或执行一些涉及使用所需项目填充临时表并加入临时表的内容。

答案 24 :(得分:7)

ColdFusion我们只是这样做:

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

答案 25 :(得分:6)

我的答案不需要UDF,XML 因为IN接受select语句 例如SELECT * FROM测试数据输入(SELECT值FROM TABLE)

你真的只需要一种方法将字符串转换成表格。

这可以通过递归CTE或带有数字表(或Master..spt_value)的查询来完成

这是CTE版本。

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);

答案 26 :(得分:6)

在SQL Server 2016+中,另一种可能性是使用OPENJSON函数。

此方法在OPENJSON - one of best ways to select rows by list of ids

中发表了博文

下面的完整工作示例

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 

答案 27 :(得分:6)

我使用更简洁的版本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)));

它会循环标记参数两次;但这在大多数情况下并不重要(它不会成为你的瓶颈;如果是,则展开循环)。

如果您真的对性能感兴趣并且不想两次遍历循环,那么这里的版本就不那么漂亮了:

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);

答案 28 :(得分:5)

这是这个问题的另一个答案。

(新版于2013年6月4日发布)。

    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;
    }

干杯。

答案 29 :(得分:4)

唯一获胜的举动就是不参加比赛。

对你来说没有无限的可变性。只有有限的可变性。

在SQL中你有一个这样的子句:

and ( {1}==0 or b.CompanyId in ({2},{3},{4},{5},{6}) )

在C#代码中,你可以这样做:

  int origCount = idList.Count;
  if (origCount > 5) {
    throw new Exception("You may only specify up to five originators to filter on.");
  }
  while (idList.Count < 5) { idList.Add(-1); }  // -1 is an impossible value
  return ExecuteQuery<PublishDate>(getValuesInListSQL, 
               origCount,   
               idList[0], idList[1], idList[2], idList[3], idList[4]);

所以基本上如果计数为0则没有过滤器,一切都会通过。如果计数高于0,则该值必须在列表中,但列表已填充到五个不可能的值(因此SQL仍然有意义)

有时,蹩脚的解决方案是唯一真正有效的解决方案。

答案 30 :(得分:4)

这是针对同一问题的解决方案的交叉帖子。比保留分隔符更强大 - 包括转义和嵌套数组,并理解NULL和空数组。

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

然后您可以加入表值函数。

答案 31 :(得分:3)

(编辑:如果表值参数不可用) Best似乎是将大量IN参数拆分为多个具有固定长度的查询,因此您有许多已知的SQL语句具有固定的参数计数且没有虚拟/重复值,也没有解析字符串,XML等

以下是C#中关于此主题的一些代码:

public static T[][] SplitSqlValues<T>(IEnumerable<T> values)
{
    var sizes = new int[] { 1000, 500, 250, 125, 63, 32, 16, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
    int processed = 0;
    int currSizeIdx = sizes.Length - 1; /* start with last (smallest) */
    var splitLists = new List<T[]>();

    var valuesDistSort = values.Distinct().ToList(); /* remove redundant */
    valuesDistSort.Sort();
    int totalValues = valuesDistSort.Count;

    while (totalValues > sizes[currSizeIdx] && currSizeIdx > 0)
    currSizeIdx--; /* bigger size, by array pos. */

    while (processed < totalValues)
    {
        while (totalValues - processed < sizes[currSizeIdx]) 
            currSizeIdx++; /* smaller size, by array pos. */
        var partList = new T[sizes[currSizeIdx]];
        valuesDistSort.CopyTo(processed, partList, 0, sizes[currSizeIdx]);
        splitLists.Add(partList);
        processed += sizes[currSizeIdx];
    }
    return splitLists.ToArray();
}

(你可能有进一步的想法,省略排序,使用valuesDistSort.Skip(已处理).Take(size [...])而不是列表/数组CopyTo)。

插入参数变量时,可以创建如下内容:

foreach(int[] partList in splitLists)
{
    /* here: question mark for param variable, use named/numbered params if required */
    string sql = "select * from Items where Id in("
        + string.Join(",", partList.Select(p => "?")) 
        + ")"; /* comma separated ?, one for each partList entry */

    /* create command with sql string, set parameters, execute, merge results */
}

我已经看过NHibernate对象关系映射器生成的SQL(当查询数据以创建对象时),这看起来最适合多个查询。在NHibernate中,可以指定批量大小;如果必须提取许多对象数据行,它会尝试检索等于批量大小的行数

SELECT * FROM MyTable WHERE Id IN (@p1, @p2, @p3, ... , @p[batch-size])

,而不是发送数百或数千

SELECT * FROM MyTable WHERE Id=@id

当剩余的ID小于批量大小但仍然多于一个时,它会分成较小的语句,但仍然具有一定的长度。

如果批量大小为100,查询包含118个参数,则会创建3个查询:

  • 一个有100个参数(批量大小),
  • 然后一个12
  • 和另一个6,

但是没有118或18.这样,它将可能的SQL语句限制为可能已知的语句,从而防止了太多不同的,因此太多的查询计划,这些查询计划填充了缓存,而且很多部分永远不会被重用。上面的代码也是如此,但长度分别为1000,500,250,125,63,32,16,10到-1。还会拆分包含1000多个元素的参数列表,以防止因大小限制而导致数据库错误。

无论如何,最好有一个直接发送参数化SQL的数据库接口,而不需要单独的Prepare语句和句柄来调用。像SQL Server和Oracle这样的数据库通过字符串相等性来记住SQL(值更改,SQL中没有绑定参数!)并重用查询计划(如果可用)。不需要单独的准备语句,以及代码中查询句柄的繁琐维护! ADO.NET的工作方式与此类似,但似乎Java仍然使用handle(不确定)准备/执行。

我对这个主题有我自己的问题,最初建议用重复项填充IN子句,但后来更喜欢NHibernate样式语句拆分: Parameterized SQL - in / not in with fixed numbers of parameters, for query plan cache optimization?

这个问题仍然很有意思,甚至超过5年后被问到......

编辑:我注意到在SQL Server上,在给定的情况下,具有许多值(如250或更多)的IN查询仍然很慢。虽然我希望DB在内部创建一种临时表并加入它,但它似乎只重复单值SELECT表达式n次。每个查询的时间长达200毫秒 - 甚至比将原始ID检索SELECT与其他相关表连接更糟糕。此外,SQL Server Profiler中有大约10到15个CPU单元,这对于重复执行相同参数化而言是不常见的查询,建议在重复调用时创建新的查询计划。也许像个人查询这样的临时性并不会更糟。我不得不将这些查询与非拆分查询进行比较,最终结论的大小不断变化,但就目前而言,似乎应该避免使用长IN条款。

答案 32 :(得分:3)

    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 x.i.value('.', 'int') AS token
    FROM @x.nodes('//t') x(i)
    ORDER BY 1

RETURN
END

- 你的查询

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

答案 33 :(得分:3)

您可以通过执行以下操作以可重复使用的方式执行此操作 -

public static class SqlWhereInParamBuilder
{
    public static string BuildWhereInClause<t>(string partialClause, string paramPrefix, IEnumerable<t> parameters)
    {
        string[] parameterNames = parameters.Select(
            (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
            .ToArray();

        string inClause = string.Join(",", parameterNames);
        string whereInClause = string.Format(partialClause.Trim(), inClause);

        return whereInClause;
    }

    public static void AddParamsToCommand<t>(this SqlCommand cmd, string paramPrefix, IEnumerable<t> parameters)
    {
        string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray();

        string[] parameterNames = parameterValues.Select(
            (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
            ).ToArray();

        for (int i = 0; i < parameterNames.Length; i++)
        {
            cmd.Parameters.AddWithValue(parameterNames[i], parameterValues[i]);
        }
    }
}

有关详细信息,请查看此博客文章 - Parameterized SQL WHERE IN clause c#

答案 34 :(得分:2)

使用动态查询。前端只生成所需的格式:

DECLARE @invalue VARCHAR(100)
SELECT @invalue = '''Bishnu'',''Gautam'''

DECLARE @dynamicSQL VARCHAR(MAX)
SELECT @dynamicSQL = 'SELECT * FROM #temp WHERE [name] IN (' + @invalue + ')'
EXEC (@dynamicSQL)

SQL Fiddle

答案 35 :(得分:2)

这是Mark Bracket的优秀答案中可重复使用的解决方案。

扩展方法:

for

用法:

public static class ParameterExtensions
{
    public static Tuple<string, SqlParameter[]> ToParameterTuple<T>(this IEnumerable<T> values)
    {
        var createName = new Func<int, string>(index => "@value" + index.ToString());
        var paramTuples = values.Select((value, index) => 
        new Tuple<string, SqlParameter>(createName(index), new SqlParameter(createName(index), value))).ToArray();
        var inClause = string.Join(",", paramTuples.Select(t => t.Item1));
        var parameters = paramTuples.Select(t => t.Item2).ToArray();
        return new Tuple<string, SqlParameter[]>(inClause, parameters);
    }
}

答案 36 :(得分:1)

有一种很好,简单且经过测试的方法:

/* Create table-value string: */
CREATE TYPE [String_List] AS TABLE ([Your_String_Element] varchar(max) PRIMARY KEY);
GO
/* Create procedure which takes this table as parameter: */

CREATE PROCEDURE [dbo].[usp_ListCheck]
@String_List_In [String_List] READONLY  
AS   
SELECT a.*
FROM [dbo].[Tags] a
JOIN @String_List_In b ON a.[Name] = b.[Your_String_Element];

我已经开始使用这种方法来修复我们在实体框架中遇到的问题(对我们的应用程序来说不够健壮)。所以我们决定给Dapper(与Stack相同)一个机会。同时将您的字符串列表指定为具有PK列的表,可以修复您的执行计划。 Here是一篇关于如何将表格传递给Dapper的好文章 - 全部快速且干净。

答案 37 :(得分:0)

创建一个存储名称的临时表,然后使用以下查询:

select * from Tags 
where Name in (select distinct name from temp)
order by Count desc

答案 38 :(得分:0)

在SQL SERVER 2016或更高版本中,您可以使用 STRING_SPLIT

DECLARE @InParaSeprated VARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails'
DECLARE @Delimeter VARCHAR(10) = ','
SELECT 
    * 
FROM 
    Tags T
    INNER JOIN STRING_SPLIT(@InputParameters,@Delimeter) SS ON T.Name = SS.value
ORDER BY 
    Count DESC

之所以使用它,是因为有时连接速度比 Like Operator 在查询中的工作速度快。
此外,您可以随意输入任意数量的分隔格式的任意数量的输入。
我喜欢这个..

答案 39 :(得分:0)

步骤1:-

string[] Ids = new string[] { "3", "6", "14" };
string IdsSP = string.Format("'|{0}|'", string.Join("|", Ids));

步骤2:-

@CurrentShipmentStatusIdArray [nvarchar](255) = NULL

步骤3:-

Where @CurrentShipmentStatusIdArray is null or @CurrentShipmentStatusIdArray LIKE '%|' + convert(nvarchar,Shipments.CurrentShipmentStatusId) + '|%'

Where @CurrentShipmentStatusIdArray is null or @CurrentShipmentStatusIdArray LIKE '%|' + Shipments.CurrentShipmentStatusId+ '|%'