저장 프로 시저에 배열 매개 변수 전달


53

많은 레코드 (1000)를 가져 와서 처리하는 프로세스가 있으며 완료되면 많은 수의 레코드를 처리 된 것으로 표시해야합니다. 큰 ID 목록으로 이것을 나타낼 수 있습니다. "루프 업데이트"패턴을 피하려고 노력하고 있으므로이 ID 백을 MS SQL Server 2008 저장 프로 시저로 보내는보다 효율적인 방법을 찾고 싶습니다.

제안 # 1-테이블 값 매개 변수. ID 필드만으로 테이블 유형을 정의하고 ID로 가득 찬 테이블을 보내 업데이트 할 수 있습니다.

제안서 # 2-proc 본문에 OPENXML ()이있는 XML 매개 변수 (varchar).

제안서 # 3-목록 구문 분석. 다루기 어려우며 오류가 발생하기 쉬우므로 가능하면 피하는 것이 좋습니다.

이 중 선호하거나 내가 놓친 아이디어가 있습니까?


큰 ID 목록을 어떻게 얻습니까?
Larry Coleman

다른 저장 프로 시저를 통해 "페이로드"데이터와 함께 가져옵니다. 그래도 모든 데이터를 업데이트 할 필요는 없습니다. 특정 레코드의 플래그 만 업데이트하면됩니다.
D. Lambert

답변:


42

이 문제에 관한 최고의 기사는 Erland Sommarskog의 글입니다.

그는 모든 옵션을 다루고 꽤 잘 설명합니다.

답이 부족하여 죄송하지만 Arrays에 대한 Erland의 기사는 나무와 다른 SQL 취급에 관한 Joe Celko의 책과 같습니다. :)


23

StackOverflow 에 대한 많은 논의가 있습니다. SQL Server 2008+에서 선호하는 것은 테이블 반환 매개 변수사용하는 것 입니다. 이는 기본적으로 값 목록을 저장 프로 시저에 전달하여 문제에 대한 SQL Server의 솔루션입니다.

이 방법의 장점은 다음과 같습니다.

  • 하나의 매개 변수로 전달 된 모든 데이터로 하나의 저장 프로 시저 호출
  • 테이블 입력이 구조화되고 강력하게 형식화 됨
  • XML의 문자열 작성 / 구문 분석 또는 처리가 필요 없음
  • 테이블 입력을 사용하여 쉽게 필터링, 조인 또는 기타

그러나 주를 가지고 : 당신은 ADO.NET 또는 ODBC 및 SQL Server 프로파일 러와 활동을 살펴 통해 TVPs를 사용하는 저장 프로 시저를 호출하는 경우, 당신은 SQL Server가 여러 수신 알 수 INSERTTVP,로드 문 각 행에 대해 하나를 TVP 에서 프로 시저를 호출합니다. 이것은 의도적으로 설계된 동작 입니다. 이 배치는 INSERT프로 시저가 호출 될 때마다 컴파일되어야하며 작은 오버 헤드를 구성합니다. 그러나 이러한 오버 헤드에도 불구하고 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

@Damian- CREATE TYPE처음에 명령문이 성공적으로 실행 되었습니까? 어떤 버전의 SQL Server를 실행하고 있습니까?
Nick Chammas

SP 코드에는 다음 문장이 있습니다.`SELECT @ param1 AS param1; ' . 목적은 무엇입니까? 당신은 param1을 사용하지 않으므로 왜 이것을 SP 헤더에 매개 변수로 넣었습니까?
EAmez

@EAmez-임의의 예일뿐입니다. 요점은 @customer_list아닙니다 @param1. 이 예는 단순히 다른 매개 변수 유형을 혼합 할 수 있음을 보여줍니다.
Nick Chammas

21

전체 주제는 Erland Sommarskog 최종 기사 : "SQL Server의 배열 및 목록"에서 설명 합니다. 선택할 버전을 선택하십시오.

TVP가 나머지를 능가하는 SQL Server 2008 이전 버전 요약

  • CSV, 원하는 방식으로 나눕니다 (일반적으로 숫자 테이블을 사용합니다)
  • XML 및 구문 분석 (SQL Server 2005 이상)
  • 클라이언트에서 임시 테이블을 만듭니다.

이 기사는 다른 기술과 사고를보기 위해 읽을 가치가 있습니다.

편집 : 다른 곳의 거대한 목록에 대한 늦은 대답 : 배열 매개 변수를 저장 프로 시저에 전달


14

나는이 파티에 늦었다는 것을 알고 있지만, 과거에는 100K까지 큰 숫자를 보내야하는 몇 가지 문제가 있었고 몇 가지 벤치 마크를 수행했습니다. 우리는 이미지로 바이너리 형식으로 전송하여 최대 100K 숫자의 다른 모든 것보다 빠릅니다.

다음은 이전 (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

다음 코드는 정수를 이진 블롭으로 패킹합니다. 바이트 순서를 반대로 바꾸고 있습니다.

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를 언급하거나 여기에 대답하는 것 사이에서 찢어졌습니다. 'cos 이것은 거의 프로그래밍 질문입니다. 그러나 이미 사용중인 솔루션을 가지고 있기 때문에 ... 나는 그것을 게시 할 것입니다.)

이 방법은 쉼표로 구분 된 문자열 (단순 분할, 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. Lambert

1
나는 노력했지만 진실이 가장 쉽다고 말합니다. 쉼표로 구분 된 목록을 C #으로 몇 초 만에 뱉을 수 있으며이 함수를 sproc에 넣은 후이 함수에 신속하게 던질 수 있으며 거의 ​​생각할 필요가 없습니다. ~ 그리고 나는 당신이 함수를 사용하고 싶지 않다고 말했지만, 그것이 가장 간단한 방법이라고 생각합니다. (가장 효과적이지 않을 수도 있습니다)
jcolebrand

5

다양한 SQL Server 저장 프로 시저에서 처리하기 위해 응용 프로그램에서 보낸 1000 개의 행과 10000 개의 행 집합을 정기적으로받습니다.

성능 요구를 충족시키기 위해 TVP를 사용하지만 기본 처리 모드에서 일부 성능 문제를 극복하려면 자체 dbDataReader의 추상을 구현해야합니다. 이 요청의 범위를 벗어난 방법과 이유에 대해서는 다루지 않겠습니다.

10,000 개가 넘는 "행"으로 성능이 유지되는 XML 구현을 찾지 못했기 때문에 XML 처리를 고려하지 않았습니다.

리스트 처리는 1 차원 및 2 차원 탈리 (숫자) 테이블 처리로 처리 할 수 ​​있습니다. 우리는 다양한 영역에서 이러한 기술을 성공적으로 사용했지만 잘 관리 된 TVP는 수백 개가 넘는 "행"이있을 때 성능이 우수합니다.

SQL Server 처리와 관련된 모든 선택과 마찬가지로 사용 모델을 기반으로 선택해야합니다.


5

마침내 TableValuedParameters를 수행 할 수있는 기회가 있었고 훌륭하게 작동하므로 현재 코드 중 일부의 샘플과 함께 사용 방법을 보여주는 전체 로타 코드를 붙여 넣을 것입니다. (참고 : 우리는 ADO를 사용합니다. .그물)

또한 참고 : 서비스 용 코드를 작성 중이며 다른 클래스에 미리 정의 된 코드 비트가 많이 있지만 디버깅 할 수 있도록 콘솔 응용 프로그램으로 작성하고 있습니다. 콘솔 앱. "하드 코딩 된 연결 문자열과 같은"코딩 스타일을 "버려 둘 빌드 하나"로 실례합니다. List<customObject>저장 프로 시저에서 사용할 수 있도록 a를 사용하고 테이블로 쉽게 데이터베이스에 푸시 하는 방법을 보여주고 싶었습니다 . 아래 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 및 a 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에 의한 또 다른 훌륭한 기사가 있습니다. BTW 나는 마리안이 그의 답변에서 언급 한 기사에서 그것을 발견했다.

링크를주는 대신 내용 목록을 인용하십시오.

  • 소개
  • 배경
  • T-SQL의 테이블 반환 매개 변수
  • ADO .NET에서 테이블 반환 매개 변수 전달
    • 리스트 사용
    • DataTable 사용
    • DataReader 사용
    • 최종 비고
  • 다른 API에서 테이블 반환 매개 변수 사용
    • ODBC
    • OLE DB
    • 야단법석
    • LINQ 및 엔터티 프레임 워크
    • JDBC
    • PHP
    • API가 TVP를 지원하지 않는 경우
  • 성능 고려 사항
    • 서버 측
    • 고객 입장에서
    • 기본 키?
  • 감사의 말 및 피드백
  • 개정 이력

거기에 언급 된 기술 외에도, 일부 경우에 벌크 카피 및 대량 삽입이 일반적인 경우와 관련하여 언급 될 가치가 있다고 생각합니다.


1

저장 프로 시저에 배열 매개 변수 전달

MS SQL 2016 최신 버전

MS SQL 2016에서는 SPLIT_STRING () 이라는 새로운 함수가 도입되어 여러 값을 구문 분석합니다.

이렇게하면 문제를 쉽게 해결할 수 있습니다.

MS SQL 이전 버전의 경우

이전 버전을 사용하는 경우 다음 단계를 수행하십시오.

먼저 하나의 기능을 만드십시오.

 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.