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

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

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

C # и строка T-SQL [] Утилита упаковать / распаковать функции

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

3 голосов
/ 18 марта 2015
    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 ,',')])
3 голосов
/ 28 января 2016

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

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 #

3 голосов
/ 12 января 2014

(Изменить: если параметры с табличными значениями недоступны) Лучше всего разделить большое количество параметров 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 (обработано). Взять (размер [...]) вместо списка / массива 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.

2 голосов
/ 30 декабря 2016

Это многоразовый вариант решения в отличном ответе Марка Брэкет

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

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);
        }
2 голосов
/ 01 января 2014

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

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

1 голос
/ 12 апреля 2017

Существует хороший, простой и проверенный способ сделать это:

/* 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 (такой же, как у стека). Также указание списка строк в виде таблицы с колонкой PK исправит ваши планы выполнения. Здесь - хорошая статья о том, как передать таблицу в Dapper - все быстро и ЧИСТО.

0 голосов
/ 25 февраля 2019

В 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 Operator работает в моих запросах.
Кроме того, вы можете поместить неограниченное количество входов в любом отдельном формате, который вам нравится.
Мне нравится это ..

0 голосов
/ 30 июля 2017

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

select * from Tags 
where Name in (select distinct name from temp)
order by Count desc
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...