Passando parâmetros de matriz para um procedimento armazenado


53

Eu tenho um processo que pega um monte de registros (milhares) e os opera, e quando terminar, preciso marcar um grande número deles como processados. Eu posso indicar isso com uma grande lista de IDs. Estou tentando evitar o padrão "atualizações em loop", então gostaria de encontrar uma maneira mais eficiente de enviar esse pacote de IDs para um processo armazenado no MS SQL Server 2008.

Proposta nº 1 - Parâmetros com valor de tabela. Posso definir um tipo de tabela com apenas um campo de ID e enviar uma tabela cheia de IDs para atualizar.

Proposta 2 - Parâmetro XML (varchar) com OPENXML () no corpo do processo.

Proposta nº 3 - análise de lista. Prefiro evitar isso, se possível, pois parece difícil e propenso a erros.

Alguma preferência entre essas ou alguma idéia que eu perdi?


Como você está recebendo a grande lista de IDs?
Larry Coleman

Estou puxando-os para baixo junto com dados de "carga útil" por meio de outro processo armazenado. Mas não preciso atualizar todos esses dados - basta atualizar um sinalizador em determinados registros.
D. Lambert

Respostas:


42

Os melhores artigos de sempre sobre esse assunto são de Erland Sommarskog:

Ele cobre todas as opções e explica muito bem.

Desculpe a falta de resposta, mas o artigo de Erland sobre Arrays é como os livros de Joe Celko sobre árvores e outras guloseimas do SQL :)


23

Há uma grande discussão sobre isso no StackOverflow, que abrange muitas abordagens. O que eu prefiro para o SQL Server 2008+ é usar parâmetros com valor de tabela . Essa é essencialmente a solução do SQL Server para o seu problema - passando uma lista de valores para um procedimento armazenado.

As vantagens dessa abordagem são:

  • faça uma chamada de procedimento armazenado com todos os seus dados passados ​​como 1 parâmetro
  • entrada da tabela é estruturada e fortemente tipada
  • nenhuma construção / análise ou manipulação de string de XML
  • pode usar facilmente a entrada da tabela para filtrar, ingressar ou qualquer outra coisa

No entanto, observe: Se você chamar um procedimento armazenado que usa TVPs via ADO.NET ou ODBC e examinar a atividade com o SQL Server Profiler, notará que o SQL Server recebe várias INSERTinstruções para carregar o TVP, uma para cada linha no TVP , seguido pela chamada para o procedimento. Isso ocorre por design . Esse lote de INSERTs precisa ser compilado toda vez que o procedimento é chamado e constitui uma pequena sobrecarga. No entanto, mesmo com essa sobrecarga, os TVPs ainda descartam outras abordagens em termos de desempenho e usabilidade para a maioria dos casos de uso.

Se você quiser saber mais, Erland Sommarskog tem o skinny cheio em como parâmetros com valor de tabela trabalhar e fornece vários exemplos.

Aqui está outro exemplo que inventei:

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

Quando eu executo isso, recebo um erro: Msg 2715, Nível 16, Estado 3, Procedimento tvp_test, Linha 4 [Linha 4 do Lote Inicial] Coluna, parâmetro ou variável # 2: Não é possível encontrar o tipo de dados id_list. O parâmetro ou variável '@customer_list' possui um tipo de dados inválido. Msg 1087, Nível 16, Estado 1, Procedimento tvp_test, Linha 13 [Linha 4 do Início do Lote] Deve declarar a variável da tabela "@customer_list".
Damian

@ Damian - A CREATE TYPEdeclaração no início foi executada com sucesso? Qual versão do SQL Server você está executando?
Nick Chammas

No código SP, você tem esta frase em linha `SELECT @ param1 AS param1; ' . Qual é o propósito? Você não usa ou param1; por que você colocou isso como um parâmetro no cabeçalho do SP?
EAmez 18/01

@EAmez - Foi apenas um exemplo arbitrário. A questão @customer_listnão é @param1. O exemplo simplesmente demonstra que você pode misturar diferentes tipos de parâmetros.
Nick Chammas 19/01

21

Todo o assunto é discutido no artigo definitiva por Erland Sommarskog: "Matrizes e lista em SQL Server" . Escolha qual versão escolher.

Resumo, para pré SQL Server 2008 onde TVPs trunfo o resto

  • CSV, divida como quiser (geralmente uso uma tabela do Numbers)
  • XML e análise (melhor com o SQL Server 2005+)
  • Crie uma tabela temporária no cliente

De qualquer maneira, vale a pena ler o artigo para ver outras técnicas e pensamentos.

Edit: resposta tardia para grandes listas em outros lugares: Passando parâmetros de matriz para um procedimento armazenado


14

Sei que estou atrasado para esta festa, mas tive um problema no passado, tendo que enviar até 100 mil números grandes e fiz alguns benchmarks. Acabamos enviando-os em formato binário, como uma imagem - que era mais rápida do que tudo o resto para números de até 100K.

Aqui está o meu código antigo (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

O código a seguir está compactando números inteiros em um blob binário. Estou revertendo a ordem dos bytes aqui:

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

Estou dividido entre referir você a SO ou responder aqui, porque essa é quase uma questão de programação. Mas como já tenho uma solução que uso ... vou postar isso;)

A maneira como isso funciona é que você alimenta uma string delimitada por vírgula (divisão simples, não faz divisões no estilo CSV) no procedimento armazenado como um varchar (4000) e, em seguida, alimenta essa lista nessa função e obtém uma tabela útil novamente, uma tabela de apenas varchars.

Isso permite que você envie os valores apenas dos IDs que deseja processar, e você pode fazer uma associação simples nesse ponto.

Como alternativa, você pode fazer algo com uma CLT DataTable e alimentá-lo, mas isso é um pouco mais caro para dar suporte e todo mundo entende as listas 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

Bem, eu estava especificamente tentando evitar a lista delimitada por vírgulas para não precisar escrever algo assim, mas como já está escrito, acho que precisaria lançar essa solução novamente. ;-)
D. Lambert

11
Eu digo que tentar e verdadeiro é mais fácil. Você pode cuspir uma lista separada por vírgula em C # em segundos de código, e pode jogá-la nessa função (depois de inseri-la no seu sproc) com rapidez suficiente, e nem precisa pensar nisso. ~ E eu sei que você disse que não queria usar uma função, mas eu acho que é a maneira mais simples (talvez não o mais eficaz)
jcolebrand

5

Eu recebo regularmente conjuntos de milhares de linhas e 10000 linhas enviadas do nosso aplicativo para serem processadas por vários procedimentos armazenados do SQL Server.

Para atender às demandas de desempenho, usamos TVPs, mas você deve implementar seu próprio resumo do dbDataReader para superar alguns problemas de desempenho em seu modo padrão de processamento. Não vou entrar nos comos e nos porquês, pois estão fora do escopo desta solicitação.

Não considerei o processamento XML, pois não encontrei uma implementação XML com desempenho superior a 10.000 "linhas".

O processamento de lista pode ser tratado pelo processamento de tabela de contagem unidimensional e dupla dimensão (números). Nós os usamos com sucesso em várias áreas, mas os TVPs bem gerenciados têm melhor desempenho quando existem mais de algumas centenas de "linhas".

Como em todas as opções relacionadas ao processamento do SQL Server, você deve fazer sua seleção com base no modelo de uso.


5

Finalmente tive a chance de fazer alguns TableValuedParameters e eles funcionam muito bem, então vou colar um código inteiro que mostra como os estou usando, com uma amostra de alguns dos meus códigos atuais: (note: we use ADO .INTERNET)

Observe também: estou escrevendo algum código para um serviço e tenho muitos bits de código predefinidos na outra classe, mas estou escrevendo isso como um aplicativo de console para que eu possa depurá-lo, então retirei tudo isso de o aplicativo do console. Desculpe meu estilo de codificação (como cadeias de conexão codificadas), pois era como "criar um para jogar fora". Eu queria mostrar como uso um List<customObject>e enviá-lo ao banco de dados facilmente como uma tabela, que posso usar no procedimento armazenado. Código C # e TSQL abaixo:

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

Além disso, aceitarei críticas construtivas sobre o meu estilo de codificação, se você tiver isso a oferecer (a todos os leitores que se depararem com essa pergunta), mas mantenha-a construtiva;) ... Se você realmente me quiser, encontre-me na sala de bate-papo aqui . Felizmente, com esse pedaço de código, é possível ver como eles podem usar o List<Current>que eu defini como uma tabela no banco de dados e um List<T>no aplicativo.


3

Eu aceitaria a proposta nº 1 ou, como alternativa, criaria uma tabela de rascunho que contém apenas os IDs processados. Insira essa tabela durante o processamento e, depois de concluído, chame um processo semelhante ao abaixo:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

Você fará muitas inserções, mas elas serão para uma mesa pequena, portanto deve ser rápido. Você também pode colocar em lote suas inserções usando o ADO.net ou qualquer adaptador de dados que esteja usando.


2

O título da pergunta inclui a tarefa de transmitir dados de um aplicativo para o procedimento armazenado. Essa parte é excluída pelo corpo da pergunta, mas deixe-me tentar responder a isso também.

No contexto do sql-server-2008, conforme especificado pelas tags, há outro ótimo artigo de E. Sommarskog Arrays and Lists no SQL Server 2008 . Aliás, encontrei no artigo que Marian mencionou em sua resposta.

Em vez de apenas fornecer o link, cito sua lista de conteúdo:

  • Introdução
  • fundo
  • Parâmetros com valor de tabela no T-SQL
  • Passando parâmetros com valor de tabela do ADO .NET
    • Usando uma lista
    • Usando uma DataTable
    • Usando um DataReader
    • Considerações finais
  • Usando parâmetros com valor de tabela de outras APIs
    • ODBC
    • OLE DB
    • ADO
    • LINQ e estrutura de entidade
    • JDBC
    • PHP
    • Perl
    • E se a sua API não suportar TVPs
  • Considerações de desempenho
    • Lado do servidor
    • Lado do cliente
    • Chave primária ou não?
  • Agradecimentos e Feedback
  • Histórico de Revisão

Além das técnicas mencionadas, tenho a sensação de que, em alguns casos, a cópia em massa e a inserção em massa merecem ser mencionadas no escopo do caso geral.


1

Passando parâmetros de matriz para um procedimento armazenado

Para a versão mais recente do MS SQL 2016

Com o MS SQL 2016, eles introduzem uma nova função: SPLIT_STRING () para analisar vários valores.

Isso pode resolver seu problema facilmente.

Para versão anterior do MS SQL

Se você estiver usando uma versão mais antiga, siga esta etapa:

Primeiro faça uma função:

 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

Depois de fazer isso, basta passar sua string para esta função com separador.

Espero que isso seja útil para você. :-)


-1

Use isso para criar "criar tabela de tipos". exemplo simples para usuário

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.