Espaço em disco cheio durante a inserção, o que acontece?

17

Hoje eu descobri o disco rígido que armazena meus bancos de dados estava cheio. Isso já aconteceu antes, geralmente a causa é bastante evidente. Geralmente, há uma consulta incorreta, que causa um grande derramamento no tempdb, que aumenta até o disco ficar cheio. Dessa vez, ficou um pouco menos evidente o que aconteceu, já que o tempdb não foi a causa da unidade completa, foi o próprio banco de dados.

Os fatos:

  • O tamanho usual do banco de dados é de cerca de 55 GB, cresceu para 605 GB.
  • O arquivo de log tem tamanho normal, o arquivo de dados é enorme.
  • O arquivo de dados possui 85% de espaço disponível (eu interpreto isso como 'air': espaço que foi usado, mas foi liberado. O SQL Server reserva todo o espaço depois de alocado).
  • O tamanho do tempdb é normal.

Eu encontrei a causa provável; há uma consulta que seleciona muitas linhas (junção incorreta causa seleção de 11 bilhões de linhas, onde são esperadas algumas centenas de milhares). Esta é uma SELECT INTOconsulta, que me fez pensar se o seguinte cenário poderia ter acontecido:

  • SELECT INTO é executado
  • A tabela de destino é criada
  • Os dados são inseridos à medida que são selecionados
  • O disco enche, causando falha na inserção
  • SELECT INTO é abortado e revertido
  • A reversão libera espaço (os dados já inseridos são removidos), mas o SQL Server não libera o espaço liberado.

Nessa situação, no entanto, eu não esperava que a tabela criada pelo SELECT INTOainda existisse, ela deveria ser descartada pela reversão. Eu testei isso:

BEGIN TRANSACTION 
SELECT  T.x
INTO    TMP.test
FROM    (VALUES(1))T(x)

ROLLBACK

SELECT  * 
FROM    TMP.test

Isto resulta em:

(1 row affected)
Msg 208, Level 16, State 1, Line 8
Invalid object name 'TMP.test'.

No entanto, a tabela de destino existe. Porém, a consulta real não foi executada em uma transação explícita. Isso pode explicar a existência da tabela de destino?

As suposições aqui esboçadas estão corretas? É provável que isso tenha acontecido?

HoneyBadger
fonte

Respostas:

17

Porém, a consulta real não foi executada em uma transação explícita. Isso pode explicar a existência da tabela de destino?

Sim exatamente.

Se você fizer um simples select intofora de um explicit transaction, existem dois transactionsno modo de confirmação automática: o primeiro cria o tablee o segundo o preenche.

Você pode provar isso para si mesmo desta maneira:

Em um databaseservidor dedicado em um servidor de teste simple recovery model, primeiro faça checkpointe garanta que o log contenha apenas algumas linhas (3 no caso de 2016) relacionadas a checkpoint. Em seguida, execute uma select intode uma linha e verifique lognovamente, procurando um begin tranassociado a select into:

checkpoint;

select *
from sys.fn_dblog(null, null);

select 'a' as col
into dbo.t3;  

select *
from sys.fn_dblog(null, null)
where Operation = 'LOP_BEGIN_XACT'
      and [Transaction Name] = 'SELECT INTO';

Você terá 2 linhas, mostrando que você tinha 2 transactions.

As suposições aqui esboçadas estão corretas? É provável que isso tenha acontecido?

Sim, eles estão corretos.

A insertparte de select intowas rolled back, mas não libera espaço para dados. Você pode verificar isso executando sp_spaceused; você verá bastante unallocated space.

Se você deseja que o banco de dados libere esse espaço não alocado, você deve shrinkarquivar seus arquivos de dados.

sepupico
fonte
15

Você está correto, o SELECT...INTOcomando não é atômico. Isso não foi documentado no momento da postagem original, mas agora é chamado especificamente na página SELECT - INTO Cláusula (Transact-SQL) no MS Docs (yay código aberto!):

A SELECT...INTOinstrução opera em duas partes - a nova tabela é criada e as linhas são inseridas. Isso significa que, se as inserções falharem, elas serão revertidas, mas a nova tabela (vazia) permanecerá. Se você precisar que toda a operação seja bem-sucedida ou falhe como um todo, use uma transação explícita .

Vou criar um banco de dados que usa o modelo de recuperação completa. Vou dar a ele um arquivo de log razoavelmente pequeno e dizer que o arquivo de log não pode crescer automaticamente:

CREATE DATABASE [SelectIntoTestDB]
ON PRIMARY 
( 
    NAME = N'SelectIntoTestDB', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\SelectIntoTestDB.mdf', 
    SIZE = 8192KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
( 
    NAME = N'SelectIntoTestDB_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\SelectIntoTestDB_log.ldf', 
    SIZE = 8192KB, 
    FILEGROWTH = 0
)

E então tentarei inserir todas as postagens da minha cópia do banco de dados StackOverflow2010. Isso deve escrever várias coisas no arquivo de log.

USE [SelectIntoTestDB];
GO

SELECT *
INTO dbo.Posts
FROM StackOverflow2010.dbo.Posts;

Isso resultou no seguinte erro após a execução por 4 segundos:

Mensagem 9002, Nível 17, Estado 4, Linha 1
O log de transações do banco de dados 'SelectIntoTestDB' está cheio devido a 'ACTIVE_TRANSACTION'.

Mas há uma tabela Posts vazia no meu novo banco de dados:

captura de tela de zero resultados da tabela recém-criada

Então, como você suspeitava, o CREATE TABLEconseguiu, mas a INSERTparte foi revertida. Uma solução alternativa seria usar uma transação explícita (que você já anotou na sua pergunta).

Josh Darnell
fonte