Já escrevi várias vezes sobre o uso de cursores e como, na maioria dos casos, é mais eficiente reescrever seus cursores usando lógica baseada em conjunto.
Mas sou realista.
Eu sei que há casos em que os cursores são "necessários" - você precisa chamar outro procedimento armazenado ou enviar um e-mail para cada linha, está fazendo tarefas de manutenção em cada banco de dados ou está executando uma tarefa única que simplesmente não vale a pena investir tempo para converter para set-based.
Como você está (provavelmente) fazendo isso hoje
Independentemente do motivo pelo qual você ainda está usando cursores, você deve pelo menos ter cuidado para não usar as opções padrão bastante caras. A maioria das pessoas inicia seus cursores assim:
DECLARE c CURSOR FOR SELECT whatever FROM ...
Agora, novamente, para tarefas pontuais e pontuais, isso provavelmente é bom. Mas há…
Outras maneiras de fazer isso
Eu queria executar alguns testes usando os padrões e compará-los com diferentes opções de cursor, como
LOCAL
, ESTÁTICO
, READ_ONLY
e FAST_FORWARD
. (Há uma tonelada de opções, mas essas são as mais comumente usadas, pois são aplicáveis aos tipos mais comuns de operações de cursor que as pessoas usam.) Eu não apenas queria testar a velocidade bruta de algumas combinações diferentes, mas também o impacto no tempdb e na memória, tanto após uma reinicialização do serviço frio quanto com um cache quente. A consulta que decidi alimentar para o cursor é uma consulta muito simples em
sys.objects
, no banco de dados de exemplo AdventureWorks2012. Isso retorna 318.500 linhas no meu sistema (um sistema de 2 núcleos muito humilde com 4 GB de RAM):SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2;
Em seguida, envolvi essa consulta em um cursor com várias opções (incluindo os padrões) e executei alguns testes, medindo a memória total do servidor, páginas alocadas para tempdb (de acordo com
sys.dm_db_task_space_usage
e/ou sys.dm_db_session_space_usage
) e duração total. Também tentei observar a contenção do tempdb usando scripts de Glenn Berry e Robert Davis, mas no meu sistema insignificante não consegui detectar nenhuma contenção. É claro que também estou no SSD e absolutamente nada mais está sendo executado no sistema, portanto, essas podem ser coisas que você deseja adicionar aos seus próprios testes se o tempdb for mais provável de ser um gargalo. Então, no final, as consultas ficaram mais ou menos assim, com consultas de diagnóstico salpicadas em pontos apropriados:
DECLARE @i INT = 1; DECLARE c CURSOR -- LOCAL -- LOCAL STATIC -- LOCAL FAST_FORWARD -- LOCAL STATIC READ_ONLY FORWARD_ONLY FOR SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2 ORDER BY c1.[object_id]; OPEN c; FETCH c INTO @i; WHILE (@@FETCH_STATUS = 0) BEGIN SET @i += 1; -- meaningless operation FETCH c INTO @i; END CLOSE c; DEALLOCATE c;
Resultados
Duração
Indiscutivelmente a medida mais importante e comum é "quanto tempo demorou?" Bem, levou quase cinco vezes mais tempo para executar um cursor com as opções padrão (ou apenas com
LOCAL
especificado), em comparação com a especificação de STATIC
ou FAST_FORWARD
:Memória
Eu também queria medir a memória adicional que o SQL Server solicitaria ao preencher cada tipo de cursor. Então, eu simplesmente reiniciei antes de cada teste de cache frio, medindo o contador de desempenho
Total Server Memory (KB)
antes e depois de cada teste. A melhor combinação aqui foi LOCAL FAST_FORWARD
:uso do tempdb
Esse resultado foi surpreendente para mim. Como a definição de um cursor estático significa que ele copia todo o resultado para tempdb e, na verdade, é expresso em
sys.dm_exec_cursors
como INSTANTÂNEO
, eu esperava que o acerto nas páginas tempdb fosse maior com todas as variantes estáticas do cursor. Este não era o caso; novamente, vemos um acerto de aproximadamente 5X no uso do tempdb com o cursor padrão e aquele com apenas LOCAL
Especificadas:Conclusão
Há anos venho enfatizando que a seguinte opção deve sempre ser especificada para seus cursores:
LOCAL STATIC READ_ONLY FORWARD_ONLY
A partir deste ponto, até que eu tenha a chance de testar outras permutações ou encontrar casos em que não seja a opção mais rápida, recomendarei o seguinte:
LOCAL FAST_FORWARD
(Como um aparte, também fiz testes omitindo o
LOCAL
opção, e as diferenças foram insignificantes.) Dito isso, isso não é necessariamente verdade para *todos* os cursores. Nesse caso, estou falando apenas de cursores em que você está apenas lendo dados do cursor, apenas para frente, e não está atualizando os dados subjacentes (pela chave ou usando
WHERE CURRENT OF ). Esses são testes para outro dia.