web-dev-qa-db-ja.com

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

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

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

プロポーザル#2-プロシージャ本体にOPENXML()を含むXMLパラメータ(varchar)。

提案#3-リストの解析。扱いにくく、エラーが発生しやすいように見えるので、できれば回避します。

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

54
D. Lambert

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

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

答えが短かすぎて申し訳ありませんが、Erlandの配列に関する記事は、Joe Celkoによる木やその他のSQLの扱いに関する本のようなものです:)

44
Marian

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

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

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

ただし、注意してください:ADO.NETまたはODBCを介してTVPを使用するストアドプロシージャを呼び出し、 SQL Serverプロファイラを使用してアクティビティを見ると、SQL ServerがTVPをロードするためのいくつかのINSERTステートメントを受信して​​いることがわかります TVPの各行に1つ の後に、これは 設計による です。この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
23
Nick Chammas

主題全体については、Erland Sommarskogによるthedefinitive article: "Arrays and List in SQL Server" で説明されています。選択するバージョンを選択してください。

要約、preSQL Server 2008の場合、TVPが残りのSQLPに勝る

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

とにかく、この記事を読んで、他の手法や考え方を確認してください。

編集:hugeリストの別の場所への回答が遅い: 配列パラメーターをストアドプロシージャに渡す

21
gbn

私はこのパーティーに遅れていることを知っていますが、過去にそのような問題があり、最大10万のビギント数を送信しなければならず、いくつかのベンチマークを行いました。結局、それらをイメージとしてバイナリ形式で送信することになりました。これは、100,000までの数で他のすべてよりも高速でした。

これが私の古い(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;
}
14
A-K

SOに言及するか、ここで答えるのは難しいのですが、これはほとんどプログラミングの質問です。しかし、すでに使用している解決策があるので...投稿しますそれ ;)

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

これにより、処理したい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
9
jcolebrand

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

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

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

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

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

5
Robert Miller

ようやくTableValuedParametersを実行する機会を得て、それらがうまく機能するので、現在のコードの一部のサンプルを使用して、それらの使用方法を示すロッタコード全体を貼り付けます(注:ADOを使用します)。 。ネット)

また注意:私はサービスのコードをいくつか書いており、他のクラスには事前定義されたコードビットがたくさんありますが、デバッグできるようにこれをコンソールアプリとして書いているので、これをすべてからリッピングしましたコンソールアプリ。私のコーディングスタイル(ハードコードされた接続文字列のような)は一種の「捨てるために構築する」ものだったので、失礼します_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>_を定義しているので、どのように使用できるかを確認できます。

5
jcolebrand

私は提案#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または使用しているデータアダプターを使用して、挿入をバッチ処理することもできます。

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

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

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

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

そこで述べられている手法以外にも、バルクコピーやバルクインサートについては、一般的なケースで説明する価値があると感じる場合があります。

2
bernd_k

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

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
Ankit Bhalala