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

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

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

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


当前回答

这可能是一种有点讨厌的方法,我用过一次,相当有效。

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

创建一个只有一列的临时表。 将每个查找值插入到该列中。 不使用IN,只需使用标准JOIN规则。(灵活性++)

这为您所能做的事情提供了一些额外的灵活性,但它更适合这样的情况:需要查询一个大型表,有良好的索引,并且希望多次使用参数化列表。节省了执行两次,所有的卫生工作都是手动完成的。

我从来没有时间去分析它到底有多快,但在我的情况下,它是需要的。

其他回答

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

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

请看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的方法。这是聪明的。这很有效。

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

下面是我用过的一个快速而又复杂的技巧:

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

两个问题:

演出糟透了。像“%……%"查询没有索引。 请确保没有任何|、blank或null标记,否则将无法工作

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

对于这样数量可变的参数,我所知道的唯一方法是显式地生成SQL,或者做一些涉及用所需项填充临时表并与临时表连接的事情。

在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 

这是一个解决同样问题的交叉帖子。比保留分隔符更健壮-包括转义和嵌套数组,并理解null和空数组。

c# & T-SQL string[]打包/解包实用函数

然后可以连接到表值函数。