Para algumas pessoas, é a pergunta errada. SQL CURSOR IS o erro. O diabo está nos detalhes! Você pode ler todo tipo de blasfêmia em toda a blogosfera SQL em nome de SQL CURSOR.
Se você se sente da mesma forma, o que fez você chegar a essa conclusão?
Se for de um amigo e colega de confiança, não posso culpá-lo. Acontece. Às vezes muito. Mas se alguém o convenceu com provas, isso é outra história.
Não nos conhecemos antes. Você não me conhece como amigo. Mas espero poder explicar com exemplos e convencê-lo de que o SQL CURSOR tem seu lugar. Não é muito, mas esse pequeno lugar em nosso código tem regras.
Mas primeiro, deixe-me contar minha história.
Comecei a programar com bancos de dados usando xBase. Isso foi na faculdade até meus dois primeiros anos de programação profissional. Estou lhe dizendo isso porque, antigamente, costumávamos processar dados sequencialmente, não em lotes definidos como o SQL. Quando aprendi SQL, foi como uma mudança de paradigma. O mecanismo de banco de dados decide por mim com seus comandos baseados em conjuntos que eu emiti. Quando eu aprendi sobre o SQL CURSOR, parecia que eu estava de volta com as formas antigas, mas confortáveis.
Mas alguns colegas seniores me avisaram:“Evite o SQL CURSOR a todo custo!” Recebi algumas explicações verbais, e foi isso.
SQL CURSOR pode ser ruim se você usá-lo para o trabalho errado. Como usar um martelo para cortar madeira, é ridículo. É claro que erros podem acontecer, e é aí que nosso foco estará.
1. Usando SQL CURSOR quando comandos baseados em conjunto funcionarão
Eu não posso enfatizar isso o suficiente, mas ESTE é o coração do problema. Quando eu aprendi o que era SQL CURSOR, uma lâmpada acendeu. "Rotações! Eu sei que!" No entanto, não até que me deu dores de cabeça e meus superiores me repreenderam.
Você vê, a abordagem do SQL é baseada em conjuntos. Você emite um comando INSERT dos valores da tabela e ele fará o trabalho sem loops em seu código. Como eu disse anteriormente, é o trabalho do mecanismo de banco de dados. Portanto, se você forçar um loop para adicionar registros a uma tabela, estará ignorando essa autoridade. Vai ficar feio.
Antes de tentarmos um exemplo ridículo, vamos preparar os dados:
SELECT TOP (500)
val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2
SELECT
tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'
A primeira instrução irá gerar 500 registros de dados. O segundo receberá um subconjunto dele. Então, estamos prontos. Vamos inserir os dados ausentes de TestTable em TestTable2 usando o CURSOR SQL. Ver abaixo:
DECLARE @val INT
DECLARE test_inserts CURSOR FOR
SELECT val FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
INSERT INTO TestTable2
(val, modified, status)
VALUES
(@val, GETDATE(),'inserted')
FETCH NEXT FROM test_inserts INTO @val
END
CLOSE test_inserts
DEALLOCATE test_inserts
Isso é como fazer um loop usando o SQL CURSOR para inserir um registro ausente um por um. Bem longo, não é?
Agora, vamos tentar uma maneira melhor - a alternativa baseada em conjuntos. Aqui vai:
INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
Isso é curto, limpo e rápido. Quão rápido? Veja a Figura 1 abaixo:
Usando o xEvent Profiler no SQL Server Management Studio, comparei os números de tempo de CPU, duração e leituras lógicas. Como você pode ver na Figura 1, usar o comando baseado em set para INSERT registros vence o teste de desempenho. Os números falam por si. Usar o SQL CURSOR consome mais recursos e tempo de processamento.
Portanto, antes de usar o SQL CURSOR, tente primeiro escrever um comando baseado em conjunto. Vai render melhor a longo prazo.
Mas e se você precisar do SQL CURSOR para fazer o trabalho?
2. Não usando as opções apropriadas do SQL CURSOR
Outro erro que eu mesmo cometi no passado foi não usar as opções apropriadas em DECLARE CURSOR. Existem opções para escopo, modelo, simultaneidade e se rolável ou não. Esses argumentos são opcionais e é fácil ignorá-los. No entanto, se SQL CURSOR for a única maneira de fazer a tarefa, você precisa ser explícito com sua intenção.
Então, pergunte-se:
- Ao percorrer o loop, você navegará pelas linhas apenas para frente ou passará para a primeira, última, anterior ou próxima linha? Você precisa especificar se o CURSOR é somente para frente ou rolável. Isso é DECLARE
CURSOR FORWARD_ONLY ou DECLARECURSOR SCROLL . - Você vai atualizar as colunas no CURSOR? Use READ_ONLY se não for atualizável.
- Você precisa dos valores mais recentes ao percorrer o loop? Use STATIC se os valores não importam se são mais recentes ou não. Use DYNAMIC se outras transações atualizarem colunas ou excluirem linhas que você usa no CURSOR e você precisar dos valores mais recentes. Observação :DINÂMICO será caro.
- O CURSOR é global para a conexão ou local para o lote ou um procedimento armazenado? Especifique se LOCAL ou GLOBAL.
Para obter mais informações sobre esses argumentos, consulte a referência do Microsoft Docs.
Exemplo
Vamos tentar um exemplo comparando três CURSORs para o tempo de CPU, leituras lógicas e duração usando o xEvents Profiler. O primeiro não terá opções apropriadas após DECLARE CURSOR. O segundo é LOCAL STATIC FORWARD_ONLY READ_ONLY. O último é LOtyuiCAL FAST_FORWARD.
Aqui está o primeiro:
-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.
-- DECLARE CURSOR with no options
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR FOR
SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
Há uma opção melhor do que o código acima, é claro. Se o objetivo for apenas gerar um script a partir de tabelas de usuários existentes, SELECT servirá. Em seguida, cole a saída em outra janela de consulta.
Mas se você precisar gerar um script e executá-lo de uma vez, é outra história. Você deve avaliar o script de saída se vai sobrecarregar seu servidor ou não. Veja o erro nº 4 mais tarde.
Para mostrar a comparação de três CURSORs com opções diferentes, isso serve.
Agora, vamos ter um código semelhante, mas com LOCAL STATIC FORWARD_ONLY READ_ONLY.
--- STATIC LOCAL FORWARD_ONLY READ_ONLY
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
Como você pode ver acima, a única diferença do código anterior é o LOCAL STATIC FORWARD_ONLY READ_ONLY argumentos.
O terceiro terá um LOCAL FAST_FORWARD. Agora, de acordo com a Microsoft, FAST_FORWARD é um FORWARD_ONLY, READ_ONLY CURSOR com otimizações habilitadas. Veremos como isso se sairá com os dois primeiros.
Como eles se comparam? Veja a Figura 2:
O que leva menos tempo de CPU e duração é o LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Observe também que o SQL Server tem padrões se você não especificar argumentos como STATIC ou READ_ONLY. Há uma consequência terrível nisso, como você verá na próxima seção.
O que sp_describe_cursor revelado
sp_describe_cursor é um procedimento armazenado do mestre banco de dados que você pode usar para obter informações do CURSOR aberto. E aqui está o que revelou do primeiro lote de consultas sem opções de CURSOR. Consulte a Figura 3 para ver o resultado de sp_describe_cursor :
Exagerar muito? Pode apostar. O CURSOR do primeiro lote de consultas é:
- global para a conexão existente.
- dinâmico, o que significa que rastreia as alterações na tabela #commands para atualizações, exclusões e inserções.
- otimista, o que significa que o SQL Server adicionou uma coluna extra a uma tabela temporária chamada CWT. Esta é uma coluna de soma de verificação para rastrear alterações nos valores da tabela #commands.
- rolagem, o que significa que você pode passar para a linha anterior, seguinte, superior ou inferior no cursor.
Absurdo? Concordo plenamente. Por que você precisa de uma conexão global? Por que você precisa acompanhar as alterações na tabela temporária #commands? Rolamos em qualquer lugar que não seja o próximo registro no CURSOR?
Como um SQL Server determina isso para nós, o loop CURSOR se torna um erro terrível.
Agora você percebe porque especificar explicitamente as opções do SQL CURSOR é tão crucial. Portanto, a partir de agora, sempre especifique esses argumentos CURSOR se precisar usar um CURSOR.
O plano de execução revela mais
O Plano de Execução Real tem algo mais a dizer sobre o que acontece toda vez que um comando FETCH NEXT FROM command_builder INTO @command é executado. Na Figura 4, uma linha é inserida no índice clusterizado CWT_PrimaryKey no tempdb tabela CWT :
As gravações acontecem em tempdb em cada FETCH NEXT. Além disso, há mais. Lembra que o CURSOR está OPTIMISTIC na Figura 3? As propriedades do Clustered Index Scan na parte mais à direita do plano revelam a coluna extra desconhecida chamada Chk1002 :
Esta poderia ser a coluna Checksum? O XML do Plano confirma que este é realmente o caso:
Agora, compare o Plano de Execução Real do FETCH NEXT quando o CURSOR for LOCAL STATIC FORWARD_ONLY READ_ONLY:
Ele usa tempdb também, mas é muito mais simples. Enquanto isso, a Figura 8 mostra o Plano de Execução quando LOCAL FAST_FORWARD é usado:
Recomendações
Um dos usos apropriados do SQL CURSOR é gerar scripts ou executar alguns comandos administrativos para um grupo de objetos de banco de dados. Mesmo que haja usos menores dele, sua primeira opção é usar o LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR ou LOCAL FAST_FORWARD. Aquele com um plano melhor e leituras lógicas vencerá.
Em seguida, substitua qualquer um deles pelo apropriado, conforme a necessidade. Mas você sabe o que? Na minha experiência pessoal, usei apenas um CURSOR somente leitura local com travessia somente para frente. Nunca precisei tornar o CURSOR global e atualizável.
Além de usar esses argumentos, o momento da execução é importante.
3. Usando SQL CURSOR em transações diárias
não sou administrador. Mas tenho uma ideia de como é um servidor ocupado pelas ferramentas do DBA (ou de quantos decibéis os usuários gritam). Nestas circunstâncias, você vai querer adicionar mais encargos?
Se você está tentando criar seu código com um CURSOR para transações do dia-a-dia, pense novamente. CURSORs são bons para execuções únicas em um servidor menos ocupado com pequenos conjuntos de dados. No entanto, em um dia cheio típico, um CURSOR pode:
- Bloqueie linhas, especialmente se o argumento de simultaneidade SCROLL_LOCKS for especificado explicitamente.
- Provocar alto uso de CPU.
- Usar tempdb extensivamente.
Imagine que você tenha vários desses funcionando simultaneamente em um dia típico.
Estamos prestes a terminar, mas há mais um erro sobre o qual precisamos falar.
4. Não avaliar o impacto que o SQL CURSOR traz
Você sabe que as opções de CURSOR são boas. Você acha que especificá-los é suficiente? Você já viu os resultados acima. Sem as ferramentas, não chegaríamos à conclusão certa.
Além disso, há código dentro do CURSOR . Dependendo do que faz, adiciona mais aos recursos consumidos. Estes podem estar disponíveis para outros processos. Toda a sua infraestrutura, seu hardware e a configuração do SQL Server adicionarão mais à história.
E quanto ao volume de dados ? Eu só usei SQL CURSOR em algumas centenas de registros. Pode ser diferente para você. O primeiro exemplo levou apenas 500 registros porque esse era o número que eu concordaria em esperar. 10.000 ou mesmo 1.000 não foram suficientes. Eles tiveram um desempenho ruim.
Eventualmente, não importa o quão menos ou mais, verificar as leituras lógicas, por exemplo, pode fazer a diferença.
E se você não verificar o Plano de Execução, as leituras lógicas ou o tempo decorrido? Que coisas terríveis podem acontecer além dos congelamentos do SQL Server? Só podemos imaginar todos os tipos de cenários apocalípticos. Você entendeu.
Conclusão
SQL CURSOR funciona processando dados linha por linha. Tem seu lugar, mas pode ser ruim se você não tomar cuidado. É como uma ferramenta que raramente sai da caixa de ferramentas.
Então, em primeiro lugar, tente resolver o problema usando comandos baseados em conjuntos. Ele responde à maioria das nossas necessidades de SQL. E se você usar o SQL CURSOR, use-o com as opções certas. Estime o impacto com o plano de execução, STATISTICS IO e xEvent Profiler. Em seguida, escolha o momento certo para executar.
Tudo isso fará seu uso do SQL CURSOR um pouco melhor.