1097

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

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

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

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

4

40 に答える 40

752

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

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がある場合は、SQLServerがパラメーターの一般的なカウントの計画もキャッシュすることになると思います。いつでも5つのパラメーターを追加し、指定されていないタグをNULLにすることができると思います-クエリプランは同じである必要がありますが、それは私にはかなり醜いようで、マイクロ最適化の価値があるかどうかはわかりません(ただし、 Stack Overflowで-それはそれだけの価値があるかもしれません)。

また、SQL Server 7以降ではクエリが自動パラメーター化されるため、パフォーマンスの観点からはパラメーターの使用は実際には必要ありませんが、セキュリティの観点からは重要です。特に、このようなユーザー入力データの場合は重要です。

于 2008-12-03T16:35:54.533 に答える
323

これが私が使用した手っ取り早いテクニックです:

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

これを実現する方法は他にもありますが、一部の人はよりクリーンだと考えるかもしれませんので、読み続けてください。

于 2008-12-03T16:41:17.037 に答える
256

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= new SqlMetaData(columnName, SqlDbType.NVarChar, 50); //50 as per SQL Type
    return values.Select(v => 
    {
       var r = new SqlDataRecord(metadata);
       r.SetValues(v);
       return r;
    });
}

@Dougに従って更新

避けてくださいvar metadata = SqlMetaData.InferFromValue(firstRecord, columnName);

最初の値の長さが設定されているため、最初の値が3文字の場合、設定されている最大長は3であり、他のレコードは3文字を超えると切り捨てられます。

だから、使ってみてください:var metadata= new SqlMetaData(columnName, SqlDbType.NVarChar, maxLen);

注:-1最大長の場合。

于 2008-12-03T16:53:19.507 に答える
191

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

ここで、これは元の質問に対する回答ではないことを述べさせてください。他の良い答えには、すでにいくつかのデモンストレーションがあります。

そうは言っても、先に進んでこの回答にフラグを立て、反対票を投じ、回答ではないとしてマークしてください...あなたが正しいと信じることは何でもしてください。

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

選択した回答

ここで取り上げたいのは、Joel Spolsky の回答で与えられたアプローチであり、正解として「選択された」回答です。

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

Nameしかし、このアプローチを一般化するという点では、列にワイルドカード文字 (LIKE 述語によって認識される) が含まれている場合など、よりあいまいなコーナー ケースも考えてみましょう。最も一般的に使用されているワイルドカード文字は%(パーセント記号) です。それでは、ここでそれを扱い、後で他のケースに進みましょう。

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

の名前の値を検討してください'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 '$'

このアプローチは、SQL Server だけでなく Oracle や MySQL でも機能するため、エスケープするよりも好きです。(\ バックスラッシュは正規表現で使用する文字なので、通常はエスケープ文字として使用します。

それらの厄介なブラケット

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

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

インライン式が乱雑になる

SQL のインライン式は長くなり、見苦しくなります。私たちはおそらくそれを機能させることができますが、天国は後になってそれを解読しなければならない貧しい魂を助けます. 私はインライン式のファンなので、ここではインライン式を使用しない傾向があります。主な理由は、混乱の理由を説明し、謝罪するコメントを残す必要がないためです。

関数どこ?

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

また、その関数は、DBMS とバージョンによって動作が異なる場合があります。(あらゆるデータベースエンジンを交換可能に使用できることに熱心なすべての Java 開発者に感謝します。)

領域知識

列のドメイン (つまり、列に適用される許容値のセット) に関する専門的な知識を持っている場合があります。列に格納されている値にはパーセント記号、アンダースコア、またはブラケットが含まれないことをアプリオリに知っている場合があります。その場合、それらのケースがカバーされているという簡単なコメントを含めます。

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

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


その他の問題の要約

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

  • SQL インジェクション(ユーザーが提供したと思われる情報を取得し、それをバインド変数を介して提供するのではなく、SQL テキストに含めます。バインド変数を使用する必要はありません。SQL インジェクションを阻止するための便利なアプローチの 1 つに過ぎません。他の方法もあります。それに対処する方法:

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

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


結論

私は Joel Spolsky のアプローチが好きです。賢いです。そして、それは機能します。

しかし、それを見た途端、すぐに潜在的な問題に気づきました。それを滑らせるのは私の性質ではありません。他人の努力を批判するつもりはありません。多くの開発者が自分の仕事を非常に個人的に受け止めていることを私は知っています。個人攻撃ではないのでご了承ください。ここで特定しているのは、テストではなく本番環境で発生する問題のタイプです。

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

于 2009-05-29T23:18:15.533 に答える
138

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

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

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
于 2008-12-03T16:27:11.353 に答える
66

今日のポッドキャストでJeff/Joelがこれについて話しているのを聞いた(エピソード34、2008-12-16(MP3、31 MB)、1時間03分38秒-1時間06分45秒)。StackOverflowを思い出したと思いました。LINQ to SQLを使用していましたが、おそらくそれは捨てられました。LINQtoSQLでも同じことが言えます。

var inValues = new [] { "ruby","rails","scruffy","rubyonrails" };

var results = from tag in Tags
              where inValues.Contains(tag.Name)
              select tag;

それでおしまい。そして、はい、LINQはすでに十分に後方を向いていますが、このContains句は私には余分に後方に見えます。作業中のプロジェクトに対して同様のクエリを実行する必要がある場合、ローカル配列とSQL Serverテーブルの間で結合を行うことで、当然、これを間違った方法で実行しようとしました。LINQからSQLへのトランスレータは、どういうわけか翻訳。そうではありませんでしたが、説明的なエラーメッセージが表示され、 Containsを使用するように指示されました。

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

于 2008-12-19T05:40:15.740 に答える
53

.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;
于 2011-06-15T11:04:06.157 に答える
39

関数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;

ライブデモ

受け入れられた答えはもちろん機能し、それは進むべき道の1つですが、それはアンチパターンです。

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。この質問は重複して使用されることが多いため、この回答を参照として追加しました。

于 2016-05-02T10:20:19.820 に答える
31

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

目的によっては、役に立つかもしれません。

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

これにより、できることの柔軟性が少し増しますが、クエリするテーブルが大きく、適切なインデックスがあり、パラメーター化されたリストを複数回使用したい場合により適しています。2 回実行する必要がなく、すべてのサニテーションを手動で行う必要がありません。

正確な速度をプロファイリングすることはできませんでしたが、私の状況では必要でした。

于 2008-12-03T17:04:00.563 に答える
25

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

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
于 2008-12-03T17:11:52.230 に答える
20

これは大雑把ですが、少なくとも1つあることが保証されている場合は、次のことができます。

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

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

于 2008-12-03T16:31:50.393 に答える
19

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

于 2008-12-03T16:30:13.970 に答える
18

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

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

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

于 2010-07-22T14:47:17.087 に答える
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;

オラクルのバージョン:

 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" は、リスト内で見つけることができるアイテムの最大数と同じ数の行を返さなければなりません)

于 2009-02-04T18:51:49.297 に答える
11

SQL Server 2008以降を使用している場合は、 Table Valued Parameter を使用します

運が悪くて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
于 2012-08-15T16:32:20.070 に答える
10

ここで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)
于 2011-10-24T18:41:41.893 に答える
10

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

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

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

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

崖のメモ:

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

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

動的 SQL。テストでより良いパフォーマンスが示された場合はパラメーターを使用できます。

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

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

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

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

于 2010-06-09T20:28:50.923 に答える
9

コンマ (,) で区切られた 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 にマップできます。

フィドラーのデモ

編集: 次のスクリプトを使用して、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
于 2012-11-23T18:13:47.133 に答える
9

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

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

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

于 2015-06-11T15:16:32.783 に答える
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
于 2012-07-26T05:39:30.733 に答える
8

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

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
于 2013-04-06T02:39:54.713 に答える
8

別の可能な解決策は、可変数の引数をストアド プロシージャに渡す代わりに、目的の名前を含む単一の文字列を渡しますが、名前を「<>」で囲んで一意にすることです。次に、PATINDEX を使用して名前を見つけます。

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0
于 2010-02-12T19:22:03.213 に答える
7

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

于 2008-12-03T16:31:13.967 に答える
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)
于 2012-05-29T23:49:39.050 に答える
7

ColdFusionでは、次のことを行うだけです。

<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
    <cfquery name="q">
        select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
    </cfquery>
于 2008-12-10T21:54:18.770 に答える
6

UDF、XML を必要としない回答があります IN は select ステートメントを受け入れるため、たとえば 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);
于 2011-05-13T15:03:27.923 に答える
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);
于 2015-03-12T18:11:18.840 に答える
4

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

C# & T-SQL string[] Pack/Unpack ユーティリティ関数

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

于 2012-08-10T14:29:44.760 に答える
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 が意味をなすようにするため)。

不十分なソリューションが実際に機能する唯一のソリューションである場合があります。

于 2011-04-28T21:56:21.520 に答える
4

(編集: テーブル値のパラメーターが利用できない場合) 多数の 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();
}

(さらにアイデアがあるかもしれません。並べ替えを省略し、リスト/配列の 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 がバッチサイズよりも小さいが、まだ 2 つ以上の場合、より小さなステートメントに分割されますが、それでも一定の長さになります。

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

  • 100個のパラメータを持つもの(バッチサイズ)、
  • 次に12で1
  • もう1つは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 スタイルのステートメント分割を好みました

この質問は、尋ねられてから5年以上経った今でも興味深いものです...

編集: 多くの値 (250 以上など) を持つ IN クエリは、特定のケースでは、SQL Server では依然として遅くなる傾向があることに注意しました。DB が一種の一時テーブルを内部で作成し、それに対して結合することを期待していましたが、単一値の SELECT 式を n 回しか繰り返していないように見えました。クエリあたりの時間は最大で約 200 ミリ秒でした。元の ID の取得 SELECT を他の関連テーブルに結合するよりもさらに悪い..また、SQL Server Profiler には 10 から 15 の CPU ユニットがあり、同じパラメーター化されたものを繰り返し実行するのは異常です。これは、繰り返しの呼び出しで新しいクエリ プランが作成されたことを示唆しています。おそらく、個別のクエリのようなその場しのぎはまったく悪くないでしょう。最終的な結論を得るために、これらのクエリをサイズを変更した非分割クエリと比較する必要がありましたが、今のところ、とにかく長い IN 句を避ける必要があるようです。

于 2014-01-12T00:32:38.367 に答える
3
    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 ,',')])
于 2015-03-18T07:43:22.630 に答える