Como passar uma matriz para um procedimento armazenado do SQL Server

293

Como passar uma matriz para um procedimento armazenado do SQL Server?

Por exemplo, eu tenho uma lista de funcionários. Eu quero usar esta lista como uma tabela e juntá-la a outra tabela. Mas a lista de funcionários deve ser passada como parâmetro em C #.

Sergey
fonte
senhor espero que esta ligação irá ajudá-lo Passando uma lista / matriz para SQL Server SP
patrick choi
É a mesma classe que Parameterize uma cláusula SQL IN
Lukasz Szozda 15/03

Respostas:

437

SQL Server 2008 (ou mais recente)

Primeiro, no seu banco de dados, crie os dois objetos a seguir:

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

Agora no seu código 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

Se você estiver usando o SQL Server 2005, eu ainda recomendaria uma função de divisão sobre XML. Primeiro, crie uma função:

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

Agora seu procedimento armazenado pode ser apenas:

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

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

E no seu código C # você só precisa passar a lista como '1,2,3,12'...


Acho que o método de passar por parâmetros com valor de tabela simplifica a capacidade de manutenção de uma solução que a utiliza e, muitas vezes, aumenta o desempenho em comparação com outras implementações, incluindo XML e divisão de cadeias.

As entradas são claramente definidas (ninguém precisa adivinhar se o delimitador é uma vírgula ou ponto-e-vírgula) e não temos dependências de outras funções de processamento que não são óbvias sem inspecionar o código para o procedimento armazenado.

Comparado às soluções que envolvem o esquema XML definido pelo usuário em vez de UDTs, isso envolve um número semelhante de etapas, mas, na minha experiência, o código é muito mais simples de gerenciar, manter e ler.

Em muitas soluções, você pode precisar apenas de um ou alguns desses UDTs (tipos definidos pelo usuário) que você reutiliza para muitos procedimentos armazenados. Como neste exemplo, o requisito comum é passar por uma lista de ponteiros de ID, o nome da função descreve qual contexto esses IDs devem representar e o nome do tipo deve ser genérico.

Aaron Bertrand
fonte
3
Gosto da ideia dos parâmetros da tabela - nunca pensei nisso - felicidades. Para o que vale a pena o delimitador precisa passar para a chamada de função SplitInts ().
Drammy
Como posso usar o parâmetro de tabela, se só tem acesso a uma vírgula separados corda
bdwain
O @bdwain derrotaria o objetivo - você teria que usar uma função dividida para dividi-la em linhas e inseri-la no TVP. Divida-o no código do seu aplicativo.
Aaron Bertrand
1
@AaronBertrand obrigado pela resposta, eu realmente descobri. Eu tenho que usar um sub selecionar entre parênteses: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz
3
@ th1rdey3 Eles são implicitamente opcionais. stackoverflow.com/a/18926590/61305
Aaron Bertrand
44

Com base na minha experiência, ao criar uma expressão delimitada a partir dos IDs dos funcionários, há uma solução complicada e agradável para esse problema. Você só deve criar uma expressão de cadeia como ';123;434;365;'em que 123, 434e 365algumas employeeIDs. Chamando o procedimento abaixo e passando essa expressão para ele, você pode buscar os registros desejados. Você pode facilmente associar a "outra tabela" a esta consulta. Esta solução é adequada em todas as versões do SQL server. Além disso, em comparação com o uso de variável de tabela ou tabela temporária, é uma solução muito mais rápida e otimizada.

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
Hamed Nazaktabar
fonte
Agradável! Eu realmente gosto dessa abordagem em que estou filtrando as chaves int! 1
MDV2000
@ MDV2000 thanks :) Nas chaves de string, isso também tem um bom desempenho por não converter colunas int para varchar ...
Hamed Nazaktabar
Estou atrasado para o jogo, mas isso é muito inteligente! Funciona muito bem para o meu problema.
user441058
Isso é incrível! Definitivamente vou usar isso, obrigado
Omar Ruder
26

Use um parâmetro com valor de tabela para seu procedimento armazenado.

Quando você o passa do C #, adiciona o parâmetro ao tipo de dados SqlDb.Structured.

Consulte aqui: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Exemplo:

// 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();
}
Levi W
fonte
17

Você precisa passá-lo como um parâmetro XML.

Edit: código rápido do meu projeto para ter uma idéia:

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)

Então você pode usar a tabela temporária para se juntar às suas tabelas. Definimos arrayOfUlong como um esquema XML interno para manter a integridade dos dados, mas você não precisa fazer isso. Eu recomendo usá-lo, então aqui está um código rápido para garantir que você sempre obtenha um XML com longs.

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
Fedor Hajdu
fonte
Eu pensei que era uma má idéia usar variáveis ​​de tabela quando há muitas linhas? Não é melhor desempenho usar uma tabela temporária (#table)?
Ganders
@ Ganders: eu diria vice-versa.
abatishchev
14

O contexto é sempre importante, como o tamanho e a complexidade da matriz. Para listas pequenas e médias, várias das respostas postadas aqui são excelentes, embora alguns esclarecimentos devam ser feitos:

  • Para dividir uma lista delimitada, um divisor baseado em SQLCLR é o mais rápido. Existem inúmeros exemplos, se você quiser escrever o seu próprio, ou você pode simplesmente fazer o download da biblioteca SQL # gratuita de funções CLR (que eu escrevi, mas a função String_Split e muitas outras são totalmente gratuitas).
  • A divisão de matrizes baseadas em XML pode ser rápida, mas você precisa usar XML baseado em atributo, não XML baseado em elemento (que é o único tipo mostrado nas respostas aqui, embora o exemplo XML de @ AaronBertrand seja o melhor, pois seu código está usando o text()Função XML Para obter mais informações (análise de desempenho) sobre o uso de XML para dividir listas, consulte "Usando XML para passar listas como parâmetros no SQL Server", de Phil Factor.
  • O uso de TVPs é ótimo (supondo que você esteja usando pelo menos o SQL Server 2008 ou mais recente), pois os dados são transmitidos para o proc e são exibidos pré-analisados ​​e fortemente tipados como uma variável de tabela. No entanto, na maioria dos casos, armazenar todos os dados DataTablesignifica duplicar os dados na memória à medida que são copiados da coleção original. Portanto, o uso do DataTablemétodo de transmissão em TVPs não funciona bem para conjuntos maiores de dados (ou seja, não é bem dimensionado).
  • O XML, diferentemente das simples listas delimitadas de Ints ou Strings, pode lidar com mais de matrizes unidimensionais, assim como TVPs. Mas, assim como o DataTablemétodo TVP, o XML não é escalável, pois mais do que duplica o tamanho dos dados na memória, pois precisa levar em conta adicionalmente a sobrecarga do documento XML.

Com tudo isso dito, se os dados que você está usando são grandes ou ainda não muito grandes, mas crescem consistentemente, o IEnumerablemétodo TVP é a melhor opção, pois transmite os dados para o SQL Server (como o DataTablemétodo), MAS NÃO requer qualquer duplicação da coleção na memória (diferente de qualquer outro método). Publiquei um exemplo do código SQL e C # nesta resposta:

Passar dicionário para procedimento armazenado T-SQL

Solomon Rutzky
fonte
6

Não há suporte para a matriz no servidor sql, mas existem várias maneiras pelas quais você pode passar a coleção para um processo armazenado.

  1. Usando dados
  2. Usando XML.Tente converter sua coleção em um formato xml e depois passá-la como uma entrada para um procedimento armazenado

O link abaixo pode ajudá-lo

passando a coleção para um procedimento armazenado

praveen
fonte
5

Eu estive pesquisando todos os exemplos e respostas de como passar qualquer array para o servidor sql sem o incômodo de criar um novo tipo de tabela, até encontrar esse link , abaixo é como o apliquei ao meu projeto:

--O código a seguir obtém uma matriz como parâmetro e insere os valores dessa matriz em outra tabela

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 

Espero que goste

Adão
fonte
2
@zaitsman: LIMPO não significa melhor ou mais apropriado. Geralmente, renuncia-se à flexibilidade e / ou complexidade e / ou desempenho "apropriados" para obter código "limpo". Esta resposta aqui é "ok", mas apenas para pequenos conjuntos de dados. Se a matriz de entrada @sfor CSV, seria mais rápido simplesmente dividir isso (por exemplo, INSERIR EM ... SELECIONAR NA FunçãoFunção). A conversão para XML é mais lenta que o CLR e o XML baseado em atributos é muito mais rápido. E esta é uma lista simples, mas a transmissão em XML ou TVP também pode lidar com matrizes complexas. Não sabe ao certo o que é ganho, evitando uma tarefa simples e única CREATE TYPE ... AS TABLE.
Solomon Rutzky
5

Isso irá ajudá-lo. :) Siga os próximos passos,

  1. Abra o Query Designer
  2. Copiar Cole o seguinte código como ele é, ele criará a Função que converte a String em 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. Crie o seguinte procedimento armazenado

     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. Executar este SP. Usando exec sp_DeleteId '1,2,3,12'isso, é uma sequência de IDs que você deseja excluir,

  5. Você converte sua matriz em string em C # e a passa como um parâmetro de procedimento armazenado

    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 ;

Isso excluirá várias linhas. Tudo de bom

Charan Ghate
fonte
Eu usei essa função de análise separada por vírgula, ela funcionaria para um conjunto de dados pequeno; se você verificar seu plano de execução, causará um problema no conjunto de dados grande e onde você precisará da lista csv múltipla no procedimento armazenado
Saboor Awan
2

Levei muito tempo para descobrir isso, por isso, caso alguém precise ...

Isso é baseado no método SQL 2005 na resposta de Aaron e usando sua função SplitInts (acabei de remover o parâmetro delim, pois sempre usarei vírgulas). Estou usando o SQL 2008, mas queria algo que funcionasse com conjuntos de dados digitados (XSD, TableAdapters) e sei que parâmetros de string funcionam com eles.

Eu estava tentando fazer com que a função dele funcionasse em uma cláusula do tipo "where in (1,2,3)" e não tendo sorte no caminho direto. Então, criei uma tabela temporária primeiro e depois fiz uma junção interna em vez de "where in". Aqui está o meu exemplo de uso: no meu caso, eu queria obter uma lista de receitas que não contêm certos ingredientes:

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)
eselk
fonte
De um modo geral, é melhor não ingressar em uma variável de tabela, mas em uma tabela temporária. As variáveis ​​da tabela, por padrão, parecem ter apenas uma linha, embora exista um truque ou dois (consulte o excelente e detalhado artigo de @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky
1

Como outros observaram acima, uma maneira de fazer isso é converter sua matriz em uma cadeia de caracteres e depois dividir a cadeia dentro do SQL Server.

No SQL Server 2016, existe uma maneira interna de dividir seqüências de caracteres chamada

STRING_SPLIT ()

Ele retorna um conjunto de linhas que você pode inserir na sua tabela temporária (ou tabela real).

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

produziria:

valor
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66.

Se você quiser ficar mais chique:

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

forneceria os mesmos resultados acima (exceto que o nome da coluna é "número depois"), mas classificado. Você pode usar a variável de tabela como qualquer outra tabela, para poder associá-la facilmente a outras tabelas no banco de dados, se desejar.

Observe que a instalação do SQL Server deve estar no nível de compatibilidade 130 ou superior para que a STRING_SPLIT()função seja reconhecida. Você pode verificar seu nível de compatibilidade com a seguinte consulta:

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

A maioria dos idiomas (incluindo C #) possui uma função "join" que você pode usar para criar uma string a partir de uma matriz.

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

Então você passa sqlparamcomo seu parâmetro para o procedimento armazenado acima.

Patrick Chu
fonte
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
Shabir Mustafa
fonte