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

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

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

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


当前回答

在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 

其他回答

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

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

请看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,或者做一些涉及用所需项填充临时表并与临时表连接的事情。

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

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

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

(编辑:如果表值参数不可用) 最好的方法似乎是将大量的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(processed). take (size[…])而不是list/array 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中的参数不会!)并重用查询计划(如果可用的话)。不需要单独的prepare语句,也不需要在代码中维护查询句柄!ADO。NET是这样工作的,但是Java似乎仍然使用prepare/execute by句柄(不确定)。

关于这个主题,我有自己的问题,最初建议用重复的IN子句填充,但后来更喜欢NHibernate样式语句split: 参数化SQL -在/不在与固定数量的参数,查询计划缓存优化?

这个问题仍然很有趣,即使在被问了5年多之后……

EDIT: I noted that IN queries with many values (like 250 or more) still tend to be slow, in the given case, on SQL Server. While I expected the DB to create a kind of temporary table internally and join against it, it seemed like it only repeated the single value SELECT expression n-times. Time was up to about 200ms per query - even worse than joining the original IDs retrieval SELECT against the other, related tables.. Also, there were some 10 to 15 CPU units in SQL Server Profiler, something unusual for repeated execution of the same parameterized queries, suggesting that new query plans were created on repeated calls. Maybe ad-hoc like individual queries are not worse at all. I had to compare these queries to non-split queries with changing sizes for a final conclusion, but for now, it seems like long IN clauses should be avoided anyway.

在ColdFusion中,我们只需要:

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