web-dev-qa-db-ja.com

SQL IN句をパラメータ化する

このように、可変数の引数を持つIN句を含むクエリをパラメータ化する方法を教えてください。

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

このクエリでは、引数の数は1から5のいずれかになります。

私はこれ(またはXML)に専用のストアドプロシージャを使用したくないでしょうが、 SQL Server 2008 に特有の洗練された方法があれば、私はそれにオープンです。

993
Jeff Atwood

これは私が使った手っ取り早いテクニックです。

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

2つの注意点

  • パフォーマンスはひどいです。 LIKE "%...%"クエリはインデックス付けされていません。
  • |、空白、nullタグがないことを確認してください。

これを実現するには他の方法もありますが、もっと清潔だと思う人もいるでしょう。

300
Joel Spolsky

each valueをパラメータ化できますので、次のようにします。

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"プレフィックスと配列のインデックスだけに基づいています。インデックスは 常に になり、ユーザーが生成するものではなく、安全です。

ユーザが入力した値はまだパラメータに詰め込まれているので、そこに脆弱性はありません。

編集する

インジェクションの問題はさておき、(上記のように)可変数のパラメータに対応するようにコマンドテキストを作成すると、キャッシュクエリを利用するSQLサーバの機能が妨げられることに注意してください。その結果、(SQL自体に述語文字列を単に挿入するのではなく)最初にパラメータを使用することの価値をほぼ確実に失うことになります。

キャッシュされたクエリプランは価値がないというわけではありませんが、IMOこのクエリはそれから多くの恩恵を受けるほど十分に複雑ではありません。コンパイルコストは実行コストに近づく(あるいは超える)可能性がありますが、それでもミリ秒単位で話しています。

十分なRAMがある場合は、SQL Serverがパラメータの共通数の計画もキャッシュすることを期待しています。私はあなたが常に5つのパラメータを追加し、未指定のタグをNULLにすることができると思います - クエリプランは同じであるべきです、しかしそれは私にとってかなり醜いように思われます。スタックオーバーフローに - それは非常に価値があるかもしれません)。

また、SQL Server 7以降では クエリの自動パラメータ化 なので、パフォーマンスの観点からはパラメータの使用は必ずしも必要ではありません。ただし、セキュリティの観点からは critical です。このようなデータを入力しました。

699
Mark Brackett

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;
    });
}
242
Mark Brackett

あなたは文字列としてパラメータを渡すことができます

だからあなたは文字列を持っている

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
130
David Basarab

Jeff/Joelが本日Podcastで話すのを聞いた(エピソード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テーブルの間の結合をすることによってこれを間違った方法でやろうとしました。どういうわけか翻訳。そうではありませんでしたが、説明的なエラーメッセージが表示され、 Contains を使用するように指示されました。

とにかく、これを強く推奨されている LINQPad で実行してこのクエリを実行すると、SQL LINQプロバイダが生成した実際のSQLを表示できます。それぞれの値がIN節にパラメータ化されて表示されます。

65
Peter Meyer

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

ここで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;
47
Marc Gravell

これはおそらく半分厄介なやり方で、私はかつて使ったことがありますが、かなり効果的でした。

あなたの目的によってはそれが役に立つかもしれません。

  1. 1つの列を持つ 一時テーブル を作成します。
  2. その列への各ルックアップ値のINSERT
  3. INを使う代わりに、標準のJOINルールを使うことができます。 (柔軟性++)

これには少し柔軟性がありますが、大きなテーブルを作成してインデックスを作成し、パラメータ化されたリストを複数回使用する場合に適しています。それを二度実行し、すべての衛生管理を手動で行わなければならないことを省きます。

fast がどのように正確にプロファイリングされているのか私は理解していませんでしたが、私の状況ではそれは必要でした。

27
Kent Fredric

結合できるテーブル変数を作成する関数があります。

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
23
David Robbins

これは総計ですが、少なくとも1つがあることが保証されている場合は、次のようにすることができます。

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

IN( 'tag1'、 'tag2'、 'tag1'、 'tag1'、 'tag1')を持つことは、SQL Serverによって簡単に最適化されます。さらに、あなたは直接インデックスシークを得ます

18
Matt Rogish

私の意見では、この問題を解決するための最良の情報源は、このサイトに投稿されているものです。

システムコメント。ディナカルネティ

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

17
Paulo Henrique

私は( SQL Server 2008 から)テーブル型パラメータを渡し、where exists、または内部結合を行います。 sp_xml_preparedocumentを使用してXMLを使用してから、その一時テーブルにインデックスを付けることもできます。

17
eulerfx

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。値のリストで行を見つける

これは、アプリケーション層やTransact-SQLで動的SQL文字列を作成する、LIKE演算子を使用するなど、一般的なアンチパターンの代わりになります。

SELECT ProductId, Name, Tags
FROM Product
WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%';

元の質問には要件SQL Server 2008があります。この質問は重複していることが多いので、この回答を参考にしました。
15
Lukasz Szozda

正しい方法は、リストを文字列で格納することです(DBMSがサポートするものによって長さが制限されます)。唯一のトリックは、(処理を簡単にするために)文字列の最初と最後に区切り文字(私の例ではカンマ)があることです。これは、リストを値ごとに1行を含む1列のテーブルに変換することによって、「その場で正規化」することを目的としています。これであなたは向きを変えることができます

(ct1、ct2、ct3 ... ctn)に

で(選択...)

リストに重複した値があることによる問題を回避するために "distinct"を追加する場合は、通常の結合を使用します。

残念ながら、文字列をスライスする手法はかなり製品固有のものです。これが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"はリストから見つけることができるアイテムの最大数と同じ数の行を返さなければなりません)

11
SFA

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

ここで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
MindLoggedOut

私はデフォルトでテーブル値関数(文字列からテーブルを返す)を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

あなたがばかげて長い文字列を持っていない限り、これはテーブルインデックスでうまくいくはずです。

必要に応じてそれを一時テーブルに挿入し、インデックスを付けてから結合を実行できます。

9
Eli Ekstein

私はこれが静的なクエリがちょうど良い方法ではない場合に当てはまると思います。 in句のリストを動的に作成し、一重引用符をエスケープして、SQLを動的に作成します。この場合、リストが小さいため、どのメソッドとも大きな違いはないと思われますが、最も効率的な方法は、投稿に記載されているとおりにSQLを送信することです。最も見栄えの良いコードを作成することや、動的にSQLを構築することは悪い習慣ではなく、最も効率的な方法で記述することをお勧めします。

パラメータが大きくなる多くの場合、分割関数はクエリ自体よりも実行に時間がかかります。 SQL 2008のテーブル値パラメータを持つストアドプロシージャは、私が検討する唯一の他のオプションですが、これはおそらくあなたのケースでは遅くなるでしょう。 TVPは、TVPの主キーで検索している場合には、おそらく大きなリストの方が速いでしょう。これは、SQLがリストの一時テーブルを作成するためです(リストが大きい場合)。テストしない限り、確実にわかりません。

デフォルト値がnullの500個のパラメータを持ち、WHERE Column1 IN(@ Param1、@ Param2、@ Param3、...、@ Param500)を持つストアドプロシージャも見ました。これにより、SQLは一時テーブルを作成し、ソート/個別化を実行してから、インデックスシークではなくテーブルスキャンを実行しました。それは本質的にあなたがそのクエリをパラメータ化することによってしていることですが、それが目立った違いを生むことがないように十分に小さい規模であるけれども。それがNOT INに変更されても意図したとおりには機能しないため、INリストにNULLを含めることを強くお勧めします。あなたは動的にパラメータリストを構築することができますが、あなたが得ることができる唯一の明白なことはオブジェクトがあなたのために一重引用符をエスケープするということです。オブジェクトがパラメータを見つけるためにクエリを解析しなければならないので、このアプローチはアプリケーション側でも少し遅くなります。パラメータ化されたクエリは、クエリを実行した回数だけsp_prepare、sp_executeを呼び出し、その後にsp_unprepareを呼び出すため、SQLのほうが速い場合もあれば速い場合もあります。

ストアドプロシージャまたはパラメータ化されたクエリの実行プランを再利用すると、パフォーマンスが向上する可能性がありますが、最初に実行されたクエリによって決まる1つの実行プランにロックされます。多くの場合、それはその後のクエリにとって理想的とは言えないかもしれません。あなたの場合、実行計画の再利用はおそらくプラスになるでしょうが、例は本当に単純なクエリであるため、まったく違いがないかもしれません。

崖メモ:

どんな場合でも、リスト内の固定数の項目を使用したパラメータ化(使用されていない場合はnull)、パラメータを使用するまたは使用しないクエリの動的構築、テーブル値パラメータを含むストアドプロシージャの使用はそれほど大きな違いはありません。 。ただし、私の一般的な推奨事項は次のとおりです。

あなたのcase /少数のパラメータを持つ単純なクエリ:

テストによってパフォーマンスが向上する場合は、動的SQLをパラメータ付きで使用します。

再利用可能な実行プランを持つクエリ。単にパラメータを変更するか、クエリが複雑な場合は複数回呼び出されます。

動的パラメータを持つSQL.

大きなリストを含むクエリ:

テーブル値パラメータを持つストアドプロシージャリストが大きく変わる可能性がある場合は、ストアード・プロシージャーでWITH RECOMPILEを使用するか、またはパラメーターなしで動的SQLを使用して各照会ごとに新しい実行計画を生成してください。

9
Scott

もう1つの解決策は、ストアドプロシージャに可変数の引数を渡す代わりに、後に続く名前を含む単一の文字列を渡すが、それらを '<>'で囲むことによって一意にすることです。次に、PATINDEXを使って名前を見つけます。

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

次のストアドプロシージャを使用してください。これはカスタム分割関数を使用しています。これは ここ にあります。

 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
mangeshkt

これは別の方法です。ストアドプロシージャに文字列パラメータとしてカンマ区切りのリストを渡すだけです。

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
Metaphor

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

このような可変数の引数に対して、私が知っている唯一の方法は、明示的にSQLを生成するか、または必要な項目を一時テーブルに追加して一時テーブルと結合することを含むものを実行することです。

ColdFusion /にするだけです。

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

これは、クエリ文字列で使用されるローカルテーブルを再作成する手法です。この方法でそれをすることはすべての構文解析問題を排除します。

文字列は任意の言語で作成できます。この例では、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)
7
Rockfish

SQL Server 2016以降では、 OPENJSON 関数を使用することもできます。

このアプローチは OPENJSON - IDのリストで行を選択するための最良の方法の1つ - にブログが書かれています

以下の完全な例

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

UDF、XMLを必要としない答えがあります。INはselect文を受け入れるからです。 SELECT * FROMデータがどこにあるかをテストします(SELECT値FROM TABLE)

あなたは本当に文字列をテーブルに変換する方法だけが必要です。

これは、再帰的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);
6
Runonthespot

私はより簡潔なバージョンを使用します 上の投票された答えの

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

タグパラメータを2回ループします。しかし、それはほとんどの場合問題ではありません(それはあなたのボトルネックにはなりません;もしそうなら、ループを広げてください)。

もしあなたが本当にパフォーマンスに興味があり、ループを2回繰り返したくないのであれば、これはあまり美しくないバージョンです。

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

これはこの問題に対するもう一つの答えです。

(6/4/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;
    }

乾杯。

5
Darek

これは同じ問題に対する解決策へのクロスポストです。予約済みの区切り文字よりも堅牢 - エスケープやネストした配列を含み、NULLや空の配列を理解できます。

C#とT-SQL文字列[]パック/アンパックユーティリティ関数

その後、テーブル値関数に参加できます。

4
Jason Kleban

唯一の勝利の動きはプレーすることではありません。

あなたにとって無限の可変性はありません。有限の変動性のみ.

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より大きい場合は、値はリストに含まれている必要がありますが、リストは不可能な値で5にパディングされています(したがって、SQLは意味をなします)。

ラメの解決策が実際に機能する唯一の解決策である場合もあります。

4
Jason Henriksen
    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

あなたは次のようにすることで再利用可能な方法でこれを行うことができます -

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
Bryan

(編集:テーブル値のパラメータが利用できない場合) 固定長の多数のINパラメータを複数のクエリに分割するのが最善のようです。ダミー/重複値、文字列、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();
}

(さらにアイデアがある場合は、ソートを省略し、list/array CopyToの代わりにvaluesDistSort.Skip(processed).Take(size [...])を使用してください)。

パラメータ変数を挿入するときは、次のように作成します。

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 */
}

私はNHibernateオブジェクトリレーショナルマッパーによって生成されたSQLを見てきました(そこからオブジェクトを作成するためにデータをクエリするとき)、そしてそれは複数のクエリで最もよく見えます。 NHibernateでは、バッチサイズを指定できます。多数のオブジェクトデータ行をフェッチする必要がある場合は、バッチサイズに相当する行数を取得しようとします。

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

何百、何千という送信せずに

SELECT * FROM MyTable WHERE Id=@id

残りのIDがバッチサイズより小さいが、それでも1つ以上の場合、小さいIDに分割されますが、それでも一定の長さになります。

バッチサイズが100で、118個のパラメータを持つクエリがある場合、3つのクエリが作成されます。

  • 100個のパラメータを持つもの(バッチサイズ)
  • それから12との1つ
  • そしてもう一つは6

このようにして、可能性のあるSQLステートメントを既知のステートメントに制限し、キャッシュがいっぱいになり、大部分が再利用されることのない、多すぎる、つまり多すぎるクエリプランを防ぎます。上記のコードも同じですが、長さが1000、500、250、125、63、32、16、10対1です。 1000を超える要素を持つパラメータリストも分割され、サイズ制限によるデータベースエラーを防ぎます。

とにかく、別のPrepareステートメントと呼び出し用のハンドルを使用せずに、パラメータ化されたSQLを直接送信するデータベースインタフェースを使用するのが最善です。 SQL ServerやOracleなどのデータベースは、SQLを文字列の等価性(値の変更、SQLのバインドパラメータの変更は不可)で覚え、可能であればクエリプランを再利用します。別々のprepareステートメント、およびコード内のクエリハンドルの面倒なメンテナンスは不要です。 ADO.NETはこのように機能しますが、Javaはまだハンドルによる準備/実行を使用しているようです(確信がありません)。

私はこのトピックについて私自身の質問をしました。もともとIN句を重複で埋めることを提案しましたが、それからNHibernateスタイルのステートメント分割を好みます: パラメータ化されたSQL。

この質問は、まだ5年以上質問されてからも興味深いものです。

編集:私は、多くの値(250以上など)を持つINクエリは、SQL Server上では依然として遅くなる傾向があることに注目しました。私は、DBが一種の一時テーブルを内部的に作成しそれに対して結合することを期待していましたが、単一値のSELECT式をn回繰り返しただけのようでした。クエリあたり最大200ミリ秒の時間 - 他の関連するテーブルに対してSELECT元のID検索を結合するよりもさらに悪い。さらに、SQL Server Profilerには10から15のCPUユニットがあり、同じパラメータ化の繰り返し実行には珍しい新しいクエリプランが繰り返しの呼び出しで作成されたことを示唆しています。個々のクエリのようなアドホックはまったく悪くないかもしれません。最終的な結論として、サイズを変えてこれらのクエリを非分割クエリと比較する必要がありましたが、現時点では、長いIN句はとにかく避けなければならないようです。

3
Erik Hart

動的クエリを使用してください。フロントエンドは必要なフォーマットを生成するだけです:

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

2
brykneval

これは、Mark Bracketの優れた答えの中で、このソリューションの再利用可能なバリエーションです。

延長方法:

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

ステップ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+ '|%'
1
Adel Mourad

それを行うには、シンプルでテスト済みの素晴らしい方法があります。

/* 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にテーブルを渡す方法の良い記事です。

1
Bartosz X

名前が格納されている一時テーブルを作成してから、次のクエリを使用します。

select * from Tags 
where Name in (select distinct name from temp)
order by Count desc
0
guru008

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
Mili