Почему 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 для этой простой команды, чтобы привести к такому огромному росту?

PapillonUK
источник
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, если все значения уже были указаны для столбца.
Эндрю
32

При выполнении команды

ALTER COLUMN ... NOT NULL

Кажется, это реализовано как операция добавления столбца, обновления, удаления столбца.

  • Новая строка вставляется sys.sysrscolsдля представления нового столбца. statusБит 128установлен , указывающий столбец не допускает NULLсек
  • Обновление выполняется в каждой строке таблицы, устанавливая новое значение столбца равным старому значению столбца. Если версии строки «до» и «после» в точности совпадают, это не приводит к записи чего-либо в журнал транзакций, в противном случае обновление регистрируется.
  • Исходный столбец помечается как отброшенный (это изменение метаданных только 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, то он первоначально применяется как изменение только метаданных и фактически записывается в строки только при следующем обновлении (запись, которая фактически происходит в этом последнем экземпляре, просто обновляется до секция подсчета столбца и в NULL_BITMAPкачестве NULL varcharстолбца в конце строки не занимает никакого пространства)
Мартин Смит
источник
5

Я столкнулся с той же проблемой, что и таблица с 200.000.000 строк. Сначала я добавил столбец, который можно обнулять, затем обновил все строки и, наконец, изменил столбец с 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 столбца 'Log Reserve' для строки 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.

TroubleshootingSQL
источник
2
Вопрос в том, почему изменение существующего столбца NOT NULLвызывает ведение журнала. Изменение в 2012 году касается добавления нового NOT NULLстолбца со значением по умолчанию.
Мартин Смит