Параметризация предложения SQL IN

как параметризовать запрос, содержащий IN предложение с переменным числом аргументов, например, это?

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

в этом запросе число аргументов может быть от 1 до 5.

Я бы предпочел не использовать специальную хранимую процедуру для этого (или XML), но если есть какой-то элегантный способ, специфичный для SQL Server 2008, Я открыт для этого.

30 ответов


вот быстрый и грязный метод, который я использовал:

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 "%...%" запросы не индексируются.
  • убедитесь, что у вас нет никаких |, пустые или нулевые теги или это не будет работать

есть и другие способы сделать это, что некоторые люди могут считать чище, поэтому, пожалуйста, продолжайте читать.


Вы можете параметризовать каждого значением, что-то вроде:

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" и индексе массива. Индекс всегда быть целым числом, не генерируется пользователем и является безопасным.

введенные пользователем значения по-прежнему набивается в параметры, поэтому уязвимости там нет.

Edit:

Injection касается в сторону, обратите внимание, что построение текста команды для размещения переменного количества параметров (как указано выше) препятствует способности SQL server использовать кэшированные запросы. В результате вы почти наверняка потеряете значение использования параметров в первую очередь (в отличие от простой вставки строк предикатов в SQL сам.)

не то, что кэшированные планы запросов не являются ценными, но IMO этот запрос не настолько сложен, чтобы увидеть большую выгоду от него. Хотя затраты на компиляцию могут приближаться (или даже превышать) затраты на выполнение, вы все еще говорите о миллисекундах.

Если у вас достаточно ОЗУ, я ожидал бы, что SQL Server, вероятно, кэширует план для общего количества параметров. Я полагаю, вы всегда можете добавить пять параметров, и пусть неопределенные теги будут NULL - план запроса должен быть таким же, но он кажется мне довольно уродливым, и я не уверен, что он стоит микро-оптимизации (хотя, при переполнении стека - это может стоить того).

кроме того, SQL Server 7 и более поздние версии будут автоматическая параметризация запросов, поэтому использование параметров не обязательно с точки зрения производительности-это, однако,критическое С точки зрения безопасности-особенно с введенными пользователем данными, как это.


для 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 = SqlMetaData.InferFromValue(firstRecord, columnName);
    return values.Select(v => 
    {
       var r = new SqlDataRecord(metadata);
       r.SetValues(v);
       return r;
    });
}

исходный вопрос был " как параметризовать запрос ..."

позвольте мне заявить здесь, что это не ответ к исходному вопросу. Уже есть некоторые доказательства этого в других хороших ответах.

С учетом сказанного, идите вперед и отметьте этот ответ, опустите его, отметьте его как не ответ... делайте все, что считаете правильным.

см. ответ от Марка Брэкетта для предпочтительного ответа что я (и 231 другие) проголосовали. Подход, приведенный в его ответ позволяет 1) для эффективного использования переменных связывания, и 2) для предикатов, которые sargable.

выбранный ответ

подход Джоэла Спольски умный. И он работает разумно, он будет демонстрировать предсказуемое поведение и предсказуемую производительность, заданные "нормальные" значения и с нормативными граничными случаями, такими как NULL и пустая строка. И этого может быть достаточно для конкретного применения.

но в терминах, обобщающих этот подход, давайте также рассмотрим более неясные угловые случаи, например, когда Name столбец содержит подстановочный знак (как распознано подобным предикатом.) Чаще всего используется подстановочный знак % (знак процента.). Так что давайте разберемся с этим здесь и сейчас, а позже перейдем к другим случаи.

некоторые проблемы с % character

рассмотрим значение имени 'pe%ter'. (Для примеров здесь я использую литеральное строковое значение вместо имени столбца.) Строка со значением имени "pe%ter" будет возвращена запросом формы:

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

но та же строка будет не будет возвращен, если порядок поисковых запросов будет изменен:

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

поведение, которое мы наблюдаем, является странным. Изменение порядка поисковых терминов в списке изменяет результирующий набор.

это почти само собой разумеется, что мы, возможно, не хочу!--9--> чтобы соответствовать арахисовому маслу, независимо от того, насколько он его любит.

темном углу случай

(Да, я согласен, что это загадочное дело. Вероятно, тот, который вряд ли будет проверен. Мы не ожидали бы подстановочный знак в значении столбца. Мы можем предположить, что приложение предотвращает такое значение от будет храниться. Но по моему опыту, я редко видел ограничение базы данных, которое специально запрещало символы или шаблоны, которые считались бы подстановочными знаками в правой части LIKE оператор сравнения.

латание дыры

один подход к латанию этой дыры-избежать % символ. (Для тех, кто не знаком с escape-предложением оператора, вот ссылка на SQL Server документация.

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

теперь мы можем соответствовать литерал %. Конечно, когда у нас есть имя столбца, нам нужно динамически избегать подстановочного знака. Мы можем использовать REPLACE функция для поиска вхождений %символ и вставьте символ обратной косой черты перед каждым из них, например:

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

таким образом, это решает проблему с подстановочным знаком%. Почти.

побег побег

мы признайте, что наше решение породило еще одну проблему. Экранирующий символ. Мы видим, что нам также нужно будет избежать каких-либо событий самого escape-персонажа. На этот раз мы используем ! как Escape-символ:

select ...
 where '|pe%t!r|'
  like '%|' + REPLACE(REPLACE( 'pe%t!r' ,'!','!!'),'%','!%') + '|%' escape '!'

подчеркивание тоже

теперь, когда мы в рулоне, мы можем добавить еще REPLACE ручка подчеркивание символ. И просто для удовольствия, этот раз, мы будем использовать $ в качестве Escape-символа.

select ...
 where '|p_%t!r|'
  like '%|' + REPLACE(REPLACE(REPLACE( 'p_%t!r' ,'$','$$'),'%','$%'),'_','$_') + '|%' escape '$'

I предпочитайте этот подход к экранированию, потому что он работает в Oracle и MySQL, а также SQL Server. (Обычно я использую \ backslash как escape-символ, так как это символ, который мы используем в регулярных выражениях. Но зачем ограничиваться условностями?

эти надоедливые скобки

SQL Server также позволяет рассматривать подстановочные знаки как литералы, заключая их в квадратные скобки []. Таким образом, мы еще не закончили исправление, по крайней мере, для SQL Server. Поскольку пары скобок имеют особое значение, нам также нужно избежать их. Если нам удастся правильно избежать скобок, то, по крайней мере, нам не придется беспокоиться о дефисе - и карат ^ в квадратных скобках. И мы можем оставить любой %и _ символы внутри скобок сбежали, так как мы в основном отключили специальное значение скобок.

найти подходящие пары скобок не должно быть так сложно. Это немного больше сложнее, чем обрабатывать вхождения singleton % и _. (Обратите внимание, что недостаточно просто избежать всех вхождений скобок, потому что одноэлементная скобка считается литералом и не нуждается в экранировании. Логика становится немного расплывчатой, чем я могу справиться без запуска большего количества тестовых случаев.)

встроенное выражение становится грязным

это встроенное выражение в SQL становится длиннее и уродливее. Мы, наверное, можем заставить ее работать, но небеса помогают бедной душе, которая идет позади и должна расшифровать его. Как большой поклонник я для встроенных выражений, я склонен не использовать его здесь, в основном потому, что я не хочу оставлять комментарий, объясняющий причину беспорядка, и извиняясь за это.

функция где ?

хорошо, поэтому, если мы не обрабатываем это как встроенное выражение в SQL, ближайшая альтернатива, которую мы имеем, - это определяемая пользователем функция. И мы знаем, что не ускорить все (если мы не можем определить индекс на нем, как мы могли бы с Oracle.) Если нам нужно создать функцию, мы могли бы лучше сделать это в коде, который вызывает SQL-оператор.

и эта функция может иметь некоторые различия в поведении, в зависимости от СУБД и версии. (Крик всем разработчикам Java, которые так заинтересованы в возможности использовать любой компонент database engine взаимозаменяемо.)

знание предметной области

у нас есть специализированные знания о домене для столбца (то есть набор допустимых значений, применяемых для столбца. Мы можем знать априори значения, хранящиеся в столбце, никогда не будут содержать знак процента, подчеркивание или пары скобок. В этом случае мы просто включаем краткое замечание о том, что эти случаи охвачены.

значения, хранящиеся в столбце, могут содержать символы % или_, но ограничение может потребовать экранирования этих значений, возможно, с помощью определенный символ, такой, что значения похожи на сравнение "безопасный". Опять же, быстрый комментарий о допустимом наборе значений, и в частности, какой символ используется в качестве escape-символа, и идти с подходом Джоэла Спольски.

но, в отсутствие специализированных знаний и гарантии, для нас важно, по крайней мере, рассмотреть возможность обработки этих неясных угловых случаев и рассмотреть, является ли поведение разумным и " за спецификация."


другие вопросы сводились

я считаю, что другие уже достаточно указали на некоторые из других обычно рассматриваемых проблемных областей:

  • SQL-инъекций (принимая то, что кажется предоставленной пользователем информацией, и включая это в текст SQL, а не предоставляя их через переменные привязки. Использование переменных bind не требуется, это всего лишь один удобный подход к срыву с помощью SQL-инъекции. Есть и другие способы справиться с этим:

  • оптимизатор план с использованием сканирования индекса, а не Индекс ищет, возможная потребность в выражении или функции для экранирования подстановочных знаков (возможный индекс на выражение или функцию)

  • использование литеральных значений вместо воздействия переменных привязки масштабируемость


вывод

Да, я ушел далеко от первоначального вопроса. Но где еще оставить эту заметку о том, что я считаю важным вопросом с "выбранным" ответом на вопрос?


вы можете передать параметр в виде строки

Итак, у вас есть строка

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

Я слышал, как Джефф / Джоэл говорил об этом сегодня в подкасте (эпизод 34, 2008-12-16 (MP3, 31 MB), 1 h 03 min 38 secs - 1 h 06 min 45 secs), и я думал, что вспомнил, что переполнение стека использовало 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 будет достаточно умен, чтобы каким-то образом обработать перевод. Это не так, но он предоставил сообщение об ошибке, которое было описательным и указывало мне на использование содержит.

в любом случае, если вы запустите в настоятельно рекомендуется помощью linqpad и запустите этот запрос, вы можете просмотреть фактический SQL, который Создан поставщик SQL LINQ. Он покажет вам каждое из значений, параметризованных в 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;

это, возможно, наполовину неприятный способ сделать это, я использовал его однажды, был довольно эффективным.

в зависимости от ваших целей это может пригодиться.

  1. создать таблица temp С одной колонкой.
  2. INSERT каждое значение поиска в этом столбце.
  3. вместо 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. )

имея в( 'значения tag1', 'tag2', 'вкладка 1', 'значения tag1', 'вкладка 1' ) будет легко оптимизировать SQL-сервером. Кроме того, вы получаете прямой индекс ищет


на мой взгляд, лучшим источником для решения этой проблемы является то, что было опубликовано на этом сайте:

Syscomments. Dinakar Nethi

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


Я бы передал параметр типа таблицы (так как это SQL Server 2008), и where exists, или внутреннее соединение. Вы также можете использовать XML, используя sp_xml_preparedocument, а затем даже индексировать эту временную таблицу.


на SQL Server 2016+ можно использовать SPLIT_STRING функция:

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;

LiveDemo

на принято отвечать будет конечно работать, и это один из способов, чтобы пойти, но это анти-паттерн.

E. найти строки по списку значений

это замена для общего анти-шаблона, такого как создание динамического Строка SQL в слое приложения или Transact-SQL или с помощью оператора LIKE:

SELECT ProductId, Name, Tags
FROM Product
WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%';


оригинальный вопрос имеет требование SQL Server 2008. Поскольку этот вопрос часто используется как дубликат, я добавил этот ответ в качестве ссылки.

правильный способ ИМХО-хранить список в символьной строке (ограниченной по длине тем, что поддерживает СУБД); единственный трюк заключается в том, что (для упрощения обработки) у меня есть разделитель (запятая в моем примере) в начале и в конце строки. Идея состоит в том, чтобы" нормализовать на лету", превратив список в таблицу с одним столбцом, содержащую одну строку на значение. Это позволяет превратить

in (ct1, ct2, ct3 ... ctn)

в Ан

in (select ...)

или (решение, которое я бы предпочел) регулярное соединение, если вы просто добавите "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;

версия Oracle:

 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 или позже я бы использовал Параметр С Табличным Значением.

Если вам не повезло, чтобы застрять на SQL Server 2005 вы можете добавить CLR


Я бы подошел к этому по умолчанию с передачей табличной функции (которая возвращает таблицу из строки) в условие IN.

вот код для UDF (я получил его от переполнения стека где-то, я не могу найти сейчас источник)

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

Если у вас невероятно длинные строки, это должно хорошо работать с индексом таблицы.

при необходимости вы можете вставить его во временную таблицу, индексировать ее, а затем запустить соединение...


Я думаю, что это случай, когда статический запрос-это просто не путь. Динамически создавайте список для предложения in, избегайте одинарных кавычек и динамически создавайте SQL. В этом случае вы, вероятно, не увидите большой разницы с любым методом из-за небольшого списка, но самый эффективный метод-отправить SQL точно так, как он написан в вашем сообщении. Я думаю, что это хорошая привычка писать его наиболее эффективным способом, а не делать то, что делает самый красивый код, или считать его плохая практика для динамического построения SQL.

Я видел, что функции разделения занимают больше времени, чем сам запрос во многих случаях, когда параметры становятся большими. Хранимая процедура с табличными параметрами в SQL 2008-единственный вариант, который я бы рассмотрел, хотя в вашем случае это, вероятно, будет медленнее. TVP, вероятно, будет быстрее только для больших списков, если вы ищете первичный ключ TVP, потому что SQL построит временную таблицу для списка в любом случае (если список большой). Ты не узнаешь наверняка, пока не проверишь.

Я также видел хранимые процедуры, которые имели 500 параметров со значениями по умолчанию null, и имеющие, где Column1 в (@Param1, @Param2, @Param3,..., @Param500). Это заставило SQL создать временную таблицу, выполнить сортировку / различение, а затем выполнить сканирование таблицы вместо поиска индекса. Это по существу то, что вы будете делать, параметризуя этот запрос, хотя и в достаточно небольшом масштабе, что он не сделает заметная разница. Я настоятельно рекомендую не иметь NULL в ваших списках, как будто это будет изменено на не в нем, не будет действовать по назначению. Вы можете динамически создавать список параметров, но единственное, что вы получите, - это то, что объекты будут избегать одинарных кавычек для вас. Этот подход также немного медленнее в конце приложения, так как объекты должны анализировать запрос для поиска параметров. Это может быть или не быть быстрее на SQL, так как параметризованные запросы вызывают sp_prepare, sp_execute столько раз, сколько вы выполняете запрос, а затем sp_unprepare.

повторное использование планов выполнения для хранимых процедур или параметризованных запросов может дать вам прирост производительности, но он заблокирует вас в один план выполнения, определенный первым выполняемым запросом. Это может быть менее идеальным для последующих запросов во многих случаях. В вашем случае повторное использование планов выполнения, вероятно, будет плюсом, но это может не иметь никакого значения, поскольку пример очень простой запрос.

скалы Примечания:

для вашего случая все, что вы делаете, будь то параметризация с фиксированным количеством элементов в списке (null, если не используется), динамическое построение запроса с параметрами или без них или использование хранимых процедур с параметрами, имеющими табличное значение, не будет иметь большого значения. Однако мои общие рекомендации таковы:--1-->

ваш случай/простые запросы с нескольких параметры:

динамический SQL, возможно, с параметрами, если тестирование показывает лучшую производительность.

запросы с многоразовыми планами выполнения, вызываемые несколько раз, просто изменяя параметры или если запрос сложен:

SQL с динамическими параметрами.

запросы с большими списками:

хранимая процедура с табличными значениями параметров. Если список может отличаться большим количеством, используйте с 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)

использовать следующую хранимую процедуру. Он использует пользовательскую функцию разделения, которую можно найти здесь.

 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

другое возможное решение-вместо передачи переменного числа аргументов хранимой процедуре передать одну строку, содержащую имена, которые вам нужны, но сделать их уникальными, окружив их ''. Затем используйте PATINDEX, чтобы найти имена:

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0

вот метод, который воссоздает локальную таблицу для использования в строке запроса. Это устраняет все проблемы парсинга.

строка может быть построена на любом языке. В этом примере я использовал 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)

вот еще одна альтернатива. Просто передайте список с разделителями-запятыми в качестве строкового параметра хранимой процедуре и:

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

если у нас есть строки, хранящиеся внутри предложения 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.

скрипач демо

EDIT: Создайте таблицу 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

для переменного числа аргументов, подобных этому, единственный способ, о котором я знаю, - это либо явно создать SQL, либо сделать что-то, что включает заполнение временной таблицы элементами, которые вы хотите, и присоединение к временной таблице.


на ColdFusion мы просто делаем:

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

Я использую более краткую версию топ-проголосовали ответ:

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

в SQL Server 2016 + Еще одна возможность-использовать OPENJSON


У меня есть ответ, который не требует UDF, XML Потому что IN принимает оператор select например, выберите * из теста, где данные в (выберите значение из таблицы)

вам действительно нужен только способ преобразования строки в таблицу.

Это можно сделать с помощью рекурсивного 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);

вот еще один ответ на эту проблему.

(новая версия размещена на 6/4/13).

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

Ура.


вот кросс-пост к решению той же проблемы. Более надежный, чем зарезервированные разделители-включает экранирующие и вложенные массивы и понимает нули и пустые массивы.

C# & T-SQL string [] Pack / Unpack функции утилиты

затем вы можете присоединиться к функции, возвращающей табличное значение.