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

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

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

我不喜欢使用专门的存储过程(或XML),但如果有一些特定于SQL Server 2008的优雅方式,我愿意接受。


当前回答

你可以参数化每个值,就像这样:

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

用户输入的值仍然被填充到参数中,因此不存在漏洞。

编辑:

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

并不是说缓存的查询计划没有价值,但在我看来,这个查询还没有复杂到可以从中看到很多好处。虽然编译成本可能接近(甚至超过)执行成本,但仍然是毫秒级的。

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

此外,SQL Server 7及后续版本将自动参数化查询,因此从性能的角度来看,使用参数并不是真正必要的——然而,从安全的角度来看,它是至关重要的——特别是对于这样的用户输入数据。

其他回答

如果你从。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});

这里是达普在思考,所以你不用思考。当然,类似的事情也可能发生在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;

在默认情况下,我将通过向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

除非你有一个长得离谱的字符串,否则这应该与表索引一起工作得很好。

如果需要,你可以把它插入一个临时表,索引它,然后运行一个连接…

也许我们可以在这里使用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)

最初的问题是“如何参数化查询……”

这不是最初那个问题的答案。在其他答案中有一些很好的演示。

请看Mark Brackett的第一个答案(第一个答案以“你可以参数化每个值”开头)和Mark Brackett的第二个答案,这是我(和其他231人)点赞的首选答案。在他的回答中给出的方法允许1)有效地使用绑定变量,2)谓词是sargable。

选择答案

我在这里讨论的是Joel Spolsky的回答中给出的方法,即“选择”答案作为正确答案。

Joel Spolsky的方法很聪明。它的工作原理是合理的,它将表现出可预测的行为和可预测的性能,给定“正常”值,并使用规范的边缘情况,如NULL和空字符串。对于特定的应用,它可能是足够的。

但是,在泛化这种方法方面,让我们还考虑更模糊的情况,比如Name列包含通配符(由like谓词识别)。我看到最常用的通配符是%(百分号)。我们先来解决这个问题,然后再讨论其他情况。

%字符有一些问题

考虑Name值为'pe%ter'。(对于这里的示例,我使用一个字面值字符串值来代替列名。)Name值为" pe%ter'的行将由以下形式的查询返回:

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

但是,如果搜索词的顺序颠倒,则不会返回同一行:

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

我们观察到的行为有点奇怪。更改列表中搜索词的顺序将更改结果集。

不用说,我们可能不希望孩子吃花生酱,不管他多么喜欢花生酱。

晦涩的角落案例

(是的,我同意这是一个模糊的案例。可能是一个不太可能被测试的。我们不期望列值中有通配符。我们可以假设应用程序阻止存储这样的值。但根据我的经验,我很少看到数据库约束明确禁止LIKE比较运算符右侧的通配符或模式。

修补洞

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

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

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

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

这样就解决了%通配符的问题。几乎。

逃离逃离

我们认识到我们的解决方案引入了另一个问题。转义字符。我们还需要对任何出现的转义字符本身进行转义。这一次,我们使用!作为转义字符:

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中工作。(我通常使用\反斜杠作为转义字符,因为这是正则表达式中使用的字符。但为什么要被传统束缚呢!

这些讨厌的括号

SQL Server also allows for wildcard characters to be treated as literals by enclosing them in brackets []. So we're not done fixing yet, at least for SQL Server. Since pairs of brackets have special meaning, we'll need to escape those as well. If we manage to properly escape the brackets, then at least we won't have to bother with the hyphen - and the carat ^ within the brackets. And we can leave any % and _ characters inside the brackets escaped, since we'll have basically disabled the special meaning of the brackets.

找到匹配的括号对应该没有那么难。这比处理单例%和_的出现要困难一些。(注意,仅仅转义所有出现的方括号是不够的,因为单例方括号被认为是一个文字,不需要转义。逻辑变得有点模糊,如果不运行更多的测试用例,我就无法处理。)

内联表达式变得混乱

SQL中的内联表达式越来越长,越来越难看。我们也许可以让它工作,但上帝保佑那些可怜的灵魂回来,必须破译它。作为内联表达式的粉丝,我在这里倾向于不使用它,主要是因为我不想留下评论解释混乱的原因,并为此道歉。

函数在哪里?

如果我们不把它作为SQL中的内联表达式来处理,我们拥有的最接近的替代方法是用户定义函数。而且我们知道这不会加快任何速度(除非我们可以在上面定义一个索引,就像我们在Oracle中所做的那样)。如果我们必须创建一个函数,最好在调用SQL语句的代码中执行。

该函数可能在行为上有一些差异,这取决于DBMS和版本。(这是对所有热衷于可互换使用任何数据库引擎的Java开发人员的一种呼吁。)

领域知识

我们可能对列的域有专门的知识(也就是说,对列强制执行的允许值集。我们可能预先知道,存储在列中的值永远不会包含百分号、下划线或括号对。在这种情况下,我们只包含一个简短的注释,说明这些情况都被涵盖了。

存储在列中的值可能允许使用%或_字符,但约束可能要求这些值转义,可能使用已定义的字符,这样这些值比较“安全”。再次,快速评论一下允许的值集,特别是哪个字符被用作转义字符,并遵循Joel Spolsky的方法。

但是,在没有专业知识和保证的情况下,我们至少要考虑处理那些模糊的极端情况,并考虑行为是否合理,是否“符合规范”。


其他问题概述

我相信其他人已经充分指出了其他一些普遍考虑的关切领域:

SQL injection (taking what would appear to be user supplied information, and including that in the SQL text rather than supplying them through bind variables. Using bind variables isn't required, it's just one convenient approach to thwart with SQL injection. There are other ways to deal with it: optimizer plan using index scan rather than index seeks, possible need for an expression or function for escaping wildcards (possible index on expression or function) using literal values in place of bind variables impacts scalability


结论

我喜欢Joel Spolsky的方法。这是聪明的。这很有效。

但当我看到它的时候,我立刻发现了潜在的问题,而我的天性不是让它顺其自然。我并不是要批评别人的努力。我知道许多开发者都非常注重自己的工作,因为他们在其中投入了大量精力,并且非常关心自己的工作。所以请理解,这不是人身攻击。我在这里确定的是在生产而不是测试中出现的问题类型。

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

OPENJSON中有关于这种方法的博客——按id列表选择行的最佳方法之一。

下面是一个完整的示例

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