Чому ALTER COLUMN NOT NULL викликає масовий ріст файлів журналу?


56

У мене є таблиця з 64м рядками, що займає 4,3 ГБ на диску для її даних.

Кожен рядок - це близько 30 байт цілих стовпців, плюс змінний NVARCHAR(255)стовпець для тексту.

Я додав стовпчик NULLABLE з типом даних Datetimeoffset(0).

Потім я оновив цей стовпець для кожного рядка і переконався, що всі нові вставки містять значення у цьому стовпці.

Коли не було записів NULL, я запустив цю команду, щоб зробити нове поле обов'язковим:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

Результатом цього стало велике зростання розміру журналу транзакцій - від 6 ГБ до понад 36 ГБ, поки не вичерпалося місця!

Хтось має уявлення про те, що на землі SQL Server 2008 R2 робить для цієї простої команди, щоб привести до такого величезного зростання?


7
SQL Server 2012 Enterprise додає можливість додавання NOT NULLстовпця з типовою умовою як операція з метаданими. Також див. "Додавання стовпців NOT NULL як операція в Інтернеті" в документації .
Пол Білий

Відповіді:


48

Коли ви змінюєте стовпець на NOT NULL, SQL Server повинен торкатися кожної окремої сторінки, навіть якщо немає значень NULL. Залежно від коефіцієнта заповнення, це фактично може призвести до розбиття сторінки. Кожна сторінка, яка торкнулася, звичайно, повинна бути зареєстрована, і я підозрюю через розбиття, що дві зміни, можливо, доведеться реєструвати на багатьох сторінках. Оскільки це все зроблено за один прохід, але журнал повинен враховувати всі зміни, щоб, якщо ви натиснете кнопку Скасувати, він точно знає, що скасувати.


Приклад. Проста таблиця:

DROP TABLE dbo.floob;
GO

CREATE TABLE dbo.floob
(
  id INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
  bar INT NULL
);

INSERT dbo.floob(bar) SELECT NULL UNION ALL SELECT 4 UNION ALL SELECT NULL;

ALTER TABLE dbo.floob ADD CONSTRAINT df DEFAULT(0) FOR bar

Тепер давайте розглянемо деталі сторінки. Спочатку нам потрібно з’ясувати, на якій сторінці та DB_ID ми маємо справу. У моєму випадку я створив базу даних під назвою foo, і DB_ID трапилася на 5.

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();

Результат вказав, що мене зацікавила сторінка 159 (єдиний рядок у DBCC INDвиведенні з PageType = 1).

Тепер давайте розглянемо деякі деталі вибраної сторінки, переглядаючи сценарій роботи ОП.

DBCC PAGE(5, 1, 159, 3);

введіть тут опис зображення

UPDATE dbo.floob SET bar = 0 WHERE bar IS NULL;    
DBCC PAGE(5, 1, 159, 3);

введіть тут опис зображення

ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;
DBCC PAGE(5, 1, 159, 3);

введіть тут опис зображення

Зараз у мене немає всіх відповідей на це, оскільки я не є глибоким хлопцем. Але зрозуміло, що - хоча операція оновлення та додавання обмежень NOT NULL безперечно записуються на сторінку - остання робить це абсолютно по-іншому. Здається, насправді змінюється структура запису, а не просто поспіль з бітами, замінюючи нульовий стовпчик на стовпчик, що не зводиться нанівець. Чому це потрібно робити, я не зовсім впевнений - гарне питання для команди двигуна зберігання , я думаю. Я вважаю, що SQL Server 2012 обробляє деякі з цих сценаріїв набагато краще, FWIW - але я ще не повинен вичерпувати тестування.


4
Така поведінка значно змінилася в пізніших версіях SQL Server. Я перевірив 2016 RC2 і з’ясував, що для цього точного сценарію та 1 мільйона рядків у таблиці генерується 29 записів журналів під час зміни з NULL на NOT NULL, якщо всі значення були вказані для стовпця.
Endrju

32

При виконанні команди

ALTER COLUMN ... NOT NULL

Це, здається, реалізовано як додавання стовпця, оновлення, випадання стовпця.

  • Новий рядок вставляється, sys.sysrscolsщоб представляти новий стовпець. statusБіт 128встановлений , який вказує стовпець не дозволяє NULLїй
  • Оновлення проводиться в кожному рядку таблиці, встановлюючи нове значення columnn до значення старого значення colum. Якщо версії "до" і "після" точно такі ж, це не призводить до того, що будь-яка річ записується в журнал транзакцій, інакше оновлення реєструється.
  • Початковий стовпчик позначений як скинутий (це лише зміна метаданих sys.sysrscols. rscolidОновлено до великого цілого числа та statusбіта 2, встановленого на вказане скидання)
  • Вхід sys.sysrscolsдля нового стовпця змінено, щоб надати йому rscolidстарого стовпця.

Операція, яка може викликати безліч журналів, є UPDATEвсіма рядками таблиці, однак це не означає, що це завжди буде відбуватися. Якщо зображення рядка "до" і "після" ідентичні, це буде розглядатися як оновлення, яке не оновлюється, і не реєструється в моєму тестуванні.

Отже, пояснення того, чому ви ведете багато журналів, залежатиме від того, чому саме версії "до" та "після" не є однаковими.

Для стовпців змінної довжини, що зберігаються у FixedVarформаті, я виявив, що налаштування NOT NULLзавжди викликає зміну рядка, який потрібно реєструвати. Кількість стовпців та кількість стовпців змінної довжини збільшуються, а новий стовпець додається до кінця розділу змінної довжини, що дублює дані.

datetimeoffset(0)є фіксованою довжиною, однак для стовпців із фіксованою довжиною, що зберігаються у FixedVarформаті, і старому, і новому стовпцям, як видається, надається однаковий проріз у частині даних із фіксованою довжиною рядка, оскільки вони мають однакову довжину та значення "до" та Версії "після" рядка однакові . Це можна побачити у відповіді @ Аарона. Обидві версії рядка до і після ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;є

0x10000c00 01000000 00000000 020000

Це не зареєстровано.

Логічно, з мого опису подій, рядок насправді повинен відрізнятися тут, оскільки кількість стовпців 02слід збільшити, 03але таких змін насправді не відбувається на практиці.

Деякі можливі причини, чому це може статися у стовпці фіксованої довжини, є

  • Якщо стовпець спочатку був оголошений як такий, SPARSEто новий стовпець буде зберігатися в іншій частині рядка від оригіналу, внаслідок чого зображення до і після рядків будуть різними.
  • Якщо ви використовуєте будь-який з параметрів стиснення, то версія до і після рядка буде відрізнятися, оскільки розділ підрахунку стовпців у масиві CD збільшується.
  • У базах даних з увімкненим одним із варіантів ізоляції знімків, тоді інформація про версії в кожному рядку оновлюється (@SQL Kiwi вказує, що це також може відбуватися в базах даних без включеного SI, як описано тут ).
  • Можливо, є якась попередня ALTER TABLEоперація, яка була реалізована як зміна метаданих і ще не була застосована до рядка. Наприклад, якщо був доданий новий стовпчик змінної довжини змінної, то він спочатку застосовується як зміна лише метаданих, і він фактично списується до рядків при наступному оновленні (запис, який насправді відбувається в цьому останньому екземплярі, просто оновлюється до секція підрахунку шпальти і в NULL_BITMAPякості NULL varcharстовпчика в кінці рядка не займає ніякого простору)

5

Я зіткнувся з тією ж проблемою щодо таблиці, що містить 200 000 000 рядків. Спочатку я додав стовпець Nullable, а потім оновлюються всі рядки, і , нарешті , змінили стовпець з NOT NULLдопомогою ALTER TABLE ALTER COLUMNзаяви. Це призвело до того, що дві величезні транзакції неймовірно підірвали журнал (170 Гб).

Найшвидший спосіб я знайшов:

  1. Додайте стовпець, використовуючи значення за замовчуванням

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Відкиньте обмеження за замовчуванням, використовуючи динамічний SQL, оскільки обмеження раніше не було названо:

    DECLARE 
        @constraint_name SYSNAME,
        @stmt NVARCHAR(510);
    
    SELECT @CONSTRAINT_NAME = DC.NAME
    FROM SYS.DEFAULT_CONSTRAINTS DC
    INNER JOIN SYS.COLUMNS C
        ON DC.PARENT_OBJECT_ID = C.OBJECT_ID
        AND DC.PARENT_COLUMN_ID = C.COLUMN_ID
    WHERE
        PARENT_OBJECT_ID = OBJECT_ID('table1')
        AND C.NAME = 'column1';
    

Час виконання зменшився з> 30 хвилин на 10 хвилин, включаючи реплікацію змін за допомогою транзакційної реплікації. Я запускаю інсталяцію SQL Server 2008 (SP2).


2

Я провів наступний тест:

create table tblCheckResult(
        ColID   int identity
    ,   dtoDateTime Datetimeoffset(0) null
    )

 go

insert into tblCheckResult (dtoDateTime)
select getdate()
go 10000

checkpoint 

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

select * from fn_dblog(null,null)

Я вважаю, що це стосується зарезервованого місця в журналі, у випадку, якщо ви повернете транзакцію. Подивіться у функцію fn_dblog у стовпці "Резерв журналу" для рядка LOP_BEGIN_XACT і подивіться, скільки місця намагається резервувати.


Якщо ви спробуєте, select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'ви можете переглянути оновлення 10000 рядків.
Мартін Сміт

-2

Поведінка для цього відрізняється в SQL Server 2012. Див. Http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Кількість записів журналів, створених для версій SQL Server 2008 R2 та нижче, буде значно більшою, ніж кількість записів журналів для SQL Server 2012.


2
Питання в тому, чому зміна існуючого стовпця NOT NULLвикликає ведення журналів. Зміна в 2012 році стосується додавання нової NOT NULLколонки за замовчуванням.
Мартін Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.