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


1041

INこのように、引数の数が可変の句を含むクエリをパラメーター化するにはどうすればよいですか?

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

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

これ(またはXML)に専用のストアドプロシージャを使用したくないのですが、SQL Server 2008に固有のエレガントな方法がある場合は、それを受け入れます



回答:


315

これが私が使った素早いテクニックです:

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タグがないことを確認してください。そうしないと機能しません。

これを実現する方法は他にもありますが、一部の人々はよりクリーンであると考えるかもしれません。


119
それは
非常に

13
はい、これはテーブルスキャンです。10行に最適、100,000にはお粗末。
Will Hartung、

17
パイプが含まれているタグでテストしてください。
Joel Coehoorn、2008

17
これでも質問の答えにはなりません。確かに、パラメーターを追加する場所は簡単にわかりますが、クエリをパラメーター化する必要さえなければ、これを解決策として受け入れるにはどうすればよいでしょうか。パラメータ化されていないため、@ Mark Brackettよりも単純に見えます。
tvanfosson 2008

21
タグが「ruby | rails」の場合はどうなりますか。一致しますが、これは誤りです。このようなソリューションを展開するときは、タグにパイプが含まれていないことを確認するか、明示的に除外する必要があります。select* from Tags where '| ruby​​ | rails | scruffy | ruby​​onrails |' 「%|」のように +名前+ |ない'!%%' LIKE '%' AND名前
AK

729

値をパラメーター化できるので、次のようになります。

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以降ではクエリ自動パラメーター化されるため、パフォーマンスの観点からはパラメーターを使用する必要はありません。ただし、特にユーザーが入力したこのようなデータでは、セキュリティの観点からは重要です。


2
基本的には「関連する」質問に対する私の回答と同じですが、解釈よりもはるかに難しいので、建設的で効率的であるため、明らかに最良のソリューションです。
tvanfosson 2008

49
これは、SQLにLINQは、ところで、それをしない方法です
マーク・シダイデ

3
@Pure:これの要点は、SQLインジェクションを回避することです。これは、動的SQLを使用した場合に脆弱になります。
レイ

4
@God of Data-はい、2100を超えるタグが必要な場合は、別のソリューションが必要になると思います。しかし、タグの平均の長さが3文字未満の場合、Basarbは2100にしか到達できませんでした(区切り文字も必要なため)。msdn.microsoft.com/en-us/library/ms143432.aspx
Mark Brackett

2
@bonCodigo-選択した値は配列にあります。配列をループして、それぞれにパラメーター(インデックスが付いたもの)を追加するだけです。
Mark Brackett 2014年

249

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

41
私たちはこれをテストし、テーブル値のパラメーターはDOGが遅いです。1つのTVPを実行するよりも、5つのクエリを実行する方が文字通り高速です。
Jeff Atwood、2011

4
@JeffAtwood-クエリを次のようなものに変更してみましたSELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);か?理論的には、これが本当に最速のアプローチです。関連するインデックス(たとえば、INCLUDEsカウントが理想的なタグ名のインデックス)を使用できます。SQLServerは、すべてのタグとそのカウントを取得するために数回シークする必要があります。計画はどのように見えますか?
Nick Chammas、2011年

9
私はこれもテストしましたが、それは高速で軽量です(大きなIN文字列を作成する場合と比較して)。ただし、「パラメーター値をInt32 []からIEnumerable`1に変換できませんでした」というメッセージが常に表示されるため、パラメーターの設定に問題がありました。とにかく、それを解決し、ここに私が作ったpastebin.com/qHP05CXcの
Fredrik Johansson

6
@FredrikJohansson-130回の賛成投票のうち、実際にこれを実行しようとしたのはあなただけです。ドキュメントを読んで間違えました。実際には、IEnumerableだけでなく、IEnumerable <SqlDataRecord>が必要です。コードを更新しました。
Mark Brackett 2013年

3
@MarkBrackett更新して素晴らしい!私はLucene検索インデックスをクエリしているため、このコードは実際に1日を本当に節約してくれました。SQLサーバーに対してダブルチェックする必要がある50.000を超えるヒットを返すことがあるので、int [](document / SQLキー)を入力すると、上記のコードが挿入されます。OP全体でかかる時間は200ミリ秒未満です:)
Fredrik Johansson

188

元の質問は「クエリをパラメータ化するにはどうすればよいですか...」でした

ここで、これは元の質問に対する回答でないことを述べておきます。他の良い答えには、すでにいくつかの実証があります。

そうは言っても、先に進んでこの回答にフラグを付け、反対票を投じ、回答ではないことをマークしてください。正しいと思われることは何でもしてください。

私(および他の231人)が賛成した好ましい回答については、Mark Brackettの回答を参照してください。彼の回答で与えられたアプローチは、1)バインド変数の効果的な使用、および2)検索可能な述語を可能にします。

選択した回答

ここで取り上げたいのは、ジョエル・スポルスキーの答えで与えられたアプローチです。正しい答えとして「選択された」答えです。

Joel Spolskyのアプローチは賢いです。そして、それは合理的に機能し、「通常の」値が与えられ、NULLや空の文字列などの規範的なエッジケースで、予測可能な動作と予測可能なパフォーマンスを発揮します。そして、それは特定のアプリケーションには十分かもしれません。

ただし、このアプローチを一般化する観点から、Name列にワイルドカード文字(LIKE述語で認識される)が含まれている場合など、よりあいまいなコーナーケースについても考えてみましょう%。それでは、ここでそれを扱い、後で他のケースに進みましょう。

%文字に関するいくつかの問題

Nameの値を考えます'pe%ter'。(ここの例では、列名の代わりにリテラル文字列値を使用しています。)Name値が「 'pe%ter'」の行は、次の形式のクエリによって返されます。

select ...
 where '|peanut|butter|' like '%|' + 'pe%ter' + '|%'

ただし、検索語の順序が逆の場合、同じ行は返されません

select ...
 where '|butter|peanut|' like '%|' + 'pe%ter' + '|%'

私たちが観察する行動は、ちょっと変わっています。リスト内の検索語の順序を変更すると、結果セットが変更されます。

pe%terピーナッツバターがどれほど好きでも、ピーナッツバターとは合いたくないかもしれません。

あいまいなコーナーケース

(はい、これはあいまいなケースであることに同意します。おそらくテストされない可能性があります。列の値にワイルドカードが含まれることは想定されていません。アプリケーションがそのような値を格納できないと想定している可能性があります。しかし、私の経験では、LIKE比較演算子の右側でワイルドカードと見なされる文字またはパターンを特に許可しないデータベース制約を見たことはほとんどありません。

穴をあける

このホールにパッチを適用する1つの方法は、%ワイルドカード文字をエスケープすることです。(演算子のエスケープ句に慣れていない人のために、ここにSQL Serverのドキュメントへのリンクがあります

select ...
 where '|peanut|butter|'
  like '%|' + 'pe\%ter' + '|%' escape '\'

これで、リテラル%を照合できます。もちろん、列名がある場合、ワイルドカードを動的にエスケープする必要があります。次のように、REPLACE関数を使用して%文字の出現箇所を検索し、それぞれの前にバックスラッシュ文字を挿入できます。

select ...
 where '|pe%ter|'
  like '%|' + REPLACE( 'pe%ter' ,'%','\%') + '|%' escape '\'

したがって、%ワイルドカードを使用して問題を解決します。ほとんど。

エスケープエスケープ

私たちのソリューションが別の問題を引き起こしたことを認識しています。エスケープ文字。また、エスケープ文字自体の出現をエスケープする必要があることもわかります。今回は!エスケープ文字として:

select ...
 where '|pe%t!r|'
  like '%|' + REPLACE(REPLACE( 'pe%t!r' ,'!','!!'),'%','!%') + '|%' escape '!'

アンダースコアも

さあ、これでREPLACE、アンダースコアワイルドカードを処理する別のハンドルを追加できます。そして、面白さのために、今回はエスケープ文字として$を使用します。

select ...
 where '|p_%t!r|'
  like '%|' + REPLACE(REPLACE(REPLACE( 'p_%t!r' ,'$','$$'),'%','$%'),'_','$_') + '|%' escape '$'

OracleとMySQLだけでなくSQL Serverでも機能するため、このアプローチはエスケープよりも優先します。(通常、エスケープ文字として\バックスラッシュを使用します。これは、エスケープ文字が正規表現で使用される文字だからです。しかし、なぜ慣例によって制約されているのですか!

それらの厄介な括弧

SQL Serverでは、ワイルドカード文字を角かっこで囲むことにより、それらをリテラルとして扱うこともできます[]。したがって、少なくともSQL Serverでは、まだ修正は完了していません。ブラケットのペアには特別な意味があるため、それらもエスケープする必要があります。かっこを適切にエスケープできれば、少なくともかっこ内のハイフン-とカラットを気にする必要はありません^。また、基本的に角括弧の特別な意味を無効にしているため、角括弧内の任意の%and _文字をエスケープしたままにすることができます。

大括弧の一致するペアを見つけることはそれほど難しくありません。シングルトン%および_の発生を処理するよりも少し難しいです。(シングルトンブラケットはリテラルと見なされ、エスケープする必要がないため、ブラケットのすべての出現をエスケープするだけでは不十分であることに注意してください。ロジックは、テストケースを実行せずに処理できるよりも少しあいまいになっています。 。)

インライン式が乱れる

SQLのそのインライン式は、ますます長く醜くなってきています。私たちはおそらくそれを機能させることができますが、天国は後ろに来てそれを解読しなければならない貧しい魂を助けます。私はインライン表現が好きなので、ここでは使用しないことにしました。主に、混乱の理由を説明するコメントを残したくないためです。

どこで関数?

さて、SQLでインライン式として処理しない場合、最も近い代替手段はユーザー定義関数です。(Oracleのようにインデックスを定義できる場合を除いて)高速化しないことはわかっています。関数を作成する必要がある場合は、SQLを呼び出すコードでそれを行う方がよいでしょう。ステートメント。

また、DBMSとバージョンによって、その機能の動作にいくつかの違いがある場合があります。(Java開発者の皆さんに、データベースエンジンを交換可能に使用できるように熱心に叫んでください。)

領域知識

列のドメイン(つまり、列に適用される許容値のセット)の専門知識がある場合があります。列に格納された値にパーセント記号、下線、または角括弧が含まれないことを先験的に知っている場合があります。その場合、それらのケースがカバーされているという簡単なコメントを含めるだけです。

列に格納された値は%または_文字を許可しますが、制約は、値がLIKE比較 "安全"になるように、おそらく定義された文字を使用して、それらの値をエスケープする必要がある場合があります。繰り返しになりますが、許可された値のセット、特にエスケープ文字として使用されている文字について簡単にコメントし、Joel Spolskyのアプローチを使用してください。

ただし、専門知識と保証がない場合は、少なくともこれらのあいまいなコーナーケースの処理を検討し、動作が合理的で「仕様どおり」であるかどうかを検討することが重要です。


その他の問題の要約

私は他の人がすでに一般的に考慮されている他の懸念事項のいくつかを十分に指摘していると思います:

  • SQLインジェクション(ユーザーが入力した情報のように見えるものを取得し、バインド変数を介して提供するのではなく、SQLテキストにその情報を含めます。バインド変数を使用する必要はなく、SQLインジェクションを阻止する便利な方法の1つにすぎません。それを処理する方法:

  • インデックスシークではなくインデックススキャンを使用するオプティマイザ計画、ワイルドカードをエスケープするための式または関数の必要性(式または関数の可能なインデックス)

  • バインド変数の代わりにリテラル値を使用すると、スケーラビリティに影響します


結論

ジョエル・スポルスキーのアプローチが好きです。それは賢いです。そしてそれは機能します。

しかし、それを見るとすぐに、潜在的な問題がすぐにわかりました。スライドさせるのは私の性質ではありません。他人の努力を批判するつもりはありません。多くの開発者が自分の仕事を非常に個人的に取っていることを知っています。したがって、これは個人的な攻撃ではありません。ここで私が識別しているのは、テストではなく生産で発生する問題のタイプです。

はい、元の質問から遠く離れています。しかし、質問の「選択された」回答で私が重要な問題であると考えるものに関して、このノートをどこに残すべきですか?


パラメータ化されたクエリを使用しているか、または気に入っているかどうかをお知らせください。この特定のケースでは、「パラメーター化されたクエリを使用する」というルールを飛び越えて、元の言語でサニタイズすることが正しいのでしょうか。たくさん
ありがとう

2
@Luis:はい、私はSQLステートメントでバインド変数を使用することを好み、バインド変数を使用するとパフォーマンスの問題が発生する場合にのみバインド変数を回避します。元の問題の標準的なパターンは、INリストに必要な数のプレースホルダーを含むSQLステートメントを動的に作成し、各値をプレースホルダーの1つにバインドすることです。Mark Brackettからの回答を参照してください。これは、私(および他の231人)が賛成した回答です。
spencer7593

133

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

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

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

2
この方法でテーブル関数に結合することもできます。
Michael Haren

私はOracleでこれに似たソリューションを使用しています。他の一部のソリューションのように、再解析する必要はありません。
リーリフェル

9
これは純粋なデータベースアプローチであり、他の方法ではデータベース外のコードで作業する必要があります。
デビッドバサラブ

これはテーブルスキャンに対してですか、それともインデックスなどを利用できますか?
Pure.Krome 2009年

SQLテーブル関数に対してCROSS APPLYを使用することをお勧めします(少なくとも2005年以降)。これは、返されるテーブルに対して基本的に結合します
adolf garlic

66

今日、ポッドキャストでジェフ/ジョエルがこれについて話しているのを聞いた(エピソード 34、2008-12-16(MP3、31 MB)、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テーブルの間の結合を行うことにより、これを間違った方法で自然に実行しようとしました。LINQto SQLトランスレーターは、なんとか翻訳。そうではありませんでしたが、説明的で、Containsを使用するよう指示するエラーメッセージを提供しました。

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


50

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

11
これはたまたまこのページで使用しているものです。実際に尋ねられた質問(dapper)についてはi.stack.imgur.com/RBAjL.png
Sam Saffron


名前が長い場合、これはフォールオーバーします
cs0815 2014年

29

これはおそらくそれを行うための半分厄介な方法です、私は一度使用しましたが、かなり効果的でした。

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

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

これにより、実行できる操作に多少の柔軟性が追加されますが、クエリを実行する大きなテーブルがあり、適切なインデックス付けがあり、パラメーター化されたリストを複数回使用する場合に適しています。それを2回実行する必要がなくなり、すべての衛生管理を手動で行う必要がなくなります。

それがどれほど速いかを正確にプロファイリングすることはできませんでしたが、私の状況ではそれが必要でした。


これは全然厄介ではありません!さらに、それは私見非常にきれいな方法です。実行プランを見ると、IN句と同じであることがわかります。一時テーブルの代わりに、SESSIONIDと一緒にパラメーターを格納する、インデックス付きの固定テーブルを作成することもできます。
SQLポリス

27

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;

ライブデモ

受け入れ答えのコースワークの意志とそれが進むべき道の一つであるが、それはアンチパターンです。

E.値のリストによって行を検索する

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

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

補遺

STRING_SPLITテーブル関数の行推定を改善するには、分割された値を一時テーブル/テーブル変数として具体化することをお勧めします。

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails,sql';

CREATE TABLE #t(val NVARCHAR(120));
INSERT INTO #t(val) SELECT s.[value] FROM STRING_SPLIT(@names, ',') s;

SELECT *
FROM Tags tg
JOIN #t t
  ON t.val = tg.TagName
ORDER BY [Count] DESC;

SEDE-ライブデモ

関連:値の一覧をストアドプロシージャに渡す方法


元の質問には要件がありますSQL Server 2008。この質問は重複して使用されることが多いため、参考としてこの回答を追加しました。


1
私はこれをテストしていませんが、これは最もクリーンな2016+ソリューションだと思います。私はまだintの配列を渡すことができるようにしたいのですが、それまでは...
Daniel

24

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

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

20

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

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

IN( 'tag1'、 'tag2'、 'tag1'、 'tag1'、 'tag1')があると、SQL Serverによって簡単に最適化されます。さらに、直接インデックスシークを取得します


1
オプティマイザは効率的なクエリを作成するために使用されるパラメータの数を必要とするため、Nullチェックのオプションパラメータはパフォーマンスを台無しにします。5つのパラメータのクエリには、500のパラメータのクエリプランとは異なるクエリプランが必要になる場合があります。
エリックハート

18

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

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

クレジット:Dinakar Nethi


優れた回答、クリーンでモジュール化された、テーブルへの最初のCSV解析(1回、少数の要素)を除いて、超高速実行。patindex()の代わりに、より単純/高速なcharindex()を使用できますか?Charindex()は、引数 'start_location'も許可します。これにより、各反復で入力文字列が切り取られないようにすることができますか?元の質問に答えるには、関数の結果と結合するだけです。
crokusek 2011

18

SQL Server 2008であるため)テーブルタイプのパラメーターを渡し、where exists内部結合を行います。を使用してXMLを使用しsp_xml_preparedocument、その一時テーブルにインデックスを作成することもできます。


Ph.Eの回答には、(csvからの)一時テーブルの作成例があります。
crokusek 2011

12

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

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

で(選択...)

または、(私がおそらく好む解決策)通常の結合(リスト内の値の重複に関する問題を回避するために「明確」を追加するだけの場合)。

残念ながら、文字列をスライスする手法はかなり製品固有です。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

あなたが持っている場合は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

これは、静的クエリだけではうまくいかない場合だと思います。in句のリストを動的に作成し、一重引用符をエスケープして、SQLを動的に作成します。この場合、リストが小さいため、どのメソッドでもほとんど違いはありませんが、最も効率的な方法は、投稿に書かれているとおりにSQLを送信することです。かわいらしいコードを作成するのではなく、最も効率的な方法で記述したり、SQLを動的に構築することは悪い習慣だと考えたりするのはよい習慣だと思います。

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

また、デフォルト値がnullの500個のパラメーターがあり、WHERE Column1 IN(@ Param1、@ Param2、@ Param3、...、@ Param500)を持つストアード・プロシージャーも見ました。これにより、SQLは一時テーブルを作成し、並べ替え/区別を行ってから、インデックスシークではなくテーブルスキャンを実行しました。これは基本的に、そのクエリをパラメーター化することで行うことですが、目立った違いはないほど十分に小さいスケールです。INリストにNULLを含めないでください。NULLをNOT INに変更すると、意図したとおりに機能しなくなります。パラメーターリストを動的に作成することもできますが、オブジェクトが単一引用符をエスケープするということだけがわかります。オブジェクトがパラメータを見つけるためにクエリを解析する必要があるため、このアプローチはアプリケーション側でも少し遅くなります。

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

崖のメモ:

あなたの場合、あなたがすることは何でも、リスト内の固定された数のアイテムによるパラメーター化(使用されない場合はnull)、パラメーターの有無にかかわらずクエリを動的に構築する、またはテーブル値パラメーターでストアドプロシージャを使用しても大きな違いはありません。ただし、私の一般的な推奨事項は次のとおりです。

いくつかのパラメーターを使用したケース/単純なクエリ:

動的SQL、おそらくテストでより良いパフォーマンスが示される場合はパラメーターを使用

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

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

大きなリストを持つクエリ:

テーブル値パラメーターを持つストアドプロシージャ。リストが大幅に異なる可能性がある場合は、ストアドプロシージャでWITH RECOMPILEを使用するか、パラメータなしで動的SQLを使用して、各クエリの新しい実行プランを生成します。


ここで「ストアドプロシージャ」とはどういう意味ですか?例を投稿できますか?
struhtanov 2013

9

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

1
CTEそして、@x非常に慎重に行われた場合に示すように、副選択の中にインライン化/除去することができ、この記事
robert4

9

私はデフォルトでこれにアプローチします(文字列からテーブルを返す)テーブル値関数を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

途方もなく長い文字列がない限り、これはテーブルインデックスでうまく機能するはずです。

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


8

別の可能な解決策は、ストアドプロシージャに可変数の引数を渡す代わりに、後の名前を含む単一の文字列を渡すが、それらを '<>'で囲むことによって一意にすることです。次に、PATINDEXを使用して名前を検索します。

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

8

次のストアドプロシージャを使用します。ここでは、カスタムの分割関数を使用しています

 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

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

可能な値のハードコードされたリストがない場合のこの作業の例を示すことができますか?
John Saunders、

@JohnSaunders、ハードコードされたリストを使用せずにスクリプトを編集しました。確認してください。
Gowdhaman008

3
このオプションの1つの制限。文字列が見つかった場合、CharIndexは1を返します。INは、正確な用語の一致を返します。「Stack」のCharIndexは「StackOverflow」という用語に対して1を返します。INは返しません。この制限を克服する '<'%name% '>'で名前を囲む、上記のPatIndexを使用したこの回答に対するマイナーな週があります。ただし、この問題のクリエイティブな解決策。
Richard Vivian

7

このような可変数の引数について、私が知っている唯一の方法は、SQLを明示的に生成するか、一時テーブルに必要な項目を設定し、一時テーブルに結合することです。


7

ColdFusionの私達はちょうど行います。

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

7

クエリ文字列で使用するローカルテーブルを再作成する手法を次に示します。この方法で行うと、解析の問題がすべて解消されます。

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

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 

7

ここに別の選択肢があります。カンマ区切りのリストを文字列パラメータとしてストアドプロシージャに渡すだけで、次のことが可能になります。

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

6

INはSELECTステートメントを受け入れるため、UDFを必要としない回答があります。例:SELECT * FROM Test where Data IN(SELECT Value 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

は上位投票の回答のより簡潔なバージョンを使用しています

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

5

この問題に対する別の答えを次に示します。

(2013年6月4日に投稿された新しいバージョン)。

    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

勝つ唯一の手はプレーしないことです。

無限の変動はありません。有限の変動のみ。

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が意味をなすようにするため)。

時々、不完全なソリューションが実際に機能する唯一のものです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.