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

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

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

7 голосов
/ 06 апреля 2013

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

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
7 голосов
/ 23 ноября 2012

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

EDIT: Создайте таблицу с именем 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
7 голосов
/ 11 декабря 2008

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

<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
    <cfquery name="q">
        select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
    </cfquery>
7 голосов
/ 30 мая 2012

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

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

В 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 
6 голосов
/ 12 марта 2015

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

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);
6 голосов
/ 13 мая 2011

У меня есть ответ, который не требует UDF, XML Потому что IN принимает оператор выбора например SELECT * FROM Проверка, где данные 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);
5 голосов
/ 04 июня 2013

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

(новая версия опубликована 04.06.13).

    private static DataSet GetDataSet(SqlConnectionStringBuilder scsb, string strSql, params object[] pars)
    {
        var ds = new DataSet();
        using (var sqlConn = new SqlConnection(scsb.ConnectionString))
        {
            var sqlParameters = new List<SqlParameter>();
            var replacementStrings = new Dictionary<string, string>();
            if (pars != null)
            {
                for (int i = 0; i < pars.Length; i++)
                {
                    if (pars[i] is IEnumerable<object>)
                    {
                        List<object> enumerable = (pars[i] as IEnumerable<object>).ToList();
                        replacementStrings.Add("@" + i, String.Join(",", enumerable.Select((value, pos) => String.Format("@_{0}_{1}", i, pos))));
                        sqlParameters.AddRange(enumerable.Select((value, pos) => new SqlParameter(String.Format("@_{0}_{1}", i, pos), value ?? DBNull.Value)).ToArray());
                    }
                    else
                    {
                        sqlParameters.Add(new SqlParameter(String.Format("@{0}", i), pars[i] ?? DBNull.Value));
                    }
                }
            }
            strSql = replacementStrings.Aggregate(strSql, (current, replacementString) => current.Replace(replacementString.Key, replacementString.Value));
            using (var sqlCommand = new SqlCommand(strSql, sqlConn))
            {
                if (pars != null)
                {
                    sqlCommand.Parameters.AddRange(sqlParameters.ToArray());
                }
                else
                {
                    //Fail-safe, just in case a user intends to pass a single null parameter
                    sqlCommand.Parameters.Add(new SqlParameter("@0", DBNull.Value));
                }
                using (var sqlDataAdapter = new SqlDataAdapter(sqlCommand))
                {
                    sqlDataAdapter.Fill(ds);
                }
            }
        }
        return ds;
    }

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

4 голосов
/ 29 апреля 2011

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

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

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

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

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