Sim, parece um problema muito genérico, mas ainda não consegui reduzi-lo muito.
Então, eu tenho uma instrução UPDATE em um arquivo em lotes sql:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
B possui 40k registros, A possui 4M e estão relacionados 1 a n via A.B_ID, embora não haja FK entre os dois.
Então, basicamente, estou pré-calculando um campo para fins de mineração de dados. Embora eu tenha mudado o nome das tabelas para esta pergunta, não mudei a declaração, é realmente assim tão simples.
Isso leva horas para ser executado, então decidi cancelar tudo. O banco de dados foi corrompido, então eu o excluí, restaurei um backup que fiz antes de executar a instrução e decidi entrar em detalhes com um cursor:
DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
RAISERROR(@Msg, 10, 1) WITH NOWAIT
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = @Id
FETCH NEXT FROM CursorB INTO @Id
END
Agora eu posso vê-lo sendo executado com uma mensagem com o ID decrescente. O que acontece é que leva cerca de 5 minutos para ir de id = 40k para id = 13
E então no id 13, por algum motivo, parece travar. O banco de dados não possui nenhuma conexão além do SSMS, mas na verdade não está travado:
- o disco rígido está funcionando continuamente, por isso definitivamente está fazendo algo (verifiquei no Process Explorer que é realmente o processo sqlserver.exe usando)
Eu executei sp_who2, localizei o SPID (70) da sessão SUSPENDED e executei o seguinte script:
selecione * de sys.dm_exec_requests r junte sys.dm_os_tasks t em r.session_id = t.session_id em que r.session_id = 70
Isso me dá o wait_type, que é PAGEIOLATCH_SH na maioria das vezes, mas na verdade muda para WRITE_COMPLETION às vezes, o que eu acho que acontece quando está liberando o log
- o arquivo de log, que tinha 1,6 GB quando eu restaurei o banco de dados (e quando ele chegou ao ID 13), agora tem 3,5 GB
Outras informações talvez úteis:
- o número de registros na tabela A para B_ID 13 não é grande (14)
- Minha colega não tem o mesmo problema em sua máquina, com uma cópia desse banco de dados (de alguns meses atrás) com a mesma estrutura.
- A tabela A é de longe a maior tabela do banco de dados
- Ele possui vários índices e várias visualizações indexadas o utilizam.
- Não há outro usuário no banco de dados, é local e nenhum aplicativo o está usando.
- O arquivo LDF não tem tamanho limitado.
- O modelo de recuperação é SIMPLES, o nível de compatibilidade é 100
- O Procmon não me fornece muita informação: o sqlserver.exe está lendo e escrevendo muito nos arquivos MDF e LDF.
Ainda estou esperando o término (já são 13h30), mas esperava que talvez alguém me desse outra ação para tentar solucionar o problema.
Editado: adicionando extração do log de procmon
15:24:02.0506105 sqlservr.exe 1760 ReadFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal
Ao usar o DBCC PAGE, parece estar lendo e gravando em campos que se parecem com as tabelas A (ou um de seus índices), mas para B_ID diferente disso. 13. Reconstruindo índices, talvez?
Editado 2: plano de execução
Portanto, cancelei a consulta (na verdade, excluí o banco de dados e seus arquivos e o restaurei) e verifiquei o plano de execução quanto a:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13
O plano de execução (estimado) é o mesmo que para qualquer B.ID e parece bastante direto. A cláusula WHERE usa uma busca de índice em um índice não clusterizado de B, o JOIN usa uma busca de índice clusterizado nas duas PKs das tabelas. A busca de índice clusterizado em A usa paralelismo (x7) e representa 90% do tempo da CPU.
Mais importante, a execução da consulta com o ID 13 é imediata.
Editado 3: fragmentação do índice
A estrutura dos índices é a seguinte:
B tem um PK em cluster (não o campo ID) e um índice exclusivo não em cluster, cujo primeiro campo é B.ID - esse segundo índice parece ser usado sempre.
A possui uma PK em cluster (campo não relacionado).
Também existem 7 visualizações em A (todas incluem o campo AX), cada uma com sua própria PK em cluster e um outro índice que também inclui o campo AX
As visualizações são filtradas (com campos que não estão nessa equação), então duvido que exista alguma maneira de a UPDATE A usar as próprias visualizações. Mas eles têm um índice que inclui o AX, portanto, alterar o AX significa escrever as 7 visualizações e os 7 índices que eles incluem o campo.
Embora seja esperado que o UPDATE seja mais lento, não há razão para que um ID específico seja muito mais longo que os outros.
Eu verifiquei a fragmentação para todos os índices, todos estavam em <0,1%, exceto os índices secundários das visualizações , todos entre 25% e 50%. Os fatores de preenchimento para todos os índices parecem bons, entre 90% e 95%.
Reorganizei todos os índices secundários e refiz o script novamente.
Ainda está pendurado, mas em um ponto diferente:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Enquanto anteriormente, o log de mensagens era assim:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Updating A for B_ID=13
Isso é estranho, porque significa que nem sequer está pendurado no mesmo ponto do WHILE
loop. O restante tem a mesma aparência: a mesma linha UPDATE aguardando em sp_who2, o mesmo tipo de espera PAGEIOLATCH_EX e o mesmo uso pesado de HD do sqlserver.exe.
O próximo passo é excluir todos os índices e visualizações e recriá-los, eu acho.
Editado 4: excluindo e reconstruindo índices
Portanto, excluí todas as visualizações indexadas que tinha na tabela (7 delas, 2 índices por visualização, incluindo a agrupada). Eu executei o script inicial (sem cursor) e ele foi executado em 5 minutos.
Portanto, meu problema se origina da existência desses índices.
Recriei meus índices depois de executar a atualização e levou 16 minutos.
Agora entendo que os índices demoram para serem reconstruídos e, na verdade, estou bem com a tarefa completa levando 20 minutos.
O que ainda não entendo é: por que, quando executo a atualização sem excluir os índices primeiro, leva várias horas, mas quando os apago primeiro e depois os recriamos, leva 20 minutos. Não deveria levar a mesma hora de qualquer maneira?
DBCC PAGE
para ver o que está sendo gravado.