配列をSQL Serverストアドプロシージャに渡す方法


293

配列をSQL Serverストアドプロシージャに渡す方法は?

たとえば、従業員のリストがあります。このリストをテーブルとして使用し、別のテーブルと結合したい。ただし、従業員のリストはC#からパラメーターとして渡す必要があります。



回答:


437

SQL Server 2008(またはそれ以降)

まず、データベースで、次の2つのオブジェクトを作成します。

CREATE TYPE dbo.IDList
AS TABLE
(
  ID INT
);
GO

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List AS dbo.IDList READONLY
AS
BEGIN
  SET NOCOUNT ON;

  SELECT ID FROM @List; 
END
GO

今あなたのC#コードで:

// Obtain your list of ids to send, this is just an example call to a helper utility function
int[] employeeIds = GetEmployeeIds();

DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("ID", typeof(int)));

// populate DataTable from your List here
foreach(var id in employeeIds)
    tvp.Rows.Add(id);

using (conn)
{
    SqlCommand cmd = new SqlCommand("dbo.DoSomethingWithEmployees", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@List", tvp);
    // these next lines are important to map the C# DataTable object to the correct SQL User Defined Type
    tvparam.SqlDbType = SqlDbType.Structured;
    tvparam.TypeName = "dbo.IDList";
    // execute query, consume results, etc. here
}

SQL Server 2005

SQL Server 2005を使用している場合でも、XMLよりも分割関数をお勧めします。まず、関数を作成します。

CREATE FUNCTION dbo.SplitInts
(
   @List      VARCHAR(MAX),
   @Delimiter VARCHAR(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT Item = CONVERT(INT, Item) FROM
      ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
        FROM ( SELECT [XML] = CONVERT(XML, '<i>'
        + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
      WHERE Item IS NOT NULL
  );
GO

これで、ストアドプロシージャは次のようになります。

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List VARCHAR(MAX)
AS
BEGIN
  SET NOCOUNT ON;

  SELECT EmployeeID = Item FROM dbo.SplitInts(@List, ','); 
END
GO

そしてあなたのC#コードではあなたはリストを'1,2,3,12'... として渡す必要があります...


テーブル値パラメーターを渡す方法は、それを使用するソリューションの保守性を簡素化し、XMLや文字列分割を含む他の実装と比較してパフォーマンスを向上させることがよくあります。

入力は明確に定義され(区切り文字がコンマまたはセミコロンであるかどうかを推測する必要はありません)、ストアドプロシージャのコードを検査しないと明らかでない他の処理関数への依存関係はありません。

UDTの代わりにユーザー定義のXMLスキーマを使用するソリューションと比較すると、これには同様の数の手順が含まれますが、私の経験では、管理、維持、および読み取るコードははるかに単純です。

多くのソリューションでは、多くのストアドプロシージャで再利用するこれらのUDT(ユーザー定義タイプ)の1つまたはいくつかのみが必要な場合があります。この例と同様に、一般的な要件はIDポインターのリストを渡すことです。関数名は、それらのIdが表すコンテキストを記述し、型名は総称でなければなりません。


3
私はテーブルパラメータのアイデアが好きです。それだけの価値があるので、区切り文字はSplitInts()関数呼び出しに渡す必要があります。
ドラマー2013年

カンマ区切りの文字列にのみアクセスできる場合に、テーブルパラメータを使用するにはどうすればよいですか
bdwain

@bdwainは目的を達成しません。TVPに入れるには、分割関数を使用して行に分割する必要があります。あなたのアプリケーションコードでそれを分解してください。
アーロンベルトラン

1
@AaronBertrandの応答に感謝、私は実際にそれを理解しました。括弧内のサブ選択を使用する必要があります:SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds)
JaKXz 2014

3
@ th1rdey3これらは暗黙的にオプションです。stackoverflow.com/a/18926590/61305
アーロンベルトラン

44

私の経験に基づいて、employeeIDsから区切られた式を作成することにより、この問題に対するトリッキーで素晴らしい解決策があります。あなただけのような文字列式を作成する必要があります';123;434;365;'する- 123434および365いくつかのemployeeIDsです。以下のプロシージャを呼び出してこの式を渡すことにより、目的のレコードをフェッチできます。「別のテーブル」をこのクエリに簡単に結合できます。このソリューションは、SQLサーバーのすべてのバージョンに適しています。また、テーブル変数や一時テーブルを使用する場合と比較して、非常に高速で最適化されたソリューションです。

CREATE PROCEDURE dbo.DoSomethingOnSomeEmployees  @List AS varchar(max)
AS
BEGIN
  SELECT EmployeeID 
  FROM EmployeesTable
  -- inner join AnotherTable on ...
  where @List like '%;'+cast(employeeID as varchar(20))+';%'
END
GO

いいね!私は、intキーでフィルタリングしているこのアプローチが本当に好きです!+1
MDV2000 2016

@ MDV2000おかげで:)文字列キーでは、int列をvarcharにキャストしないため、これも良好なパフォーマンスを発揮します...
Hamed Nazaktabar

私はゲームに遅れましたが、これはとても賢いです!私の問題に最適です。
user441058

これは素晴らしいです!私は間違いなくこれを使用します、ありがとう
Omar Ruder

26

ストアドプロシージャにテーブル値パラメーターを使用します。

C#から渡す場合は、SqlDb.Structuredのデータ型のパラメーターを追加します。

こちらを参照してください:http : //msdn.microsoft.com/en-us/library/bb675163.aspx

例:

// Assumes connection is an open SqlConnection object.
using (connection)
{
// Create a DataTable with the modified rows.
DataTable addedCategories =
  CategoriesDataTable.GetChanges(DataRowState.Added);

// Configure the SqlCommand and SqlParameter.
SqlCommand insertCommand = new SqlCommand(
    "usp_InsertCategories", connection);
insertCommand.CommandType = CommandType.StoredProcedure;
SqlParameter tvpParam = insertCommand.Parameters.AddWithValue(
    "@tvpNewCategories", addedCategories);
tvpParam.SqlDbType = SqlDbType.Structured;

// Execute the command.
insertCommand.ExecuteNonQuery();
}

17

これをXMLパラメータとして渡す必要があります。

編集:あなたのアイデアを与える私のプロジェクトからの簡単なコード:

CREATE PROCEDURE [dbo].[GetArrivalsReport]
    @DateTimeFrom AS DATETIME,
    @DateTimeTo AS DATETIME,
    @HostIds AS XML(xsdArrayOfULong)
AS
BEGIN
    DECLARE @hosts TABLE (HostId BIGINT)

    INSERT INTO @hosts
        SELECT arrayOfUlong.HostId.value('.','bigint') data
        FROM @HostIds.nodes('/arrayOfUlong/u') as arrayOfUlong(HostId)

その後、一時テーブルを使用してテーブルと結合できます。arrayOfUlongは、データの整合性を維持するための組み込みXMLスキーマとして定義しましたが、必ずしもそうする必要はありません。これを使用することをお勧めします。ここに、常にlongを含むXMLを確実に取得するための簡単なコードを示します。

IF NOT EXISTS (SELECT * FROM sys.xml_schema_collections WHERE name = 'xsdArrayOfULong')
BEGIN
    CREATE XML SCHEMA COLLECTION [dbo].[xsdArrayOfULong]
    AS N'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="arrayOfUlong">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded"
                            name="u"
                            type="xs:unsignedLong" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>';
END
GO

行が多い場合にテーブル変数を使用するのは悪い考えだと思いましたか?代わりに一時(#table)テーブルを使用する方がパフォーマンスが向上しませんか?
ganders '19 / 06/19

@ganders:その逆も言えるでしょう。
abatishchev 2014

14

配列のサイズ複雑さなど、コンテキストは常に重要です。中小規模のリストの場合、ここに掲載されている回答のいくつかは問題ありませんが、いくつかの明確化が必要です。

  • 区切りリストを分割するには、SQLCLRベースのスプリッターが最も高速です。独自のサンプルを作成する場合や、CLR関数の無料のSQL#ライブラリ(私が作成したものの、String_Split関数などの多くは完全に無料)をダウンロードできる場合は、数多くの例があります。
  • 分割XMLベースのアレイはでき AaronBertrandのXMLの例@彼のコードとして最もよく使用されているものののみ、ここでの回答に示すタイプで要素ベースのXML(、速くないことがありますが、属性ベースのXMLを使用する必要がありますtext()XML関数:XMLを使用してリストを分割する方法の詳細(つまり、パフォーマンス分析)については、フィルファクターの「XMLを使用してSQL Serverのパラメーターとしてリストを渡す」を参照してください
  • データはprocにストリーミングされ、事前に解析され、厳密に型指定されたテーブル変数として表示されるため、TVPの使用は素晴らしいです(少なくともSQL Server 2008以降を使用している場合)。ただし、ほとんどの場合、すべてのDataTableデータを格納するとは、元のコレクションからコピーされたデータをメモリに複製することを意味します。したがってDataTable、TVPを渡す方法を使用しても、より大きなデータセットではうまく機能しません(つまり、適切にスケーリングされません)。
  • XMLは、IntやStringの単純な区切りリストとは異なり、TVPと同様に1次元配列以上を処理できます。ただし、DataTableTVPメソッドと同様に、XMLは、XML文書のオーバーヘッドを追加で考慮する必要があるため、メモリ内のデータサイズを2倍以上に拡張できないため、適切にスケーリングされません。

以上すべてを踏まえて、使用しているデータが大きいか、それほど大きくないが、常に増加している場合IEnumerable、データをSQL ServerにストリーミングするTVPメソッドが(DataTableメソッドと同様に)最良の選択ですが、(他のメソッドとは異なり)メモリ内のコレクションの複製が必要です。SQLとC#コードの例をこの回答に投稿しました:

ストアドプロシージャT-SQLにディクショナリを渡す


6

SQLサーバーでは配列はサポートされていませんが、コレクションをストアドプロシージャに渡す方法はいくつかあります。

  1. データテーブルを使用する
  2. XMLを使用して、コレクションをxml形式に変換してから、それを入力としてストアドプロシージャに渡します。

以下のリンクはあなたを助けるかもしれません

コレクションをストアドプロシージャに渡す


5

私は、このlinKが見つかるまで、新しいテーブルタイプを作成する手間をかけずに配列をSQLサーバーに渡す方法のすべての例と回答を検索してきました。以下は、プロジェクトにどのように適用したかを示しています。

-次のコードは、配列をパラメーターとして取得し、その値の配列を別のテーブルに挿入します。

Create Procedure Proc1 


@UserId int, //just an Id param
@s nvarchar(max)  //this is the array your going to pass from C# code to your Sproc

AS

    declare @xml xml

    set @xml = N'<root><r>' + replace(@s,',','</r><r>') + '</r></root>'

    Insert into UserRole (UserID,RoleID)
    select 
       @UserId [UserId], t.value('.','varchar(max)') as [RoleId]


    from @xml.nodes('//root/r') as a(t)
END 

楽しんでください


2
@zaitsman:CLEANESTは、最良または最適という意味ではありません。「クリーン」なコードを取得するために、柔軟性や「適切な」複雑さ、パフォーマンスを放棄することがよくあります。この答えは「大丈夫」ですが、小さなデータセットに対してのみです。入力配列@sがCSVの場合、単純にそれを分割する方が高速です(つまり、INSERT INTO ... SELECT FROM SplitFunction)。XMLへの変換はCLRよりも遅く、属性ベースのXMLはとにかく高速です。そして、これは単純なリストですが、XMLまたはTVPで渡すと、複雑な配列も処理できます。単純な1回限りの操作を回避することで何が得られるのかわからないCREATE TYPE ... AS TABLE
ソロモンルツキー2014

5

これはあなたを助けます。:)次の手順に従います。

  1. クエリデザイナーを開く
  2. 次のコードをそのままコピーして貼り付け、文字列をIntに変換する関数を作成します

    CREATE FUNCTION dbo.SplitInts
    (
       @List      VARCHAR(MAX),
       @Delimiter VARCHAR(255)
    )
    RETURNS TABLE
    AS
      RETURN ( SELECT Item = CONVERT(INT, Item) FROM
          ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
            FROM ( SELECT [XML] = CONVERT(XML, '<i>'
            + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
              ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
          WHERE Item IS NOT NULL
      );
    GO
  3. 次のストアドプロシージャを作成する

     CREATE PROCEDURE dbo.sp_DeleteMultipleId
     @List VARCHAR(MAX)
     AS
     BEGIN
          SET NOCOUNT ON;
          DELETE FROM TableName WHERE Id IN( SELECT Id = Item FROM dbo.SplitInts(@List, ',')); 
     END
     GO
  4. このSPを実行これを使用exec sp_DeleteId '1,2,3,12'して、削除するIDの文字列を指定します。

  5. C#で配列を文字列に変換し、それをストアドプロシージャパラメーターとして渡します。

    int[] intarray = { 1, 2, 3, 4, 5 };  
    string[] result = intarray.Select(x=>x.ToString()).ToArray();

     

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandText = "sp_DeleteMultipleId";
    command.CommandType = CommandType.StoredProcedure;
    command.Parameters.Add("@Id",SqlDbType.VARCHAR).Value=result ;

これにより、複数の行が削除されます。


私はこのコンマ区切りの解析関数を使用しました。小さなデータセットで機能します。実行プランを確認すると、大きなデータセットで問題が発生し、ストアドプロシージャで複数のcsvリストを作成する必要があります
Saboor Awan

2

これを理解するのに長い時間がかかったので、誰かがそれを必要とする場合に備えて...

これは、Aaronの回答のSQL 2005メソッドに基づいており、彼のSplitInts関数を使用しています(常にコンマを使用するため、delimパラメータを削除しました)。SQL 2008を使用していますが、型指定されたデータセット(XSD、TableAdapters)で機能するものが必要でしたが、文字列パラメーターがそれらで機能することはわかっています。

私は彼の関数を "where in(1,2,3)"型の節で動作させようとしており、簡単な方法では運がありませんでした。そこで、まず一時テーブルを作成し、次に「どこに」の代わりに内部結合を行いました。これが私の使用例です。私の場合、特定の成分を含まないレシピのリストを取得したいと思います。

CREATE PROCEDURE dbo.SOExample1
    (
    @excludeIngredientsString varchar(MAX) = ''
    )
AS
    /* Convert string to table of ints */
    DECLARE @excludeIngredients TABLE (ID int)
    insert into @excludeIngredients
    select ID = Item from dbo.SplitInts(@excludeIngredientsString)

    /* Select recipies that don't contain any ingredients in our excluded table */
   SELECT        r.Name, r.Slug
FROM            Recipes AS r LEFT OUTER JOIN
                         RecipeIngredients as ri inner join
                         @excludeIngredients as ei on ri.IngredientID = ei.ID
                         ON r.ID = ri.RecipeID
WHERE        (ri.RecipeID IS NULL)

一般的に言えば、テーブル変数に結合するのではなく、一時テーブルに結合するのが最善です。テーブル変数は、デフォルトでは1行しかないように見えますが、その周りには1つまたは2つのトリックがあります(@AaronBertrandの優れた詳細な記事:sqlperformance.com/2014/06/t-sql-queries/…を確認してください)。
ソロモンRutzky

1

他の人が前述したように、これを行う1つの方法は、配列を文字列に変換してから、SQL Server内で文字列を分割することです。

SQL Server 2016以降、文字列を分割する組み込みの方法があります。

STRING_SPLIT()

一時テーブル(または実際のテーブル)に挿入できる行のセットを返します。

DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"
SELECT value FROM STRING_SPLIT(@str, ';')

次のようになります:

値
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

もっと凝ったものにしたい場合:

DECLARE @tt TABLE (
    thenumber int
)
DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"

INSERT INTO @tt
SELECT value FROM STRING_SPLIT(@str, ';')

SELECT * FROM @tt
ORDER BY thenumber

上記と同じ結果が得られますが(列名が「thenumber」の場合を除く)、ソートされています。他のテーブルと同じようにテーブル変数を使用できるため、必要に応じてDB内の他のテーブルと簡単に結合できます。

STRING_SPLIT()関数が認識されるためには、SQL Serverのインストールが互換性レベル130以上である必要があることに注意してください。次のクエリで互換性レベルを確認できます。

SELECT compatibility_level
FROM sys.databases WHERE name = 'yourdatabasename';

ほとんどの言語(C#を含む)には、配列から文字列を作成するために使用できる「結合」関数があります。

int[] myarray = {22, 33, 44};
string sqlparam = string.Join(";", myarray);

次にsqlparam、パラメーターとして上記のストアドプロシージャに渡します。


0
CREATE TYPE dumyTable
AS TABLE
(
  RateCodeId int,
  RateLowerRange int,
  RateHigherRange int,
  RateRangeValue int
);
GO
CREATE PROCEDURE spInsertRateRanges
  @dt AS dumyTable READONLY
AS
BEGIN
  SET NOCOUNT ON;

  INSERT  tblRateCodeRange(RateCodeId,RateLowerRange,RateHigherRange,RateRangeValue) 
  SELECT * 
  FROM @dt 
END
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.