Atualização de SQL demorando muito tempo / alto uso de disco por horas

8

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 WHILEloop. 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?

GFK
fonte
11
Alguma coisa no log de erros do SQL Server? Também de procmon, quais são as compensações no arquivo em que está gravando? Você pode dividir por 8.192 para obter a página e usar DBCC PAGEpara ver o que está sendo gravado.
Martin Smith
3,5 GB parece a quantidade máxima de RAM que uma instalação de 32 bits do Windows pode suportar .. perigo?
tschmit007
@MartinSmith Não há absolutamente nada desde que eu restaurado no SSMS SQL Server Logs e nada no log de eventos do Windows
GFK
Como são os seus índices na tabela A (que colunas, etc.)? Eles estão fragmentados?
Stuart Ainsworth
@ tschmit007 O SQL 2008 R2 x64 Dev Edition no Win Server 2008 R2 x64. É uma VM executando no Hyper-V (o host também é 2008 R2 x64); a VM possui 4,2 GB de memória física usada em 5 GB e 4,6 GB confirmados em 10 GB no máximo; o host possui 7,2 GB de memória física usada em 8 GB e 7,8 confirmados em 16 GB no máximo. Ambas as máquinas são mais lentas devido ao uso do HD, mas não estão entupidas.
GFK 29/01

Respostas:

0
  1. Atenha-se ao comando UPDATE. O CURSOR será mais lento para o que você está tentando fazer.
  2. Solte / desative todos os índices, incluindo os de visualizações indexadas. Se você tiver uma chave estrangeira no AX, solte-a.
  3. Crie um índice que contenha apenas A.B_ID e outro para B.ID.
  4. Mesmo que você esteja usando o modelo de recuperação simples, a última transação estará sempre no log de transações antes de ser liberada para o disco. É por isso que você precisa aumentar seu log de transações e configurá-lo para aumentar em uma quantidade maior (por exemplo, 100 MB).
  5. Além disso, defina o crescimento do arquivo de dados para uma quantidade maior.
  6. Verifique se você possui espaço em disco suficiente para aumentar ainda mais os arquivos de log e dados.
  7. Quando a atualização terminar, recrie / ative os índices que você eliminou / desativou na etapa 2.
  8. Se você não precisar mais deles, descarte os índices criados na etapa 3.

Editar: Como não posso comentar sua postagem original, responderei aqui à sua pergunta da Edição 4. Você tem 7 índices no AX Index e é uma árvore B , e cada atualização nesse campo faz com que a árvore se reequilibre. É mais rápido recriar esses índices do zero do que reequilibrá-los a cada vez.

bojan
fonte
Para o ponto 1, veja minha resposta para ik_zelf. O cursor está lá por motivos de investigação e não tem tanto impacto. Vou implementar o restante de suas sugestões, acho que é tudo o que me resta fazer. Se funcionar, eu ainda ficarei sem uma explicação sobre o que acontece agora ...
GFK
Você pode postar DDL para suas tabelas (incluindo todos os índices, restrições, etc.). Talvez haja algo que atrapalhe seu desempenho e você esteja sentindo falta disso.
bojan
11
Eliminar índices / Atualizar / Reconstruir índices funciona e, embora eu prefira não ter que fazer algo tão drástico, não vejo uma opção. Obrigado!
GFK 31/01
0

Uma coisa a considerar são os recursos do sistema (memória, disco, CPU) durante esse processo. Tentei inserir 7 milhões de linhas individuais em uma única tabela em um grande trabalho e meu servidor parou de maneira semelhante à sua.

Acontece que eu não tinha memória suficiente no meu servidor para executar esse trabalho de inserção em massa. Em situações como essa, o SQL gosta de se apegar à memória e não deixá-la ir .... mesmo após o dito comando de inserção pode ou não ter sido concluído. Quanto mais comandos são processados ​​em trabalhos grandes, mais memória é consumida. Uma reinicialização rápida liberou a memória.

O que eu faria é iniciar esse processo do zero com o Gerenciador de tarefas em execução. Se o uso da memória exceder 75%, as chances do seu sistema / processo congelar disparam astronomicamente.

Se sua memória / recursos são realmente limitados, conforme observado acima, então suas opções são cortar o processo em partes menores (com a reinicialização ocasional se o uso da memória for alto) em vez de um grande trabalho ou atualizar para um servidor de 64 bits com muita memória.

Techie Joe
fonte
0

O cenário de atualização é sempre mais rápido do que usar um procedimento.

Como você está atualizando a coluna X de todas as linhas da tabela A, certifique-se de soltar o índice primeiro. Verifique também se não existem itens como gatilhos e restrições ativos nessa coluna.

A atualização de índices é um negócio caro, assim como a validação de restrições e a execução de gatilhos no nível da linha que fazem uma pesquisa em outros dados.

ik_zelf
fonte
Eu não acho que esse é o ponto. Percebo que a atualização dos registros indexados leva tempo e sei que, em geral, parte do tempo é devida a isso. Mas espero isso e estou bem com isso: como eu disse, atualizar 99% das linhas leva 5 min (mesmo usando o cursor), mas por alguma razão, uma linha (e nem sempre a mesma) leva 5h. O que me preocupa é esse comportamento em particular.
GFK
bloqueios não é um problema que você disse .... que tal a utilização do sistema de arquivos, chegando a 90% ou mais?
ik_zelf
não, são
GFK
o que acontece se você tentar copiar a tabela como criar a tabela a_copy como selecionar * de a;
precisa saber é o seguinte