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

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

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

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

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

ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
1 068
2
367 810
39
Перейти к ответу Данный вопрос помечен как решенный

Ответы 39

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

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

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

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

Michael Haren 04.12.2008 06:06

Я использую аналогичное решение в Oracle. Его не нужно повторно анализировать, как это делают некоторые другие решения.

Leigh Riffel 18.12.2008 21:12

Это чистый подход к базе данных, другой требует работы в коде вне базы данных.

David Basarab 18.12.2008 21:31

Относится ли это к сканированию таблиц или можно использовать индексы и т. д.?

Pure.Krome 31.01.2009 04:36

лучше было бы использовать CROSS APPLY против табличной функции SQL (по крайней мере, в 2005 году), которая по существу объединяется с таблицей, которая возвращается

adolf garlic 01.04.2009 13:55

@adolf чеснок нет необходимости в перекрестном применении, потому что нет внешней ссылки. Просто присоединяйтесь к функции fnsplit. select T.* from Tags T INNER JOIN fnSplit(@tags, '|') X ON T.Name = X.item

ErikE 09.09.2010 02:51

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

adolf garlic 09.09.2010 16:05

@DavidBasarab: просто вопрос, открыто ли это для атак с использованием SQL-инъекций? Я не совсем уверен, когда я могу сказать, что этот код подвержен атакам SQL-инъекций или нет

quinekxi 09.12.2013 16:46

это динамически созданный SQL, но он не открыт для атак SQL-инъекций

juramarin 18.09.2018 22:28

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

В ответе Ph.E есть пример построения временной таблицы (из csv).

crokusek 14.12.2011 03:52

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

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

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

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

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

Erik Hart 11.01.2014 19:33

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

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

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

Редактировать:

Injection concerns aside, take care to note that constructing the command text to accomodate a variable number of parameters (as above) impede's SQL server's ability to take advantage of cached queries. The net result is that you almost certainly lose the value of using parameters in the first place (as opposed to merely inserting the predicate strings into the SQL itself).

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

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

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

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

tvanfosson 03.12.2008 19:53

Вот как это делает LINQ to SQL, BTW

Mark Cidade 18.12.2008 21:55

Нет ли максимального количества параметров? поэтому, если пользователь не знает, сколько тегов, он может превысить max_number (около 200 или 255 параметров?). Во-вторых, почему использование параметров лучше, чем просто динамический sql со значениями, созданными на лету (замените @ Tag1 значением в приведенном выше примере)?

Pure.Krome 02.01.2009 05:15

@Pure: все дело в том, чтобы избежать SQL-инъекции, к которой вы были бы уязвимы, если бы использовали динамический SQL.

Ray 05.02.2009 02:27

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

Mark 19.08.2009 23:01

@God of Data - Да, я полагаю, если вам нужно более 2100 тегов, вам понадобится другое решение. Но Basarb мог достигнуть 2100 только в том случае, если средняя длина тега была <3 символов (поскольку вам также нужен разделитель). msdn.microsoft.com/en-us/library/ms143432.aspx

Mark Brackett 11.02.2010 15:17

@Mark - это правда только наполовину, так как он будет кэшировать план для каждой версии, и даже это может быть хорошо (если нет, почему бы не оптимизировать для специальной рабочей нагрузки?). Например, в сценарии разбиения на страницы самый ваших запросов будет использовать количество параметров размера страницы при заполнении элементов (например, список вопросов SO).

Nick Craver 15.06.2011 19:58

Я прочитал его четыре раза и до сих пор не понимаю, что он делает. QuotedStr() это есть!

Ian Boyd 20.06.2012 01:00

Автоматическая параметризация в SQL Server по умолчанию включена только для запросов с одним параметром. Все более сложное рассматривается как специальный запрос. Можно принудительно настроить параметризацию, что может вызвать проблемы в другом месте. Так что параметризованный запрос по-прежнему лучше.

Erik Hart 11.01.2014 16:54

Это хорошее решение (вставьте заполнитель параметра для каждого значения IN). Однако SQL Server будет повторно использовать планы запросов по равенству строк, что приведет к созданию нового плана для каждого разного количества параметров. Если в предложении IN их всего несколько, это неплохо. Вы можете получить дюжину планов запросов, макс. 12 значений, но для макс. 1000 значений может потребоваться до 1000 планов запросов. Некоторые объектно-реляционные преобразователи используют особые алгоритмы для разделения таких запросов на несколько с повторяющимся количеством параметров в соответствии с существующими планами запросов.

Erik Hart 11.01.2014 17:00

Предположим, что теги динамические. Например. Поле со списком расширенного режима выбора с несколькими значениями. Пользователь может выбрать один или несколько. В этом случае тегов может быть один или несколько. Итак, как можно определить количество тегов, которые нужно передать в строке SQL. cmd.CommandText = "SELECT * FROM Tags WHERE Name IN (@tag0,@tag1,@tag2,@tag3, ....., @tagN)" N является переменным ... в зависимости от выбора пользователя. В чем подвох?

bonCodigo 27.06.2014 17:20

@bonCodigo - выбранные вами значения находятся в массиве; вы просто перебираете массив и добавляете параметр (с суффиксом индекса) для каждого из них.

Mark Brackett 27.06.2014 17:42

SQL-запрос находится в статическом классе как статическая строка, например. ..."WHERE TS.[SESSIONE] IN (@SessionList) AND ..." Итерация массива мне понятна, и я построил набор параметров на основе вашего ответа. Однако подключение их к вышеуказанному запросу является проблемой, поскольку параметр - @SessionList, а параметры, созданные в массиве, - это @Session1, @Session2...etc. Хм ... Я просто пропустил {(0)} вместо @SessionList?

bonCodigo 27.06.2014 17:47

Это также пойдет не так в том случае, когда (по общему признанию необычный) случай, когда клиентская БД имеет параметр DECIMAL = COMMA - вам нужно будет добавить конечный пробел после каждой запятой при генерации строки, чтобы избежать этого .... (" 1,5 "-> означает полторы, а не" один, затем пять "," 1, 5 "(запятая) ->" один, затем пять "

monojohnny 31.10.2014 18:37

@monojohnny - я бы подозревал, что десятичная запятая = не будет проблемой, поскольку разделители запятых находятся между параметром имена. Хотя это тривиально (и, возможно, лучше) для string.Join (","), чтобы сделать его более читабельным ....

Mark Brackett 06.01.2017 02:54

Это решение C#, а не SQL.

Suncat2000 30.11.2018 16:23

@ Suncat2000 В SQL нет таких параметров. Параметры - это то, что выполняется на вашем языке в сочетании с используемой вами библиотекой доступа к базе данных (например, ADO в собственном коде, ADO.net в .NET. Hibernate в Java). Например, в синтаксисе ADO в собственном коде идея состоит в том, чтобы напишите SQL SELECT * FROM Tags WHERE Name IN (?, ?, ?). Я использую ?, потому что ADO / OLEDB (например, ODBC) имеет только параметры позиционный, а не именованные. Подход на чистом SQL предполагает запись строки как есть - и убедитесь, что вы не облажались с инъекцией.

Ian Boyd 08.07.2020 19:25
Ответ принят как подходящий

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

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

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

да, он в 10 раз медленнее, но его очень легко параметризовать, хех. Не уверен, насколько быстрее было бы вызвать fnSplit (), как предлагает ответ Longhorn213

Jeff Atwood 03.12.2008 19:48

Да, это сканирование таблицы. Отлично для 10 рядов, паршиво для 100000.

Will Hartung 03.12.2008 19:48

@ Мэтт, я согласен. Метод Марка Брэкетта, вероятно, будет лучше масштабироваться.

StingyJack 03.12.2008 19:50

Согласен ... это хорошее решение для небольшого стола. Не требует никаких временных таблиц или набора параметров.

Mike Shepard 03.12.2008 19:51

Функция fnSplit в Longhorn213 будет вызываться один раз, что займет немного времени, но тогда она сможет воспользоваться индексом на Tags.Name. Решение Джоэла, вероятно, требует полного сканирования тегов, что может быть медленным для большой таблицы. Сказав это, я сам использую метод Джоэла для небольших таблиц.

Tony Andrews 03.12.2008 19:51

Убедитесь, что вы тестируете теги, в которых есть трубы.

Joel Coehoorn 03.12.2008 20:16

Это даже не отвечает на вопрос. Конечно, легко увидеть, куда добавить параметры, но как вы можете принять это решение, если оно даже не беспокоится о параметризации запроса? Он выглядит проще, чем @Mark Brackett, только потому, что не параметризован.

tvanfosson 03.12.2008 23:14

tvanfosson: Хорошее замечание. Вы не используете параметры, но на самом деле все еще просто строки ...

Matt Rogish 03.12.2008 23:37

«Конечно, легко увидеть, куда добавить параметры», это похоже на np-complete ... мы сократили запрос до типичной формы, которую легко параметризовать. Проблема с IN - это внутренняя изменчивость, сколько IN может быть у нас? 50? 1000? 10000?

Jeff Atwood 04.12.2008 00:10

Судя по всему, в MS-SQL число настолько велико, что не говорят, что это такое. Если вы получаете более 10 КБ, то решение для объединения таблиц, вероятно, лучше. Этот конкретный запрос будет становиться все хуже и хуже по мере увеличения числа. Представьте, что вы каждый раз просматриваете строку размером 50 КБ.

tvanfosson 05.12.2008 20:40

В этом случае мы, очевидно, говорим о тегах, а система SO ограничивает вас всего 5, так что, вероятно, это будет не так уж и плохо.

Joel Coehoorn 11.12.2008 17:26

@Joel - в этом решении есть два недостатка. Разбор строки символов ('|' + @tags + '|') и размер таблицы - поскольку для этого требуется сканирование таблицы. Первое не должно быть проблемой для системы тегов SO, но второе, безусловно, может быть (сейчас около 16500 тегов).

Mark Brackett 18.12.2008 17:05

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

Robert C. Barth 18.12.2008 20:29

@ Джоэл: Умно, и это работает. Так что, если он собирается выполнять сканирование индекса, производительность должна быть только «достаточно хорошей». Не зная ограничений в столбце Имя, я собираюсь рассмотреть крайние случаи (пустая строка, пустая строка, содержит символ вертикальной черты), а также неясный угловой случай, значение имени, содержащее подстановочный знак, например. 'pe% ter' будет соответствовать '| peanut | butter |' но не "| масло | арахис |". (Да, это непонятный случай, который не будет тестироваться в QA, но будет отработан в производственной среде.) Это довольно простой обходной путь (в некоторых СУБД), позволяющий избежать использования подстановочных знаков.

spencer7593 30.05.2009 01:27

Что, если ваш тег - «рубин | рельсы». Это будет совпадать, что будет неправильно. При развертывании таких решений необходимо либо убедиться, что теги не содержат каналов, либо явно отфильтровать их: выберите * из тегов, где '| ruby ​​| rails | scruffy | rubyonrails |' как "% |" + Имя + '|%' И имя не похоже на '%!%'

A-K 20.08.2009 02:21

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

Stephen Holt 14.09.2015 13:19

Работа со строкой в ​​SQL очень медленная. Вам следует избегать этого.

Major 03.01.2018 17:34

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

jim birch 18.09.2020 06:42

Для 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 для максимальной длины.

Вы не можете [легко] использовать TVP с Linq To Sql, поэтому вам нужно вернуться к старому доброму объекту SqlCommand. Мне нужно сделать именно это прямо сейчас, чтобы обойти вшивую привычку Linq-To-Sql к обновлению / вставке туда и обратно.

Mark 19.08.2009 23:03

мы проверили это, и параметры с табличным значением работают медленно. Выполнить 5 запросов буквально быстрее, чем выполнить один TVP.

Jeff Atwood 04.04.2011 09:19

Есть идеи, как подготовить это заявление? Я получаю эту ошибку, когда вызываю cmd.Prepare (). Метод Prepare требует, чтобы все параметры переменной длины имели явно заданную ненулевую длину.

Hash 18.07.2011 04:27

@JeffAtwood - Вы пробовали перетасовать запрос на что-то вроде SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);? Теоретически это действительно должен быть самый быстрый подход. Вы можете использовать соответствующие индексы (например, индекс по имени тега, в котором количество INCLUDEs было бы идеальным), и SQL Server должен сделать несколько попыток, чтобы захватить все теги и их количество. Как выглядит план?

Nick Chammas 13.10.2011 23:47

Я также тестировал это, и он БЫСТРЫЙ, КАК МОЛНИЯ (по сравнению с построением большой строки IN). У меня были некоторые проблемы с настройкой параметра, поскольку я постоянно получал сообщение «Не удалось преобразовать значение параметра из Int32 [] в IEnumerable`1.». В любом случае, решил это, и вот образец, который я сделал pastebin.com/qHP05CXc

Fredrik Johansson 02.05.2013 17:49

@FredrikJohansson - Из 130 положительных отзывов вы, возможно, единственный запуск, который на самом деле пытался запустить это! Я сделал ошибку при чтении документации, и вам действительно нужен IEnumerable <SqlDataRecord>, а не просто IEnumerable. Код обновлен.

Mark Brackett 02.05.2013 22:17

@MarkBrackett Отлично с обновлением! Фактически этот код действительно спас мне день, так как я запрашиваю поисковый индекс Lucene, и он иногда возвращает более 50 000 или около того обращений, которые необходимо дважды проверить на SQL-сервере. Поэтому я создаю массив int [] (document / SQL-ключи), а затем появляется приведенный выше код. Теперь весь OP занимает менее 200 мс :)

Fredrik Johansson 03.05.2013 10:57

@Keith, практических ограничений при использовании TVP нет - это одна из веских причин их использовать.

Lucero 18.10.2013 14:15

Сегодня разговаривал с администратором базы данных, и он предложил разделить строку по TVP. Я попробовал, и раскол действительно прошел быстрее. Конечно, я передаю большие строки, которые разбиваются на более чем 10 000 значений, но меня это все равно удивило. Глядя на сгенерированный необработанный sql, TVP просто генерирует SQL, который вручную вставляет одну строку за раз в параметр таблицы, поэтому в конечном итоге может потребоваться много кода запроса для синтаксического анализа.

jjxtra 27.08.2014 02:49

@JeffAtwood - Что касается производительности, создайте первичный ключ на TVP, чтобы он использовал индекс. Если вероятно, что у вас будет много строк, использование OPTION (RECOMPILE) также может помочь.

Martin Smith 07.11.2015 18:26

Я использовал этот код в течение многих лет, прежде чем столкнулся с этой проблемой: использование первой записи для установки SqlMetaData дает вам четырехсимвольную строку (см. Исходный код SqlMetaData.cs). Таким образом, остальные три тега ничего не соответствуют (или соответствуют только «ruby», «rail» и «scru»). Кто-нибудь еще видел такое поведение?

Doug 28.03.2018 20:00

Я только что столкнулся с ошибкой @Doug, упомянутой в примере кода: метаданные MaxLength ограничивают сравниваемые символы до 4 в случае первого слова «рубин». Минимальное исправление заключалось в использовании самого длинного значения для установки типа данных: var firstRecord = values.OrderByDescending (v => v? .Length ?? 0) .First ();

Rich 17.05.2019 13:47

@Rich - В итоге я использовал metadata = New SqlMetaData(columnName, SqlDbType.NVarChar, -1) для строк, который устанавливает тип nvarchar(max). Не уверен, влияет ли это на производительность или нет.

Doug 17.05.2019 18:37

Спасибо @Doug, я обновил ответ в соответствии с вашим комментарием.

Rikin Patel 14.07.2020 07:05

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

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

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

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

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

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

SQL Police 10.06.2015 09:17

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

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

В Холодный синтез мы просто делаем:

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

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

В любом случае, если вы запустите это в настоятельно рекомендуемом LINQPad и запустите этот запрос, вы можете просмотреть фактический SQL, созданный поставщиком SQL LINQ. Он покажет вам, что каждое из значений параметризуется в предложении IN.

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

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

в

in (select ...)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

struhtanov 26.04.2013 13:55

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

Комментарии. Динакар Нети

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

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

crokusek 14.12.2011 04:37

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

Для вас нет бесконечного разнообразия. Только конечная изменчивость.

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

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

У меня есть ответ, для которого не требуется UDF, XML Поскольку IN принимает оператор выбора например ВЫБРАТЬ * ИЗ Тест, где ДАННЫЕ ВХОДИТ (ВЫБЕРИТЕ значение ИЗ ТАБЛИЦЫ)

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

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

Если вы звоните из .NET, вы можете использовать Сеть Dapper dot:

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;

как раз то, что мы используем на этой странице, в ответ на заданный вопрос (dapper) i.stack.imgur.com/RBAjL.png

Sam Saffron 15.06.2011 15:09

Обратите внимание, что dapper теперь также поддерживает табличные параметры как первоклассные граждане

Marc Gravell 16.07.2014 20:48

Это падает, если имена длинные

cs0815 20.10.2014 18:41

Может быть, здесь можно использовать 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)
CTE и @x можно исключить / встроить в подвыборку, если сделать это очень осторожно, как показано в эта статья.
robert4 20.08.2015 06:12

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

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

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

Строка C# и T-SQL [] Вспомогательные функции Pack / Unpack

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

Если у вас есть 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

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

Обновлено: Создайте таблицу под названием 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

Можете ли вы показать пример такой работы, когда нет жестко запрограммированного списка возможных значений?

John Saunders 01.12.2012 03:34

@JohnSaunders, я редактировал скрипт без использования какого-либо жесткого списка. Пожалуйста, подтвердите.

Gowdhaman008 03.12.2012 17:32

Одно ограничение с этой опцией. CharIndex возвращает 1, если строка найдена. IN возвращает совпадение для точных условий. CharIndex для «Stack» вернет 1 для термина «StackOverflow» IN не вернет. Для этого ответа есть небольшая настройка с использованием PatIndex выше, которая включает имена с помощью '<'% name% '>', что преодолевает это ограничение. Но творческое решение этой проблемы.

Richard Vivian 17.05.2014 11:38

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

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

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

(новая версия размещена 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;
    }

Ваше здоровье.

Используйте динамический запрос. Внешний интерфейс предназначен только для создания необходимого формата:

DECLARE @invalue VARCHAR(100)
SELECT @invalue = '''Bishnu'',''Gautam'''

DECLARE @dynamicSQL VARCHAR(MAX)
SELECT @dynamicSQL = 'SELECT * FROM #temp WHERE [name] IN (' + @invalue + ')'
EXEC (@dynamicSQL)

SQL Fiddle

Это небезопасно от внедрения SQL.

Martin Smith 07.11.2015 18:24

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

Brian Knoblauch 01.07.2016 23:16

(Обновлено: если параметры с табличным значением недоступны) Кажется, лучше всего разделить большое количество параметров 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 (processing) .Take (size [...]) вместо списка / массива 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 */
}

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

SELECT * FROM MyTable WHERE Id IN (@p1, @p2, @p3, ... , @p[batch-size])

вместо того, чтобы отправлять сотни или тысячи

SELECT * FROM MyTable WHERE 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 нет!) И повторно используют планы запросов, если они доступны. Нет необходимости в отдельных операторах подготовки и утомительном обслуживании дескрипторов запросов в коде! ADO.NET работает так, но похоже, что Java все еще использует подготовку / выполнение по дескриптору (не уверен).

У меня был собственный вопрос по этой теме, изначально предлагавший заполнить предложение IN дубликатами, но затем я предпочел разделение оператора в стиле NHibernate: Параметризованный SQL - входящий / не входящий с фиксированным количеством параметров для оптимизации кеша плана запроса?

Этот вопрос все еще интересен, даже спустя более 5 лет после того, как его задали ...

Обновлено: я заметил, что запросы IN со многими значениями (например, 250 или более) по-прежнему имеют тенденцию быть медленными в данном случае на SQL Server. Хотя я ожидал, что БД создаст своего рода временную таблицу внутри и соединится с ней, казалось, что она повторяет только однозначное выражение SELECT n раз. Время составляло примерно 200 мс на запрос - даже хуже, чем объединение исходных идентификаторов SELECT для извлечения из других связанных таблиц. Кроме того, в SQL Server Profiler было от 10 до 15 единиц ЦП, что необычно для повторного выполнения одного и того же параметризованного запросы, предполагающие, что новые планы запросов были созданы при повторных вызовах. Может быть, специальные запросы, вроде индивидуальных, ничуть не хуже. Для окончательного вывода мне пришлось сравнить эти запросы с неразделенными запросами с изменяющимися размерами, но на данный момент кажется, что длинных предложений IN в любом случае следует избегать.

Я использую более сжатую версию из самых популярных ответов:

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

Самая важная часть этого new SqlParameter(paramName, SqlDbType.NVarChar(50) { Value = tags[i]; } - это синтаксическая ошибка. Должна ли вторая открытая скобка быть запятой, то есть new SqlParameter(paramName, SqlDbType.NVarChar, 50) ?

Ben Voigt 02.09.2020 18:56
    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 ,',')])

Некоторое объяснение было бы неплохо.

Peter Mortensen 18.05.2015 22:23

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

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

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

В 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 

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

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#

В 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. Find rows by list of values

This is replacement for common anti-pattern such as creating a dynamic SQL string in application layer or Transact-SQL, or by using LIKE operator:

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 - Живая демонстрация

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


Original question has requirement SQL Server 2008. Because this question is often used as duplicate, I've added this answer as reference.

Я не тестировал это perf, но мне кажется, что это самое чистое решение 2016+. Я все еще хотел бы иметь возможность просто передать массив int, но до тех пор ...

Daniel 28.06.2019 17:12

Это многоразовая вариация решения в отличном ответе Марка Брекета.

Метод расширения:

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

Хм .... Я вижу здесь решение для временной таблицы. Там, где я работаю, когда вы сталкиваетесь с трудностью, которую не понимаете или не знаете, как правильно обойти, мы просто создаем поле таблицы (флаг) или временную таблицу ... Делают ли такие решения вас лучшим инженером-программистом? Это вопрос

Kurt Miller 24.09.2018 13:01

В 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+ '|%'

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