我如何参数化一个包含有可变数量参数的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的优雅方式,我愿意接受。
当前回答
对于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. xml)。NVarChar maxLen);
注意:最大长度为-1。
其他回答
这很恶心,但如果你保证至少有一个,你可以这样做:
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,然后甚至可以索引临时表。
在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;
or:
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操作符: 选择ProductId,名称,标签 从产品 ”,1、2、3”,像“%”+投(ProductId 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。因为这个问题经常被重复使用,所以我添加了这个答案作为参考。
使用动态查询。前端只生成所需的格式:
DECLARE @invalue VARCHAR(100)
SELECT @invalue = '''Bishnu'',''Gautam'''
DECLARE @dynamicSQL VARCHAR(MAX)
SELECT @dynamicSQL = 'SELECT * FROM #temp WHERE [name] IN (' + @invalue + ')'
EXEC (@dynamicSQL)
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个查询:
一个有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.