Дисковий простір повний під час вставки, що відбувається?


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'.

І все ж цільова таблиця існує. Фактичний запит не виконувався в явній транзакції, чи може це пояснити існування цільової таблиці?

Чи правильні припущення, які я накреслив тут, правильні? Це ймовірний сценарій трапився?

Відповіді:


17

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

Так, саме так.

Якщо ви робите просту select intoпоза межами explicit transaction, transactionsу режимі автокомісії є два : перший створює, tableа другий заповнює його.

Ви можете довести це таким чином:

У виділеному databaseна тестовому сервері в simple recovery model, спочатку зробіть a 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ваші файли даних.


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".

Але в моїй новій базі даних є порожня таблиця повідомлень:

скріншот нульових результатів із новоствореної таблиці

Отже, як ви підозрювали, це CREATE TABLEвдалося, але INSERTпорцію все відкотили назад. Приблизним рішенням буде використання явної транзакції (яку ви вже відзначили у своєму запитанні).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.