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

Cursores

Como processar um result set linha por linha em T-SQL — e por que, na grande maioria dos casos, você deveria evitar isso.

1. SQL pensa em conjuntos, não em linhas

Tudo que você viu até aqui — SELECT, UPDATE, JOIN — opera sobre conjuntos de linhas de uma vez. Isso é o que o motor do SQL Server foi construído para otimizar. Um cursor quebra esse modelo: ele permite iterar linha por linha, de forma parecida com um foreach em C#. É poderoso, mas tipicamente 10-100x mais lento que a operação em conjunto equivalente.

Regra geral

Se existe uma forma de resolver o problema com UPDATE, JOIN ou uma função de janela (OVER), use essa forma. Cursor é para os casos em que a lógica realmente depende do valor de uma linha anterior de um jeito que não dá para expressar em um único comando de conjunto — por exemplo, um cálculo sequencial acumulado com regras condicionais complexas.

2. Anatomia de um cursor

Um cursor tem cinco passos, sempre na mesma ordem:

DECLARE OPEN FETCH WHILE @@FETCH_STATUS CLOSE DEALLOCATE

Fig. 1 — Os cinco passos de todo cursor, sempre nesta ordem.

SQL estrutura completa de um cursor
DECLARE @ClienteId INT, @Nome NVARCHAR(100), @Saldo DECIMAL(10,2);

DECLARE cursor_clientes CURSOR LOCAL FAST_FORWARD FOR
    SELECT c.Id, c.Nome, SUM(p.Valor)
    FROM dbo.Cliente c
    INNER JOIN dbo.Pedido p ON p.ClienteId = c.Id
    GROUP BY c.Id, c.Nome;

OPEN cursor_clientes;
FETCH NEXT FROM cursor_clientes INTO @ClienteId, @Nome, @Saldo;

WHILE @@FETCH_STATUS = 0
BEGIN
    -- lógica linha a linha (exemplo: aplicar um bônus escalonado)
    IF @Saldo > 10000
        UPDATE dbo.Cliente SET NivelFidelidade = 'Ouro' WHERE Id = @ClienteId;
    ELSE IF @Saldo > 1000
        UPDATE dbo.Cliente SET NivelFidelidade = 'Prata' WHERE Id = @ClienteId;

    FETCH NEXT FROM cursor_clientes INTO @ClienteId, @Nome, @Saldo;
END

CLOSE cursor_clientes;
DEALLOCATE cursor_clientes;
PassoO que faz
DECLARE ... CURSORDefine a query que o cursor vai percorrer
OPENExecuta a query e posiciona antes da primeira linha
FETCH NEXTAvança uma linha, copiando as colunas para variáveis
@@FETCH_STATUS0 enquanto há linha; diferente de 0 quando acabou
CLOSE / DEALLOCATELibera o result set e depois a estrutura do cursor — sempre os dois, nessa ordem
Dica

LOCAL FAST_FORWARD é a combinação de opções mais performática para o caso comum (só andar para frente, sem atualizar linhas pelo próprio cursor). Sem isso, o SQL Server usa opções mais genéricas e mais lentas por padrão.

3. O mesmo problema, sem cursor

O exemplo acima (classificar clientes por nível de fidelidade) na verdade não precisa de cursor — é um caso clássico de operação em conjunto:

SQL a mesma lógica em um único UPDATE
;WITH SaldoPorCliente AS
(
    SELECT ClienteId, SUM(Valor) AS Saldo
    FROM dbo.Pedido
    GROUP BY ClienteId
)
UPDATE c
SET NivelFidelidade = CASE
        WHEN s.Saldo > 10000 THEN 'Ouro'
        WHEN s.Saldo > 1000  THEN 'Prata'
        ELSE c.NivelFidelidade
    END
FROM dbo.Cliente c
INNER JOIN SaldoPorCliente s ON s.ClienteId = c.Id;

Uma CTE (WITH ... AS) calcula o saldo de todos os clientes de uma vez, e um único UPDATE com CASE aplica a regra a todas as linhas simultaneamente — sem loop, sem @@FETCH_STATUS, e tipicamente ordens de magnitude mais rápido em tabelas grandes.

4. Quando um cursor é mesmo a melhor opção

Casos legítimos são raros, mas existem: chamar uma procedure externa (inclusive de outro sistema) para cada linha, gerar arquivos individuais, ou aplicar uma regra de negócio verdadeiramente sequencial (onde o resultado da linha N depende do resultado já processado da linha N-1 de um jeito que não é uma soma acumulada simples).

5. Onde isso te leva

Agora você sabe escrever um cursor e, mais importante, reconhecer quando não precisa de um. O próximo capítulo muda de assunto: como construir queries dinamicamente dentro do T-SQL, e os cuidados de segurança que isso exige.

📌 Resumo do capítulo

  • Cursor processa linha por linha; SQL Server é otimizado para operações em conjunto — cursor costuma ser bem mais lento.
  • Cinco passos sempre na mesma ordem: DECLAREOPENFETCH em loop com @@FETCH_STATUSCLOSEDEALLOCATE.
  • LOCAL FAST_FORWARD é a opção mais performática para o caso comum.
  • Antes de escrever um cursor, pergunte se uma CTE + UPDATE/JOIN resolve o mesmo problema em conjunto.

✏️ Praticando

  1. Implemente o exemplo de classificação de fidelidade com cursor, depois com a versão em conjunto (CTE + UPDATE), e compare o tempo de execução em uma tabela com alguns milhares de linhas de teste.
  2. Escreva um cursor que percorre dbo.Cliente e imprime (PRINT) o nome de cada cliente inativo.