配列パラメーターをストアドプロシージャに渡す


53

大量のレコード(1000枚)を取得してそれらを処理するプロセスがあり、完了したら、大量のレコードを処理済みとしてマークする必要があります。IDの大きなリストでこれを示すことができます。「ループ内の更新」パターンを回避しようとしているので、このIDのバッグをMS SQL Server 2008ストアドプロシージャに送信するより効率的な方法を見つけたいと思います。

提案#1-テーブル値パラメーター。IDフィールドだけでテーブルタイプを定義し、更新するIDでいっぱいのテーブルを送信できます。

提案#2-proc本体のOPENXML()を使用したXMLパラメーター(varchar)。

提案#3-リストの解析。扱いにくく、エラーが発生しやすいと思われるため、可能であれば、これを避けたいと思います。

これらの中に好みがありますか、それとも私が見逃したアイデアはありますか?


IDの大きなリストをどのように取得していますか?
ラリーコールマン

別のストアドプロシージャを介して「ペイロード」データとともにそれらをプルダウンします。ただし、すべてのデータを更新する必要はありません。特定のレコードのフラグを更新するだけです。
D.ランバート

回答:


42

この問題に関する史上最高の記事は、Erland Sommarskogによるものです。

彼はすべてのオプションをカバーし、かなりよく説明しています。

答えが短かったので申し訳ありませんが、ErlandのArraysに関する記事はJoe Celkoのツリーやその他のSQLの扱いに関する本のようなものです:)


23

これについてはStackOverflowで多くのアプローチをカバーする素晴らしい議論があります。SQL Server 2008+で私が好むのは、テーブル値パラメーター使用することです。これは本質的に、SQL Serverの問題に対するソリューションです。値のリストをストアドプロシージャに渡します。

このアプローチの利点は次のとおりです。

  • 1つのパラメーターとして渡されるすべてのデータを使用して、1つのストアドプロシージャを呼び出します
  • テーブル入力は構造化され、強く型付けされています
  • XMLの文字列構築/解析または処理なし
  • 簡単にテーブル入力を使用して、フィルタリング、結合などを行うことができます

しかし、注意してください:あなたはADO.NETやODBC経由でその用途のTVPsストアドプロシージャを呼び出すと、SQL Serverプロファイラでの活動を見ている場合は、SQL Serverは、いくつか受けていることがわかりますINSERTTVPをロードするための文を、各行に1つずつTVPで、プロシージャへの呼び出しが続きます。これは仕様によるものですINSERTsのこのバッチは、プロシージャが呼び出されるたびにコンパイルする必要があり、小さなオーバーヘッドを構成します。ただし、このオーバーヘッドがあっても、TVP は大部分のユースケースでパフォーマンスと使いやすさの点で他のアプローチを吹き飛ばします。

さらに詳しく知りたい場合は、Erland Sommarskogがテーブル値パラメーターの仕組みについて完全に説明し、いくつかの例を示します。

以下に、私が作成した別の例を示します。

CREATE TYPE id_list AS TABLE (
    id int NOT NULL PRIMARY KEY
);
GO

CREATE PROCEDURE [dbo].[tvp_test] (
      @param1           INT
    , @customer_list    id_list READONLY
)
AS
BEGIN
    SELECT @param1 AS param1;

    -- join, filter, do whatever you want with this table 
    -- (other than modify it)
    SELECT *
    FROM @customer_list;
END;
GO

DECLARE @customer_list id_list;

INSERT INTO @customer_list (
    id
)
VALUES (1), (2), (3), (4), (5), (6), (7);

EXECUTE [dbo].[tvp_test]
      @param1 = 5
    , @customer_list = @customer_list
;
GO

DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO

これを実行すると、エラーが発生します:メッセージ2715、レベル16、状態3、プロシージャtvp_test、行4 [バッチ開始行4]列、パラメーター、または変数#2:データ型id_listが見つかりません。パラメーターまたは変数 '@customer_list'のデータ型が無効です。メッセージ1087、レベル16、状態1、プロシージャtvp_test、行13 [バッチ開始行4]テーブル変数「@customer_list」を宣言する必要があります。
ダミアン

@Damian- CREATE TYPE最初のステートメントは正常に実行されましたか?実行しているSQL Serverのバージョンは何ですか?
ニックチャマス

SPコードでは、この文をインライン `SELECT @ param1 AS param1; 'にします。。目的は何ですか?またはparam1を使用しないのに、なぜこれをパラメーターとしてSPヘッダーに入れたのですか?
EAmez

@EAmez-これは単なるarbitrary意的な例です。ポイントは@customer_listそうではありません@param1。この例は、さまざまなタイプのパラメーターを混合できることを示しています。
ニックチャンマス

21

主題全体については、Erland Sommarskogによる最終記事「SQL Serverの配列とリスト」で説明されています。選択するバージョンを選択してください。

要約、TVPが残りを圧倒するSQL Server 2008 以前の場合

  • CSV、好きなように分割します(通常は数値テーブルを使用します)
  • XMLと解析(SQL Server 2005+でより良い)
  • クライアントで一時テーブルを作成する

とにかく、この記事を読んで、他のテクニックや考え方を見る価値があります。

編集:他の場所にある巨大なリストに対する遅い回答:配列パラメーターをストアドプロシージャに渡す


14

私はこのパーティーに遅れていることを知っていますが、過去にそのような問題があり、最大10万個のbigint番号を送信しなければならず、いくつかのベンチマークを行いました。最終的に、それらをバイナリ形式で画像として送信することになりました。これは、最大10万個の数字に対して他のすべてよりも高速でした。

古い(SQL Server 2005)コードは次のとおりです。

SELECT  Number * 8 + 1 AS StartFrom ,
        Number * 8 + 8 AS MaxLen
INTO    dbo.ParsingNumbers
FROM    dbo.Numbers
GO

CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
    ( SELECT    CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
      FROM      dbo.ParsingNumbers
      WHERE     MaxLen <= DATALENGTH(@BIGINTs)
    )
GO

次のコードは、整数をバイナリblobにパックしています。ここでバイトの順序を逆にしています:

static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito   = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}

9

私はあなたにSOを紹介するか、ここで答えるのか、これはほとんどプログラミングの質問だから破れています。しかし、私はすでに使用しているソリューションを持っているので...私はそれを投稿します;)

これが機能する方法は、カンマ区切りの文字列(単純な分割、CSVスタイルの分割は行わない)をvarchar(4000)としてストアドプロシージャにフィードし、そのリストをこの関数にフィードして、便利なテーブルを取得することです。単なるvarcharsのテーブル。

これにより、処理したいIDの値のみを送信でき、その時点で簡単な結合を行うことができます。

代わりに、CLR DataTableを使用して何かを行うこともできますが、それはサポートするためのオーバーヘッドが少し多く、全員がCSVリストを理解します。

USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [dbo].[splitListToTable] (@list      nvarchar(MAX), @delimiter nchar(1) = N',')
      RETURNS @tbl TABLE (value     varchar(4000)      NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists 

Need an easy non-dynamic way to split a list of strings on input for comparisons

Usage like thus:

DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'

SELECT * FROM (

select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x 
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )

*/
BEGIN
   DECLARE @endpos   int,
           @startpos int,
           @textpos  int,
           @chunklen smallint,
           @tmpstr   nvarchar(4000),
           @leftover nvarchar(4000),
           @tmpval   nvarchar(4000)

   SET @textpos = 1
   SET @leftover = ''
   WHILE @textpos <= datalength(@list) / 2
   BEGIN
      SET @chunklen = 4000 - datalength(@leftover) / 2
      SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
      SET @textpos = @textpos + @chunklen

      SET @startpos = 0
      SET @endpos = charindex(@delimiter, @tmpstr)

      WHILE @endpos > 0
      BEGIN
         SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
                                             @endpos - @startpos - 1)))
         INSERT @tbl (value) VALUES(@tmpval)
         SET @startpos = @endpos
         SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
      END

      SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
   END

   INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
   RETURN
END

まあ、私はそのような何かを書く必要がないように、コンマ区切りのリストを避けようとしましたが、すでに書かれているので、私はそのソリューションをミックスに戻す必要があると思います。;-)
D.ランバート

1
私は試されたと言うと本当は最も簡単です。コードの数秒でC#でコンマ区切りのリストを吐き出すことができ、それをこの関数に(sprocに入れた後)十分に素早く投げることができ、それについて考えることさえほとんどありません。〜そして、私はあなたが機能を使用したくないと言ったけど、私はそれが最も簡単な方法(おそらく最も効果的ではない)だと思う
jcolebrand

5

さまざまなSQL Serverストアドプロシージャで処理されるように、アプリケーションから送信された1000行と10000行のセットを定期的に受け取ります。

パフォーマンスの要求を満たすために、TVPを使用しますが、dbDataReaderの独自の抽象を実装して、デフォルトモードの処理でパフォーマンスの問題を克服する必要があります。このリクエストの範囲外であるため、方法と理由については説明しません。

10,000を超える「行」でパフォーマンスを維持するXML実装が見つからなかったため、XML処理を検討しませんでした。

リスト処理は、1次元および2次元の集計(数値)テーブル処理によって処理できます。これらをさまざまな分野で使用できましたが、数百の「列」がある場合、適切に管理されたTVPの方がパフォーマンスが高くなります。

SQL Serverの処理に関するすべての選択と同様に、使用モデルに基づいて選択を行う必要があります。


5

最終的にいくつかのTableValuedParametersを実行する機会を得たので、それらはうまく機能するので、現在のコードのサンプルを使用して、それらの使用方法を示すたくさんのコードを貼り付けます:(注:ADOを使用します。ネット)

また、注意:私はサービス用のコードを書いており、他のクラスには多くの事前定義されたコードビットがありますが、これをデバッグできるようにコンソールアプリとして書いているので、これをすべてリッピングしましたコンソールアプリ。「ハードコードされた接続文字列のような」私のコーディングスタイルは、「捨てる1つを構築する」ようなものだったので、すみません。使用方法を示しList<customObject>、ストアドプロシージャで使用できるテーブルとしてデータベースに簡単にプッシュしたかったのです。以下のC#およびTSQLコード:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;

namespace a.EventAMI {
    class Db {
        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static void Update(List<Current> currents) {
            const string CONSTR = @"just a hardwired connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );
            cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class

            try {
                using ( con ) {
                    con.Open();
                    cmd.ExecuteNonQuery();
                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }
        }
    }
    class Current {
        public string Identifier { get; set; }
        public string OffTime { get; set; }
        public DateTime Off() {
            return Convert.ToDateTime( OffTime );
        }

        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static List<Current> GetAll() {
            List<Current> l = new List<Current>();

            const string CONSTR = @"just a hardcoded connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );

            try {
                using ( con ) {
                    con.Open();
                    using ( SqlDataReader reader = cmd.ExecuteReader() ) {
                        while ( reader.Read() ) {
                            l.Add(
                                new Current {
                                    Identifier = reader[0].ToString(),
                                    OffTime = reader[1].ToString()
                                } );
                        }
                    }

                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }

            return l;
        }
    }
}

-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;

namespace a {
    public static class Converter {
        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
            return GetDataTableFromIEnumerable( aIEnumerable, null );
        }

        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
            DataTable returnTable = new DataTable();

            if ( aIEnumerable != null ) {
                //Creates the table structure looping in the in the first element of the list
                object baseObj = null;

                Type objectType;

                if ( baseType == null ) {
                    foreach ( object obj in aIEnumerable ) {
                        baseObj = obj;
                        break;
                    }

                    objectType = baseObj.GetType();
                } else {
                    objectType = baseType;
                }

                PropertyInfo[] properties = objectType.GetProperties();

                DataColumn col;

                foreach ( PropertyInfo property in properties ) {
                    col = new DataColumn { ColumnName = property.Name };
                    if ( property.PropertyType == typeof( DateTime? ) ) {
                        col.DataType = typeof( DateTime );
                    } else if ( property.PropertyType == typeof( Int32? ) ) {
                        col.DataType = typeof( Int32 );
                    } else {
                        col.DataType = property.PropertyType;
                    }
                    returnTable.Columns.Add( col );
                }

                //Adds the rows to the table

                foreach ( object objItem in aIEnumerable ) {
                    DataRow row = returnTable.NewRow();

                    foreach ( PropertyInfo property in properties ) {
                        Object value = property.GetValue( objItem, null );
                        if ( value != null )
                            row[property.Name] = value;
                        else
                            row[property.Name] = "";
                    }

                    returnTable.Rows.Add( row );
                }
            }
            return returnTable;
        }

    }
}

USE [Database]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROC [dbo].[Event_Update]
    @EventCurrentTVP    Event_CurrentTVP    READONLY
AS

/****************************************************************
    author  cbrand
    date    
    descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
    caller  such and thus application
****************************************************************/

BEGIN TRAN Event_Update

DECLARE @DEBUG INT

SET @DEBUG = 0 /* test using @DEBUG <> 0 */

/*
    Replace the list of outstanding entries that are still currently disconnected with the list from the file
    This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]

INSERT INTO [database].[dbo].[Event_Current]
           ([Identifier]
            ,[OffTime])
SELECT [Identifier]
      ,[OffTime]
  FROM @EventCurrentTVP

IF (@@ERROR <> 0 OR @DEBUG <> 0) 
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END

USE [Database]
GO

CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
    [Identifier] [varchar](20) NULL,
    [OffTime] [datetime] NULL
)
GO

また、(この質問に出くわすすべての読者に)提供することがあれば、コーディングスタイルについて建設的な批判をしますが、建設的なものにしてください;)...本当に私が欲しいなら、ここのチャットルームで見つけてください。このコードのチャンクList<Current>で、dbとList<T>アプリケーションのテーブルとして定義されているように、どのように使用できるかを確認できれば幸いです。


3

提案#1を使用するか、代わりに、処理済みIDのみを保持するスクラッチテーブルを作成します。処理中にそのテーブルに挿入し、終了したら、次のようなprocを呼び出します。

BEGIN TRAN

UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;

TRUNCATE TABLE processedIds

COMMIT TRAN

多くの挿入を行いますが、小さなテーブルに対して行われるため、高速になります。ADO.netまたは使用しているデータアダプターを使用して、挿入をバッチ処理することもできます。


2

質問のタイトルには、アプリケーションからストアドプロシージャにデータを送信するタスクが含まれています。その部分は質問本文によって除外されていますが、これにも答えてみましょう。

タグで指定されているsql-server-2008のコンテキストでは、SQL Server 2008の E. Sommarskog Arrays and Listsによる別のすばらしい記事があります。ところで、私はマリアンが彼の答えで言及した記事でそれを見つけました。

リンクを提供するだけでなく、そのコンテンツのリストを引用します。

  • 前書き
  • バックグラウンド
  • T-SQLのテーブル値パラメーター
  • ADO .NETからテーブル値パラメーターを渡す
    • リストを使用する
    • DataTableを使用する
    • DataReaderを使用する
    • 最後に
  • 他のAPIからのテーブル値パラメーターの使用
    • ODBC
    • OLE DB
    • ADO
    • LINQとエンティティフレームワーク
    • JDBC
    • PHP
    • Perl
    • APIがTVPをサポートしない場合
  • パフォーマンスに関する考慮事項
    • サーバ側
    • クライアント側
    • 主キーかどうか
  • 謝辞とフィードバック
  • 改訂履歴

そこに記載されている手法を超えて、私はいくつかのケースではバルクコピーとバルク挿入が一般的なケースの範囲に言及されるに値するという気がします。


1

配列パラメーターをストアドプロシージャに渡す

MS SQL 2016最新バージョンの場合

MS SQL 2016では、複数の値を解析するための新しい関数SPLIT_STRING()が導入されています。

これにより、問題を簡単に解決できます。

MS SQLの古いバージョンの場合

古いバージョンを使用している場合は、次の手順に従ってください。

最初に1つの関数を作成します。

 ALTER FUNCTION [dbo].[UDF_IDListToTable]
 (
    @list          [varchar](MAX),
    @Seperator     CHAR(1)
  )
 RETURNS @tbl TABLE (ID INT)
 WITH 

 EXECUTE AS CALLER
 AS
  BEGIN
    DECLARE @position INT
    DECLARE @NewLine CHAR(2) 
    DECLARE @no INT
    SET @NewLine = CHAR(13) + CHAR(10)

    IF CHARINDEX(@Seperator, @list) = 0
    BEGIN
    INSERT INTO @tbl
    VALUES
      (
        @list
      )
END
ELSE
BEGIN
    SET @position = 1
    SET @list = @list + @Seperator
    WHILE CHARINDEX(@Seperator, @list, @position) <> 0
    BEGIN
        SELECT @no = SUBSTRING(
                   @list,
                   @position,
                   CHARINDEX(@Seperator, @list, @position) - @position
               )

        IF @no <> ''
            INSERT INTO @tbl
            VALUES
              (
                @no
              )

        SET @position = CHARINDEX(@Seperator, @list, @position) + 1
    END
END
RETURN
END

これを行った後、セパレータでこの関数に文字列を渡すだけです。

これがあなたのお役に立てば幸いです。:-)


-1

これを使用して、「タイプテーブルの作成」を作成します。ユーザーの簡単な例

CREATE TYPE unit_list AS TABLE (
    ItemUnitId int,
    Amount float,
    IsPrimaryUnit bit
);

GO
 CREATE TYPE specification_list AS TABLE (
     ItemSpecificationMasterId int,
    ItemSpecificationMasterValue varchar(255)
);

GO
 declare @units unit_list;
 insert into @units (ItemUnitId, Amount, IsPrimaryUnit) 
  values(12,10.50, false), 120,100.50, false), (1200,500.50, true);

 declare @spec specification_list;
  insert into @spec (ItemSpecificationMasterId,temSpecificationMasterValue) 
   values (12,'test'), (124,'testing value');

 exec sp_add_item "mytests", false, @units, @spec


//Procedure definition
CREATE PROCEDURE sp_add_item
(   
    @Name nvarchar(50),
    @IsProduct bit=false,
    @UnitsArray unit_list READONLY,
    @SpecificationsArray specification_list READONLY
)
AS


BEGIN
    SET NOCOUNT OFF     

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