Módulo 2 · SQL Server / Procedures — Capítulo 08

Table-Valued Parameters

Como mandar uma lista inteira — não só um valor — como parâmetro de uma procedure, em uma única chamada.

1. O problema: inserir vários registros de uma vez

Até agora, toda procedure de escrita processa uma linha por chamada. Mas e se a aplicação C# tem uma lista de 50 itens de pedido para inserir de uma vez? A solução ingênua — chamar a procedure 50 vezes num loop — funciona, mas gera 50 viagens de rede (o mesmo problema do capítulo 02, fig. 1). Table-Valued Parameters (TVPs) resolvem isso: você manda a lista inteira como um único parâmetro.

2. Criando um tipo de tabela

TVPs exigem um passo a mais: definir a "forma" da tabela como um tipo nomeado, com CREATE TYPE, antes de usá-la como parâmetro.

SQL definindo o tipo
CREATE TYPE dbo.ItemPedidoTableType AS TABLE
(
    ProdutoId  INT NOT NULL,
    Quantidade INT NOT NULL,
    PrecoUnit  DECIMAL(10,2) NOT NULL
);
GO

3. Usando o tipo como parâmetro

SQL procedure que recebe uma lista inteira
CREATE PROCEDURE dbo.usp_PedidoInserirComItens
    @ClienteId INT,
    @Itens     dbo.ItemPedidoTableType READONLY
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION;

        DECLARE @PedidoId INT;

        INSERT INTO dbo.Pedido (ClienteId, DataPedido)
        VALUES (@ClienteId, SYSDATETIME());

        SET @PedidoId = SCOPE_IDENTITY();

        INSERT INTO dbo.PedidoItem (PedidoId, ProdutoId, Quantidade, PrecoUnit)
        SELECT @PedidoId, ProdutoId, Quantidade, PrecoUnit
        FROM @Itens;

        COMMIT TRANSACTION;

        SELECT @PedidoId AS PedidoId;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
        THROW;
    END CATCH
END;
GO
READONLY é obrigatório

Todo TVP precisa do modificador READONLY — você pode ler o conteúdo (SELECT ... FROM @Itens), mas não pode fazer INSERT/UPDATE/DELETE diretamente nele dentro da procedure.

4. Testando direto no T-SQL

SQL montando a tabela e chamando a procedure
DECLARE @MeusItens dbo.ItemPedidoTableType;

INSERT INTO @MeusItens (ProdutoId, Quantidade, PrecoUnit)
VALUES (1, 2, 49.90),
       (5, 1, 199.00),
       (7, 3, 15.50);

EXEC dbo.usp_PedidoInserirComItens
    @ClienteId = 42,
    @Itens     = @MeusItens;

5. Enviando de C# com ADO.NET

Do lado do C#, um TVP é passado como uma DataTable (ou uma lista de SqlDataRecord), atribuída a um SqlParameter com SqlDbType.Structured:

C# montando e enviando o TVP
var tabela = new DataTable();
tabela.Columns.Add("ProdutoId", typeof(int));
tabela.Columns.Add("Quantidade", typeof(int));
tabela.Columns.Add("PrecoUnit", typeof(decimal));

tabela.Rows.Add(1, 2, 49.90m);
tabela.Rows.Add(5, 1, 199.00m);
tabela.Rows.Add(7, 3, 15.50m);

using var comando = new SqlCommand("dbo.usp_PedidoInserirComItens", conexao);
comando.CommandType = CommandType.StoredProcedure;
comando.Parameters.AddWithValue("@ClienteId", 42);

var parametroItens = comando.Parameters.AddWithValue("@Itens", tabela);
parametroItens.SqlDbType = SqlDbType.Structured;
parametroItens.TypeName  = "dbo.ItemPedidoTableType";

conexao.Open();
var pedidoId = (int)comando.ExecuteScalar();
Comparando com o que você já sabe

Pense num TVP como o equivalente T-SQL de passar uma List<ItemPedido> inteira como argumento de um método C# — só que atravessando a fronteira rede/banco em uma única chamada, em vez de serializar item por item.

6. Onde isso te leva

Com TVPs você processa listas em lote sem sacrificar a estrutura de uma procedure única. O próximo capítulo trata do outro extremo — quando você realmente precisa processar linha por linha dentro do T-SQL — e por que isso deveria ser sua última opção.

📌 Resumo do capítulo

  • Table-Valued Parameters permitem mandar uma lista inteira como um único parâmetro de procedure.
  • Requer CREATE TYPE ... AS TABLE antes de usar, e o parâmetro precisa ser READONLY.
  • Do lado do C#, envia-se como DataTable com SqlDbType.Structured e TypeName apontando para o tipo criado no banco.
  • Evita N chamadas de rede para inserir N linhas — tudo processado numa única viagem.

✏️ Praticando

  1. Crie a tabela PedidoItem (PedidoId, ProdutoId, Quantidade, PrecoUnit) e o tipo ItemPedidoTableType.
  2. Implemente usp_PedidoInserirComItens e teste via T-SQL com pelo menos 3 itens.
  3. Escreva o código C# (como no exemplo da seção 5) que monta a DataTable e chama a procedure.