如何参数化包含IN
具有可变数量参数的子句的查询,例如这个?
SELECT * FROM Tags
WHERE Name IN ('ruby','rails','scruffy','rubyonrails')
ORDER BY Count DESC
在此查询中,参数的数量可以是 1 到 5 之间的任何值。
我不希望为此(或 XML)使用专用的存储过程,但如果有一些特定于SQL Server 2008的优雅方式,我对此持开放态度。
如何参数化包含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 本身)。
并不是说缓存的查询计划没有价值,但 IMO 这个查询还不够复杂,无法从中看到很多好处。虽然编译成本可能接近(甚至超过)执行成本,但您仍然在谈论毫秒。
如果您有足够的 RAM,我希望 SQL Server 也可能会缓存一个用于常见参数计数的计划。我想你总是可以添加五个参数,让未指定的标签为 NULL - 查询计划应该是相同的,但它对我来说看起来很丑,我不确定它是否值得微优化(虽然,在 Stack Overflow 上 - 这可能非常值得)。
此外,SQL Server 7 及更高版本将自动参数化查询,因此从性能的角度来看,使用参数并不是真正必要的——然而,从安全的角度来看,它是至关重要的——尤其是对于像这样的用户输入数据。
这是我使用的一种快速而肮脏的技术:
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 "%...%"
查询未编入索引。|
、空白或空标签,否则这将不起作用还有其他方法可以实现这一点,有些人可能会认为更清洁,所以请继续阅读。
对于 SQL Server 2008,您可以使用表值参数。这有点工作,但可以说它比我的其他方法更干净。
首先,您必须创建一个类型
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= new SqlMetaData(columnName, SqlDbType.NVarChar, 50); //50 as per SQL Type
return values.Select(v =>
{
var r = new SqlDataRecord(metadata);
r.SetValues(v);
return r;
});
}
根据@Doug更新
请尽量避免var metadata = SqlMetaData.InferFromValue(firstRecord, columnName);
它设置了第一个值的长度,因此如果第一个值是 3 个字符,则其设置的最大长度为 3,如果超过 3 个字符,其他记录将被截断。
所以,请尝试使用:var metadata= new SqlMetaData(columnName, SqlDbType.NVarChar, maxLen);
注意:-1
最大长度。
最初的问题是“我如何参数化查询......”
让我在这里声明,这不是原始问题的答案。在其他好的答案中已经有一些证明。
话虽如此,继续标记这个答案,否决它,将其标记为不是答案......做任何你认为是正确的事情。
请参阅 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' + '|%'
我们观察到的行为有点奇怪。更改列表中搜索词的顺序会更改结果集。
几乎不用说,我们可能不想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 还允许通过将通配符括在方括号中来将它们视为文字[]
。所以我们还没有完成修复,至少对于 SQL Server。由于括号对具有特殊含义,因此我们也需要将其转义。如果我们设法正确地避开括号,那么至少我们不必为括号内的连字符-
和克拉而烦恼^
。我们可以将括号内的任何%
和_
字符转义,因为我们基本上会禁用括号的特殊含义。
找到匹配的括号对应该不难。这比处理单例 % 和 _ 的出现要困难一些。(请注意,仅仅转义所有出现的括号是不够的,因为单例括号被认为是文字,不需要转义。逻辑变得有点模糊,我在不运行更多测试用例的情况下无法处理.)
内联表达式变得混乱
SQL 中的内联表达式越来越长,越来越难看。我们或许可以让它发挥作用,但是上天会帮助那些落后并不得不破译它的可怜的灵魂。作为内联表达式的粉丝,我倾向于不在这里使用一个,主要是因为我不想留下评论来解释混乱的原因,并为此道歉。
一个函数在哪里?
好的,所以,如果我们不将其作为 SQL 中的内联表达式来处理,那么我们最接近的替代方案是用户定义的函数。而且我们知道这不会加速任何事情(除非我们可以在其上定义索引,就像我们可以使用 Oracle 一样。)如果我们必须创建一个函数,我们最好在调用 SQL 的代码中这样做陈述。
并且该函数可能在行为上存在一些差异,具体取决于 DBMS 和版本。(向所有热衷于能够互换使用任何数据库引擎的 Java 开发人员大声疾呼。)
领域知识
我们可能对列的域有专门的知识(即为列强制执行的一组允许值。我们可能先验地知道存储在列中的值永远不会包含百分号、下划线或括号在这种情况下,我们只包含一个快速评论,说明这些情况已涵盖。
存储在列中的值可能允许使用 % 或 _ 字符,但约束可能要求对这些值进行转义,可能使用定义的字符,以便这些值是 LIKE 比较“安全”的。同样,快速评论一下允许的值集,特别是哪个字符用作转义字符,并采用 Joel Spolsky 的方法。
但是,在没有专业知识和保证的情况下,重要的是我们至少要考虑处理那些晦涩难懂的极端情况,并考虑行为是否合理和“符合规范”。
其他问题重述
我相信其他人已经充分指出了一些其他普遍考虑的关注领域:
SQL 注入(获取看似用户提供的信息,并将其包含在 SQL 文本中,而不是通过绑定变量提供它们。不需要使用绑定变量,它只是阻止 SQL 注入的一种方便方法。还有其他处理方法:
优化器计划使用索引扫描而不是索引搜索,可能需要一个表达式或函数来转义通配符(表达式或函数上的可能索引)
使用文字值代替绑定变量会影响可伸缩性
结论
我喜欢 Joel Spolsky 的方法。这很聪明。它有效。
但一看到它,我就立即看出它有潜在的问题,让它滑落不是我的本性。我并不是要批评别人的努力。我知道许多开发人员非常重视他们的工作,因为他们在其中投入了大量资金并且非常关心它。所以请理解,这不是人身攻击。我在这里确定的是在生产中而不是在测试中出现的问题类型。
是的,我与最初的问题相去甚远。但是,关于我认为与问题的“选定”答案有关的重要问题,还有什么地方可以留下这个注释呢?
您可以将参数作为字符串传递
所以你有字符串
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
我今天听到 Jeff/Joel 在播客上谈论这个(第 34 集,2008-12-16(MP3,31 MB),1 小时 3 分 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;
在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;
接受的答案当然会起作用,这是要走的路之一,但它是反模式的。
E. 按值列表查找行
这可以替代常见的反模式,例如在应用层或 Transact-SQL 中创建动态 SQL 字符串,或者使用 LIKE 运算符:
SELECT ProductId, Name, Tags FROM Product WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%';
附录:
为了改进STRING_SPLIT
表函数行估计,将拆分值具体化为临时表/表变量是一个好主意:
DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails,sql';
CREATE TABLE #t(val NVARCHAR(120));
INSERT INTO #t(val) SELECT s.[value] FROM STRING_SPLIT(@names, ',') s;
SELECT *
FROM Tags tg
JOIN #t t
ON t.val = tg.TagName
ORDER BY [Count] DESC;
SQL Server 2008
。因为这个问题经常被用作重复,所以我添加了这个答案作为参考。
这可能是一种半讨厌的方式,我用过一次,相当有效。
根据您的目标,它可能有用。
INSERT
每个查找值到该列。IN
然后,您可以只使用标准JOIN
规则,而不是使用。(灵活性++)这在您可以做的事情上增加了一些灵活性,但它更适合您有一个大表要查询、具有良好索引并且您希望多次使用参数化列表的情况。无需执行两次并手动完成所有卫生工作。
我从来没有弄清楚它到底有多快,但在我的情况下它是需要的。
我们有一个函数可以创建一个表变量,您可以加入:
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
这很恶心,但如果你保证至少有一个,你可以这样做:
SELECT ...
...
WHERE tag IN( @tag1, ISNULL( @tag2, @tag1 ), ISNULL( @tag3, @tag1 ), etc. )
拥有 IN('tag1', 'tag2', 'tag1', 'tag1', 'tag1' ) 将很容易被 SQL Server 优化掉。另外,您可以获得直接索引搜索
我会传递一个表类型参数(因为它是SQL Server 2008),然后执行一个where exists
, 或内部连接。您也可以使用 XML,使用sp_xml_preparedocument
,然后甚至索引该临时表。
在我看来,解决这个问题的最佳来源是这个网站上发布的内容:
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
恕我直言,正确的方法是将列表存储在字符串中(长度受 DBMS 支持的限制);唯一的技巧是(为了简化处理)我在字符串的开头和结尾有一个分隔符(在我的例子中是逗号)。这个想法是“即时规范化”,将列表变成一个包含每个值一行的单列表。这使您可以转动
在 (ct1,ct2, ct3 ... ctn)
成一个
在(选择...)
或者(我可能更喜欢的解决方案)一个常规连接,如果你只是添加一个“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;
甲骨文版本:
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”必须返回与我们可以在列表中找到的最大项目数一样多的行)
如果您有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
我认为这是静态查询不可行的情况。为您的 in 子句动态构建列表,转义您的单引号,并动态构建 SQL。在这种情况下,由于列表很小,您可能不会看到与任何方法有太大区别,但最有效的方法确实是完全按照您的帖子中所写的方式发送 SQL。我认为以最有效的方式编写它是一个好习惯,而不是做最漂亮的代码,或者认为动态构建 SQL 是不好的做法。
我已经看到在参数变大的许多情况下,拆分函数的执行时间比查询本身要长。在 SQL 2008 中具有表值参数的存储过程是我会考虑的唯一其他选择,尽管在您的情况下这可能会更慢。如果您在 TVP 的主键上搜索,TVP 可能只会对大型列表更快,因为 SQL 无论如何都会为列表构建一个临时表(如果列表很大)。除非您对其进行测试,否则您将无法确定。
我还看到存储过程有 500 个参数,默认值为 null,并且具有 WHERE Column1 IN (@Param1、@Param2、@Param3、...、@Param500)。这导致 SQL 构建一个临时表,进行排序/区分,然后进行表扫描而不是索引查找。这基本上就是您通过参数化该查询所做的事情,尽管规模足够小,不会产生明显的差异。我强烈建议不要在您的 IN 列表中使用 NULL,因为如果将其更改为 NOT IN,它将无法按预期运行。您可以动态构建参数列表,但唯一明显的好处是对象会为您转义单引号。这种方法在应用程序端也稍微慢一些,因为对象必须解析查询才能找到参数。
重用存储过程或参数化查询的执行计划可能会给您带来性能提升,但它会将您锁定在一个由执行的第一个查询确定的执行计划中。在许多情况下,这对于后续查询可能不太理想。在您的情况下,重用执行计划可能是一个加分项,但它可能根本没有任何区别,因为该示例是一个非常简单的查询。
悬崖笔记:
对于您所做的任何事情,无论是在列表中使用固定数量的项目进行参数化(如果不使用,则为 null),使用或不使用参数动态构建查询,或者使用带有表值参数的存储过程不会有太大区别. 但是,我的一般建议如下:
您的案例/简单查询,参数很少:
动态 SQL,如果测试显示更好的性能,可能带有参数。
具有可重用执行计划的查询,只需更改参数或查询很复杂即可多次调用:
带有动态参数的 SQL。
具有大列表的查询:
具有表值参数的存储过程。如果列表可能相差很大,请在存储过程上使用 WITH RECOMPILE,或者简单地使用不带参数的动态 SQL 为每个查询生成新的执行计划。
也许我们可以在这里使用 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)
如果我们在 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。
编辑: 使用以下脚本创建名为 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
默认情况下,我会通过将表值函数(从字符串返回表)传递给 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
除非你有一个长得离谱的字符串,否则这应该适用于表索引。
如果需要,您可以将其插入临时表,对其进行索引,然后运行连接...
另一种可能的解决方案是,不是将可变数量的参数传递给存储过程,而是传递一个包含您所追求的名称的字符串,但通过用“<>”将它们括起来使它们唯一。然后使用 PATINDEX 查找名称:
SELECT *
FROM Tags
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0
使用以下存储过程。它使用自定义拆分功能,可在此处找到。
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
这是另一种选择。只需将逗号分隔的列表作为字符串参数传递给存储过程,然后:
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
对于像这样可变数量的参数,我知道的唯一方法是显式生成 SQL,或者执行一些涉及使用您想要的项目填充临时表并加入临时表的操作。
在ColdFusion中,我们只做:
<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
<cfquery name="q">
select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
</cfquery>
这是一种重新创建要在查询字符串中使用的本地表的技术。这样做可以消除所有解析问题。
该字符串可以用任何语言构建。在这个例子中,我使用了 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)
在 SQL Server 2016+ 中,另一种可能性是使用该OPENJSON
函数。
这种方法在OPENJSON 中有博客——这是通过 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
我有一个不需要 UDF、XML 的答案,因为 IN 接受一个选择语句,例如 SELECT * FROM Test where Data IN (SELECT Value 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);
我使用投票最多的答案的更简洁版本:
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);
这是这个问题的另一个答案。
(新版本于 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;
}
干杯。
唯一获胜的举措是不玩。
没有无限的可变性。只有有限的可变性。
在 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 仍然有意义)
有时,蹩脚的解决方案是唯一真正有效的解决方案。
(编辑:如果表值参数不可用) 最好的似乎是将大量 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 个查询:
但没有 118 或 18。这样,它将可能的 SQL 语句限制为可能的已知语句,防止太多不同的查询计划,从而防止太多的查询计划填满缓存,并且在很大程度上永远不会被重用。上面的代码做同样的事情,但长度为 1000、500、250、125、63、32、16、10 比 1。包含超过 1000 个元素的参数列表也会被拆分,以防止由于大小限制而导致数据库错误。
无论如何,最好有一个直接发送参数化 SQL 的数据库接口,而不需要单独的 Prepare 语句和句柄来调用。SQL Server 和 Oracle 等数据库通过字符串相等(值更改,SQL 中的绑定参数不!)来记住 SQL,并在可用的情况下重用查询计划。无需单独的准备语句,也无需在代码中对查询句柄进行繁琐的维护!ADO.NET 像这样工作,但似乎 Java 仍然使用按句柄准备/执行(不确定)。
我对这个主题有自己的问题,最初建议用重复项填充 IN 子句,但后来更喜欢 NHibernate 样式语句拆分: 参数化 SQL - in / not in with fixed numbers of parameters,用于查询计划缓存优化?
这个问题仍然很有趣,甚至在被问到 5 年多之后……
编辑:我注意到在 SQL Server 上,在给定的情况下,具有许多值(如 250 或更多)的 IN 查询仍然很慢。虽然我希望数据库在内部创建一种临时表并加入它,但它似乎只重复了单值 SELECT 表达式 n 次。每个查询的时间最多约为 200 毫秒 - 甚至比将原始 ID 检索 SELECT 与其他相关表连接起来还要糟糕。此外,SQL Server Profiler 中有大约 10 到 15 个 CPU 单元,这对于重复执行相同的参数化是不寻常的查询,表明在重复调用时创建了新的查询计划。也许像个人查询这样的临时查询一点也不差。我不得不将这些查询与更改大小的非拆分查询进行比较以获得最终结论,但就目前而言,似乎无论如何都应该避免使用长 IN 子句。
您可以通过执行以下操作以可重用的方式执行此操作 -
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]);
}
}
}
有关更多详细信息,请查看此博客文章 -参数化 SQL WHERE IN 子句 c#
这是 Mark Bracket 出色答案中解决方案的可重复使用变体。
扩展方法:
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);
}
}
用法:
string[] tags = {"ruby", "rails", "scruffy", "rubyonrails"};
var paramTuple = tags.ToParameterTuple();
var cmdText = $"SELECT * FROM Tags WHERE Name IN ({paramTuple.Item1})";
using (var cmd = new SqlCommand(cmdText))
{
cmd.Parameters.AddRange(paramTuple.Item2);
}
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 ,',')])
使用动态查询。前端只是生成需要的格式:
DECLARE @invalue VARCHAR(100)
SELECT @invalue = '''Bishnu'',''Gautam'''
DECLARE @dynamicSQL VARCHAR(MAX)
SELECT @dynamicSQL = 'SELECT * FROM #temp WHERE [name] IN (' + @invalue + ')'
EXEC (@dynamicSQL)
有一种很好、简单且经过测试的方法:
/* 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 列的表可以很好地修复您的执行计划。 这是一篇关于如何将表格传递给 Dapper 的好文章 - 快速且干净。
创建一个存储名称的临时表,然后使用以下查询:
select * from Tags
where Name in (select distinct name from temp)
order by Count desc
在 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 运算符在我的查询中工作的速度更快。
此外,您可以以您喜欢的任何分隔格式放置无限数量的输入。
我喜欢这个 ..
第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+ '|%'