Параметризация предложения 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 ]

17 голосов
/ 22 июля 2010

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

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

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

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

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

15 голосов
/ 02 мая 2016

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


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

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

дюйм (ct1, ct2, ct3 ... ctn)

в

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

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

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

10 голосов
/ 15 августа 2012

Если у вас 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
9 голосов
/ 10 июня 2010

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

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

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

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

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

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

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

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

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

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

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

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

9 голосов
/ 24 октября 2011

Может быть, мы можем использовать 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)
9 голосов
/ 11 июня 2015

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

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

 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
8 голосов
/ 12 февраля 2010

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

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...