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

1041

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

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

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

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

Джефф Этвуд
источник

Ответы:

315

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

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

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

Джоэл Спольски
источник
119
Это будет очень медленно
Мэтт Рогиш
13
Да, это сканирование таблицы. Отлично подходит для 10 рядов, паршиво для 100 000.
Уилл Хартунг
17
Убедитесь, что вы тестируете на тегах, которые имеют трубы в них.
Джоэл Коухорн
17
Это даже не отвечает на вопрос. Конечно, легко увидеть, где добавить параметры, но как вы можете принять это решение, если оно даже не потрудится параметризировать запрос? Он выглядит проще, чем @Mark Brackett, потому что он не параметризован.
tvanfosson
21
Что делать, если ваш тег 'ruby | rails'. Это будет соответствовать, что будет неправильно. При развертывании таких решений необходимо убедиться, что теги не содержат каналов, или явно отфильтровать их: выберите * из тегов, где '| ruby ​​| rails | scruffy | rubyonrails |' как '% |' + Имя + '|%' И имя не похоже на '%!%'
АК
729

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

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 этот запрос не настолько сложен, чтобы увидеть большую пользу от него. Хотя затраты на компиляцию могут приближаться (или даже превышать) к затратам на выполнение, вы все еще говорите миллисекунды.

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

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

Марк Брэкетт
источник
2
По сути, такой же, как мой ответ на «связанный» вопрос, и, очевидно, лучшее решение, поскольку оно скорее конструктивное и эффективное, чем интерпретирующее (гораздо сложнее).
tvanfosson
49
Вот как это делает LINQ to SQL, кстати
Марк Сидаде
3
@Pure: Суть всего этого состоит в том, чтобы избежать SQL-инъекций, к которым вы были бы уязвимы, если бы вы использовали динамический SQL.
Рэй
4
@ Бог данных - да, я полагаю, если вам нужно более 2100 тегов, вам нужно другое решение. Но Basarb's может достичь 2100, только если средняя длина тега будет <3 символа (так как вам также нужен разделитель). msdn.microsoft.com/en-us/library/ms143432.aspx
Марк Брэкетт
2
@bonCodigo - выбранные вами значения находятся в массиве; Вы просто зацикливаетесь на массиве и добавляете параметр (с индексом) для каждого.
Марк Брэкетт
249

Для 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;
    });
}
Марк Брэкетт
источник
41
мы проверили это, и табличные параметры работают медленно. Выполнить 5 запросов буквально быстрее, чем один TVP.
Джефф Этвуд
4
@JeffAtwood - Вы пытались перетасовать запрос к чему-то вроде SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);? Теоретически, это действительно должен быть самый быстрый подход. Вы можете использовать соответствующие индексы (например, индекс по имени тега, который INCLUDEсчитается идеальным), и SQL Server должен сделать несколько попыток, чтобы получить все теги и их количество. Как выглядит план?
Ник Чаммас
9
Я также проверил это, и это БЫСТРО КАК МОЛНИЯ (по сравнению с созданием большой строки IN). У меня были некоторые проблемы с настройкой параметра, так как я постоянно получал «Не удалось преобразовать значение параметра из Int32 [] в IEnumerable`1». Во всяком случае, решил это, и вот пример, который я сделал pastebin.com/qHP05CXc
Фредрик Йоханссон
6
@FredrikJohansson - Из 130 голосов, вы можете быть единственным, кто действительно пытался запустить это! Я сделал ошибку при чтении документов, и вам действительно нужен IEnumerable <SqlDataRecord>, а не какой-либо IEnumerable. Код был обновлен.
Марк Брэкетт
3
@MarkBrackett Отлично с обновлением! Обычно этот код действительно спас мне день, так как я запрашиваю поисковый индекс Lucene, и он иногда возвращает более 50 000 или около того совпадений, которые необходимо дважды проверить на сервере SQL - поэтому я создаю массив int [] (document / Ключи SQL), а затем приходит приведенный выше код. Весь OP теперь занимает менее 200 мс :)
Фредрик Йоханссон
188

Первоначальный вопрос был «Как я могу параметризовать запрос ...»

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

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

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

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

Здесь я хочу остановиться на подходе, представленном в ответе Джоэла Спольски, ответ «выбран» как правильный ответ.

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

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

Некоторые проблемы с характером%

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

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

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

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

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

Почти само собой разумеется, что мы можем не хотеть pe%ter соответствовать арахисовому маслу, независимо от того, насколько он любит это.

Мрачный угловой чехол

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

Ямочный ремонт

Один из подходов к исправлению этой дыры - избежать %символа подстановки. (Для тех, кто не знаком с оператором escape, здесь есть ссылка на документацию по 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дескриптор подчеркивания. И просто для удовольствия, на этот раз мы будем использовать $ в качестве escape-символа.

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

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

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

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

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

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

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

Функция где?

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

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

Базовые знания

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

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

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


Другие вопросы перепросмотрены

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

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

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

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


Вывод

Мне нравится подход Джоэла Спольски. Это умно. И это работает.

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

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

spencer7593
источник
Можете ли вы сообщить нам, если вы используете или любите параметризованные запросы? в этом конкретном случае правильно ли перепрыгивать через правило «использовать параметризованные запросы» и проводить санитарную обработку с использованием исходного языка?
ОГРОМНОЕ
2
@Luis: да, я предпочитаю использовать переменные связывания в операторах SQL, и буду избегать связывания переменных только тогда, когда их использование приводит к проблемам с производительностью. Моим нормативным шаблоном для исходной проблемы было бы динамическое создание оператора SQL с необходимым количеством заполнителей в списке IN, а затем привязка каждого значения к одному из заполнителей. Смотрите ответ от Марка Брэкетта, который является ответом, который я (и 231 другой) проголосовал.
spencer7593
133

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

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

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
Дэвид Басараб
источник
2
Вы также можете присоединиться к табличной функции с этим подходом.
Майкл Харен
Я использую решение, подобное этому в Oracle. Его не нужно анализировать заново, как это делают некоторые другие решения.
Ли Риффель
9
Это чистый подход к базе данных, другой требует работы в коде вне базы данных.
Дэвид Басараб
Относится ли это к просмотру таблицы или может использовать индексы и т. Д.?
Pure.Krome
Лучше было бы использовать CROSS APPLY против табличной функции SQL (по крайней мере, в 2005 г.), которая по существу объединяется с возвращаемой таблицей
Адольф Чеснок
66

Я слышал, как Джефф / Джоэл говорил об этом на подкасте сегодня ( серия 34 , 2008-12-16 (MP3, 31 МБ), 1 ч 03 мин 38 с - 1 ч 06 мин 45 с), и мне показалось, что я вспомнил переполнение стека использовал 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, сгенерированный поставщиком SQL LINQ. Он покажет вам каждое из значений, параметризованных в INпредложении.

Питер Мейер
источник
50

Если вы звоните из .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});

Здесь Даппер думает, так что тебе не нужно. Нечто подобное возможно с 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;
Марк Гравелл
источник
11
это то, что мы используем на этой странице, для фактического задаваемого вопроса ( dapper
Сэм Саффрон
3
Обратите внимание, что dapper теперь также поддерживает Табличные Значенные Параметры как первоклассные граждане
Марк Гравелл
Это падает, если имена длинные
cs0815 20.10.14
29

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

В зависимости от ваших целей это может быть полезным.

  1. Создайте временную таблицу с одним столбцом.
  2. INSERT каждое значение поиска в этом столбце.
  3. Вместо использования INвы можете просто использовать ваши стандартные JOINправила. (Гибкость ++)

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

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

Кент Фредрик
источник
Это совсем не противно! Более того, это ИМХО очень чистый способ. И если вы посмотрите на план выполнения, вы увидите, что он такой же, как предложение IN. Вместо временной таблицы вы также можете создать фиксированную таблицу с индексами, в которой вы сохраните параметры вместе с SESSIONID.
Полиция SQL
27

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

LiveDemo

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

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

Это замена обычного анти-паттерна, такого как создание динамической строки SQL на уровне приложения или Transact-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;

SEDE - Live Demo

Связанный: Как передать список значений в хранимую процедуру


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

Лукаш Шозда
источник
1
Я не проверял это, но чувствую, что это самое чистое решение 2016+. Я все еще хотел бы иметь возможность просто передать массив int, но до тех пор ...
Даниэль
24

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

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
Дэвид Роббинс
источник
20

Это брутто, но если вам гарантированно будет хотя бы один, вы можете сделать:

SELECT ...
       ...
 WHERE tag IN( @tag1, ISNULL( @tag2, @tag1 ), ISNULL( @tag3, @tag1 ), etc. )

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

Мэтт Рогиш
источник
1
Необязательные параметры с проверками Null портят производительность, поскольку оптимизатору требуется количество параметров, используемых для создания эффективных запросов. Для запроса для 5 параметров может потребоваться другой план запроса, чем для 500 параметров.
Эрик Харт
18

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

Syscomments. Динакар Нети

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', ',')

КРЕДИТЫ ДЛЯ: Динакар Нети

Пауло Энрике
источник
Отличный ответ, чистый и модульный, супер быстрое выполнение, за исключением первоначального разбора CSV в таблицу (один раз, небольшое количество элементов). Хотя могли бы использовать более простой / быстрый charindex () вместо patindex ()? Charindex () также допускает аргумент 'start_location', который может избежать обрезки входной строки каждый раз? Для ответа на исходный вопрос можно просто объединить с функцией result.
Crokusek
18

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

eulerfx
источник
В ответе Ф.Е. приведен пример построения временной таблицы (из csv).
crokusek
12

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

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

в

в (выберите ...)

или (решение, которое я, вероятно, предпочел бы) для обычного объединения, если вы просто добавляете «отличное», чтобы избежать проблем с дублирующимися значениями в списке.

К сожалению, методы нарезки строки довольно специфичны для продукта. Вот версия 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, ',', ''));

(Конечно, «сводка» должна возвращать столько строк, сколько максимальное количество элементов мы можем найти в списке)

Джефф Этвуд
источник
11

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

Если вам не повезло застрять в 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
Jodrell
источник
10

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

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

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

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

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

Для вашего случая все, что вы делаете, будь то параметризация с фиксированным числом элементов в списке (ноль, если не используется), динамическое построение запроса с параметрами или без параметров или использование хранимых процедур с параметрами с табличными параметрами, не будет иметь большого значения , Тем не менее, мои общие рекомендации таковы:

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

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

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

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

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

Хранимая процедура с табличными параметрами. Если список может сильно варьироваться, используйте WITH RECOMPILE для хранимой процедуры или просто используйте динамический SQL без параметров, чтобы сгенерировать новый план выполнения для каждого запроса.

Скотт
источник
Что вы подразумеваете под «хранимой процедурой» здесь? Не могли бы вы опубликовать пример?
струхтанов
9

Может быть, мы можем использовать 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)
MindLoggedOut
источник
1
CTEи @xмогут быть исключены / включены в подвыбор, если это сделано очень тщательно, как показано в этой статье .
robert4
9

Я бы подошел к этому по умолчанию с передачей табличной функции (которая возвращает таблицу из строки) в условие 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

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

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

Эли Экштейн
источник
8

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

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0
ArtOfCoding
источник
8

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

 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
mangeshkt
источник
8

Если в предложении 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.

Fiddler demo

РЕДАКТИРОВАТЬ: Создайте таблицу с именем 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
Gowdhaman008
источник
Можете ли вы показать пример этой работы, где нет жесткого списка возможных значений?
Джон Сондерс
@JohnSaunders, я отредактировал скрипт без использования какого-либо жестко закодированного списка. Пожалуйста, подтвердите.
Gowdhaman008
3
Одно ограничение с этой опцией. CharIndex возвращает 1, если строка найдена. IN возвращает совпадение для точных условий. CharIndex для «Stack» вернет 1 для термина «StackOverflow» IN не будет. Есть небольшой трюк к этому ответу, использующий PatIndex выше, который включает имена с '<'% name% '>', который преодолевает это ограничение. Творческое решение этой проблемы, хотя.
Ричард Вивиан
7

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

ConcernedOfTunbridgeWells
источник
7

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

<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
    <cfquery name="q">
        select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
    </cfquery>
rip747
источник
7

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

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

В SQL Server 2016+ другой возможностью является использование этой OPENJSONфункции.

Этот подход описан в OPENJSON - одном из лучших способов выбора строк по списку идентификаторов .

Полный рабочий пример ниже

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 
Мартин Смит
источник
7

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

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
метафора
источник
6

У меня есть ответ, который не требует UDF, XML, потому что IN принимает оператор выбора, например, SELECT * FROM Test, где 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);
Runonthespot
источник
6

Я использую более краткую версию ответа с наибольшим количеством голосов :

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);
Джордж Стокер
источник
5

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

(новая версия опубликована 04.06.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;
    }

Приветствия.

Дарек
источник
4

Единственный выигрышный ход - не играть.

Нет бесконечной изменчивости для вас. Только конечная изменчивость.

В 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 все еще имеет смысл)

Иногда хромое решение - единственное, которое действительно работает.

Джейсон Хенриксен
источник