Дисковое пространство заполнено во время вставки, что происходит?

17

Сегодня я обнаружил, что жесткий диск, на котором хранятся мои базы данных, переполнен. Это случалось раньше, обычно причина очевидна. Обычно это неверный запрос, который приводит к огромным разливам в базу данных tempdb, которая увеличивается до заполнения диска. На этот раз было немного менее очевидно, что произошло, так как tempdb не был причиной полного диска, это была сама база данных.

Факты:

  • Обычный размер базы данных составляет около 55 ГБ, он вырос до 605 ГБ.
  • Файл журнала имеет нормальный размер, файл данных огромен.
  • Файл данных имеет 85% доступного пространства (я интерпретирую это как «воздух»: пространство, которое использовалось, но было освобождено. SQL Server резервирует все пространство после выделения).
  • Размер Tempdb нормальный.

Я нашел вероятную причину; есть один запрос, который выбирает слишком много строк (плохое соединение приводит к выделению 11 миллиардов строк, где ожидается пара сотен тысяч). Это SELECT INTOвопрос, который заставил меня задуматься, мог ли случиться следующий сценарий:

  • SELECT INTO выполнен
  • Целевая таблица создана
  • Данные вставляются как они выбраны
  • Диск заполняется, вызывая сбой вставки
  • SELECT INTO отменяется и откатывается
  • Откат освобождает пространство (уже вставленные данные удаляются), но SQL Server не освобождает освободившееся пространство.

В этой ситуации, однако, я бы не ожидал, что таблица, созданная с помощью, SELECT INTOвсе еще существует, ее следует отбросить при откате. Я проверил это:

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

ROLLBACK

SELECT  * 
FROM    TMP.test

Это приводит к:

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

Тем не менее целевая таблица существует. Фактический запрос не был выполнен в явной транзакции, может ли это объяснить существование целевой таблицы?

Являются ли предположения, которые я сделал здесь, правильными? Это вероятный сценарий произошел?

HoneyBadger
источник

Ответы:

17

Фактический запрос не был выполнен в явной транзакции, может ли это объяснить существование целевой таблицы?

Да, именно так.

Если вы делаете простую select intoвнешнюю часть explicit transaction, transactionsв режиме автоматической фиксации есть две : первая создает, tableа вторая заполняет ее.

Вы можете доказать это себе так:

В выделенном databaseтестовом сервере simple recovery modelсначала создайте checkpointи убедитесь, что журнал содержит только несколько строк (3 для 2016 года), связанных с checkpoint. Затем запустите select intoодну строку и проверьте logснова, ища begin tranсвязанный с 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';

Вы получите 2 ряда, показывая, что у вас было 2 transactions.

Являются ли предположения, которые я сделал здесь, правильными? Это вероятный сценарий произошел?

Да, они верны.

insertЧасть select intoбыла rolled back, но это не освобождает любое пространство данных. Вы можете проверить это, выполнив sp_spaceused; вы увидите много unallocated space.

Если вы хотите, чтобы база данных освободила это нераспределенное пространство, вам следует использовать shrinkсвои файлы данных.

sepupic
источник
15

Вы правы, SELECT...INTOкоманда не атомарна. Это не было документировано во время первоначального поста, но теперь оно вызывается специально на странице SELECT - INTO (Transact-SQL) в MS Docs (да, с открытым исходным кодом!):

Оператор SELECT...INTOсостоит из двух частей - создается новая таблица, а затем вставляются строки. Это означает, что в случае неудачной вставки все они будут откатаны, но новая (пустая) таблица останется. Если вам нужна вся операция для успешного завершения или сбоя в целом, используйте явную транзакцию .

Я создам базу данных, которая использует модель полного восстановления. Я дам ему довольно маленький файл журнала, а затем скажу, что файл журнала не может автоматически расти:

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
)

И тогда я попытаюсь вставить все сообщения из моей копии базы данных StackOverflow2010. Это должно записать кучу вещей в файл журнала.

USE [SelectIntoTestDB];
GO

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

Это привело к следующей ошибке после запуска в течение 4 секунд:

Сообщение 9002, уровень 17, состояние 4, строка 1
Журнал транзакций для базы данных «SelectIntoTestDB» заполнен из-за «ACTIVE_TRANSACTION».

Но в моей новой базе данных есть пустая таблица Posts:

скриншот нулевых результатов из вновь созданной таблицы

Итак, как вы и подозревали, все CREATE TABLEполучилось, но INSERTчасть была откачена. Обходным путем будет использование явной транзакции (которую вы уже отметили в своем вопросе).

Джош Дарнелл
источник