Параметризация предложения SQL IN - PullRequest
998 голосов
/ 03 декабря 2008

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

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

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

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

Ответы [ 39 ]

701 голосов
/ 03 декабря 2008

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

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
string cmdText = "SELECT * FROM Tags WHERE Name IN ({0})";

string[] paramNames = tags.Select(
    (s, i) => "@tag" + i.ToString()
).ToArray();

string inClause = string.Join(", ", paramNames);
using (SqlCommand cmd = new SqlCommand(string.Format(cmdText, inClause))) {
    for(int i = 0; i < paramNames.Length; i++) {
       cmd.Parameters.AddWithValue(paramNames[i], tags[i]);
    }
}

Что даст вам:

cmd.CommandText = "SELECT * FROM Tags WHERE Name IN (@tag0, @tag1, @tag2, @tag3)"
cmd.Parameters["@tag0"] = "ruby"
cmd.Parameters["@tag1"] = "rails"
cmd.Parameters["@tag2"] = "scruffy"
cmd.Parameters["@tag3"] = "rubyonrails"

Нет, это не открыто для SQL-инъекции . Единственный введенный текст в CommandText не основан на вводе пользователем. Он основан исключительно на жестко заданном префиксе "@tag" и индексе массива. Индекс будет всегда целым числом, не генерируется пользователем и является безопасным.

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

Edit:

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

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

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

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

301 голосов
/ 03 декабря 2008

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

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

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

242 голосов
/ 03 декабря 2008

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

Сначала вы должны создать тип

CREATE TYPE dbo.TagNamesTableType AS TABLE ( Name nvarchar(50) )

Тогда ваш код ADO.NET выглядит следующим образом:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
cmd.CommandText = "SELECT Tags.* FROM Tags JOIN @tagNames as P ON Tags.Name = P.Name";

// value must be IEnumerable<SqlDataRecord>
cmd.Parameters.AddWithValue("@tagNames", tags.AsSqlDataRecord("Name")).SqlDbType = SqlDbType.Structured;
cmd.Parameters["@tagNames"].TypeName = "dbo.TagNamesTableType";

// Extension method for converting IEnumerable<string> to IEnumerable<SqlDataRecord>
public static IEnumerable<SqlDataRecord> AsSqlDataRecord(this IEnumerable<string> values, string columnName) {
    if (values == null || !values.Any()) return null; // Annoying, but SqlClient wants null instead of 0 rows
    var firstRecord = values.First();
    var metadata = SqlMetaData.InferFromValue(firstRecord, columnName);
    return values.Select(v => 
    {
       var r = new SqlDataRecord(metadata);
       r.SetValues(v);
       return r;
    });
}
182 голосов
/ 30 мая 2009

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

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

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

См. Ответ Марка Брэкетта для предпочтительного ответа, который я (и 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 '\'

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

Побег побег

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

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

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

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

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


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

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

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

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

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


Заключение

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

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

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

130 голосов
/ 03 декабря 2008

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

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

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
65 голосов
/ 19 декабря 2008

Я слышал, как Джефф / Джоэл говорил об этом сегодня на подкасте ( эпизод 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.

47 голосов
/ 15 июня 2011

Если вы звоните из .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;
27 голосов
/ 03 декабря 2008

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

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

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

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

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

23 голосов
/ 03 декабря 2008

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

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
18 голосов
/ 03 декабря 2008

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...