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.
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:
Fig. 1 — Os cinco passos de todo cursor, sempre nesta ordem.
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;
| Passo | O que faz |
|---|---|
DECLARE ... CURSOR | Define a query que o cursor vai percorrer |
OPEN | Executa a query e posiciona antes da primeira linha |
FETCH NEXT | Avança uma linha, copiando as colunas para variáveis |
@@FETCH_STATUS | 0 enquanto há linha; diferente de 0 quando acabou |
CLOSE / DEALLOCATE | Libera o result set e depois a estrutura do cursor — sempre os dois, nessa ordem |
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:
;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.