Является ли ALTER TABLE… DROP COLUMN действительно операцией только с метаданными?

11

Я нашел несколько источников, которые утверждают, что ALTER TABLE ... DROP COLUMN - это операция только с метаданными.

Источник

Как это может быть? Не нужно ли удалять данные во время DROP COLUMN из базовых некластеризованных индексов и кластеризованного индекса / кучи?

Кроме того, почему Microsoft Docs подразумевает, что это полностью зарегистрированная операция?

Изменения, внесенные в таблицу, регистрируются и полностью восстанавливаются. Изменения, затрагивающие все строки в больших таблицах, такие как удаление столбца или добавление столбца NOT NULL со значением по умолчанию в некоторых выпусках SQL Server, могут занять много времени и создать много записей журнала . Запустите эти операторы ALTER TABLE с той же тщательностью, что и любой оператор INSERT, UPDATE или DELETE, который влияет на множество строк.

В качестве дополнительного вопроса: как движок отслеживает пропущенные столбцы, если данные не удаляются с нижележащих страниц?

George.Palacios
источник
2
Ну, я думаю, что этот язык пережил многие версии продукта и множество других версий документации. Со временем все больше и больше операций, связанных со столбцами, стали онлайн-изменениями / метаданными. Сейчас это, возможно, плохой конкретный пример, но цель предложения состоит в том, чтобы просто предупредить вас, что, как правило, некоторые операции изменения могут быть операциями с размером данных в определенных сценариях, а не перечислять каждый отдельный сценарий.
Аарон Бертран

Ответы:

14

Существуют определенные обстоятельства, при которых удаление столбца может быть операцией только с метаданными. Определения столбцов для любой данной таблицы не включены в каждую страницу, где хранятся строки, определения столбцов хранятся только в метаданных базы данных, включая sys.sysrowsets, sys.sysrscols и т. Д.

При удалении столбца, на который не ссылается какой-либо другой объект, механизм хранения просто помечает определение столбца как более не присутствующее, удаляя соответствующие данные из различных системных таблиц. Действие удаления метаданных делает недействительным кеш процедуры, что требует повторной компиляции всякий раз, когда запрос впоследствии ссылается на эту таблицу. Поскольку перекомпиляция возвращает только те столбцы, которые в настоящий момент существуют в таблице, сведения о столбцах для удаленного столбца даже не запрашиваются; механизм хранения пропускает байты, хранящиеся на каждой странице для этого столбца, как будто столбец больше не существует.

Когда с таблицей происходит последующая операция DML, затронутые страницы перезаписываются без данных для отброшенного столбца. Если вы перестраиваете кластерный индекс или кучу, все байты для отброшенного столбца, естественно, не записываются обратно на страницу на диске. Это эффективно распределяет нагрузку от падения колонны с течением времени, делая ее менее заметной.

Существуют обстоятельства, когда вы не можете удалить столбец, например, когда столбец включен в индекс или когда вы вручную создали объект статистики для столбца. Я написал сообщение в блоге, в котором показана ошибка, возникающая при попытке изменить столбец с помощью созданного вручную объекта статистики. Та же семантика применяется при удалении столбца - если на столбец ссылается какой-либо другой объект, его нельзя просто удалить. Ссылочный объект должен быть сначала изменен, затем столбец может быть удален.

Это довольно легко показать, посмотрев содержимое журнала транзакций после удаления столбца. Приведенный ниже код создает таблицу с одним столбцом длиной 8 000 символов. Он добавляет строку, затем удаляет ее и отображает содержимое журнала транзакций, применимого к операции удаления. Записи журнала показывают модификации различных системных таблиц, в которых хранятся определения таблиц и столбцов. Если данные столбца фактически удаляются со страниц, выделенных для таблицы, вы увидите записи журнала, в которых записаны фактические данные страницы; таких записей нет

DROP TABLE IF EXISTS dbo.DropColumnTest;
GO
CREATE TABLE dbo.DropColumnTest
(
    rid int NOT NULL
        CONSTRAINT DropColumnTest_pkc
        PRIMARY KEY CLUSTERED
    , someCol varchar(8000) NOT NULL
);

INSERT INTO dbo.DropColumnTest (rid, someCol)
SELECT 1, REPLICATE('Z', 8000);
GO

DECLARE @startLSN nvarchar(25);

SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

DECLARE @a int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1)
      , @b int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1)
      , @c int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

ALTER TABLE dbo.DropColumnTest DROP COLUMN someCol;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)


--modify an existing data row 
SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

SET @a = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1);
SET @b = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1);
SET @c = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

UPDATE dbo.DropColumnTest SET rid = 2;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)

(Вывод слишком велик, чтобы показать здесь, и dbfiddle.uk не позволит мне получить доступ к fn_dblog)

Первый набор выходных данных показывает журнал как результат того, что оператор DDL отбрасывает столбец. Второй набор выходных данных показывает журнал после выполнения инструкции DML, где мы обновляем ridстолбец. Во втором наборе результатов мы видим записи в журнале, указывающие на удаление для dbo.DropColumnTest, за которым следует вставка в dbo.DropColumnTest. Длина каждой записи журнала - 8116, что указывает на то, что фактическая страница была обновлена.

Как вы можете видеть на выходе fn_dblogкоманды в вышеупомянутом тесте, вся операция будет полностью вошла. Это касается простого восстановления, а также полного восстановления. Терминология «полностью зарегистрировано» может быть неверно истолкована, поскольку изменение данных не зарегистрировано. Это не то , что происходит - изменение в журнале, и может быть полностью откат. В журнале просто записываются только те страницы, к которым было выполнено прикосновение, и поскольку ни одна из страниц данных таблицы не была записана с помощью операции DDL, то DROP COLUMNи откат, и любой возможный откат могут произойти очень быстро, независимо от размера таблицы.

Для науки , следующий код выведет страницы данных для таблицы, включенной в код выше, используя DBCC PAGEстиль «3». Стиль «3» указывает на то, что нам нужен заголовок страницы плюс подробная интерпретация для каждой строки . Код использует курсор для отображения деталей для каждой страницы в таблице, поэтому вы можете убедиться, что вы не запускаете это на большой таблице.

DBCC TRACEON(3604); --directs out from DBCC commands to the console, instead of the error log
DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT dpa.allocated_page_file_id
    , dpa.allocated_page_page_id
FROM sys.schemas s  
    INNER JOIN sys.objects o ON o.schema_id = s.schema_id
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), o.object_id, NULL, NULL, 'DETAILED') dpa
WHERE o.name = N'DropColumnTest'
    AND s.name = N'dbo'
    AND dpa.page_type_desc = N'DATA_PAGE';
OPEN cur;
FETCH NEXT FROM cur INTO @fileid, @pageid;
WHILE @@FETCH_STATUS = 0
BEGIN
    DBCC PAGE (@dbid, @fileid, @pageid, 3);
    FETCH NEXT FROM cur INTO @fileid, @pageid;
END
CLOSE cur;
DEALLOCATE cur;
DBCC TRACEOFF(3604);

Глядя на вывод для первой страницы из моей демонстрации (после удаления столбца, но до обновления столбца), я вижу это:

СТРАНИЦА: (1: 100104)


BUFFER:


BUF @ 0x0000021793E42040

bpage = 0x000002175A7A0000 bhash = 0x0000000000000000 bpageno = (1: 100104)
bdbid = 10 записей = 1 bcputicks = 0
bsampleCount = 0 bUse1 = 13760 bstat = 0x10b
blog = 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640
bstat2 = 0x0                        

ЗАГОЛОВОК СТРАНИЦЫ:


Page @ 0x000002175A7A0000

m_pageId = (1: 100104) m_headerVersion = 1 m_type = 1
m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000
m_objId (AllocUnitId.idObj) = 300 m_indexId (AllocUnitId.idInd) = 256 
Метаданные: AllocUnitId = 72057594057588736                                
Метаданные: PartitionId = 72057594051756032 Метаданные: IndexId = 1
Метаданные: ObjectId = 174623665 m_prevPage = (0: 0) m_nextPage = (0: 0)
pminlen = 8 m_slotCnt = 1 m_freeCnt = 79
m_freeData = 8111 m_reservedCnt = 0 m_lsn = (616: 14191: 25)
m_xactReserved = 0 m_xdesId = (0: 0) m_ghostRecCnt = 0
m_tornBits = 0 ID фрагмента БД = 1                      

Статус распределения

GAM (1: 2) = ВЫДЕЛЕННЫЙ SGAM (1: 3) = НЕ ВЫДЕЛЕННЫЙ          
PFS (1: 97056) = 0x40 ALLOCATED 0_PCT_FULL DIFF (1: 6) = ИЗМЕНЕНО
ML (1: 7) = НЕ MIN_LOGGED           

Слот 0 Смещение 0x60 Длина 8015

Тип записи = PRIMARY_RECORD Атрибуты записи = NULL_BITMAP VARIABLE_COLUMNS
Размер записи = 8015                  
Дамп памяти @ 0x000000B75227A060

0000000000000000: 30000800 01000000 02000001 004f1f5a 5a5a5a5a 0 ............ O.ZZZZZ
0000000000000014: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
,
,
,
0000000000001F2C: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ
0000000000001F40: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a ZZZZZZZZZZZZZZ

Слот 0 Колонка 1 Смещение 0x4 Длина 4 Длина (физическая) 4

рид = 1                             

Слот 0 Столбец 67108865 Смещение 0xf Длина 0 Длина (физическая) 8000

DROPPED = NULL                      

Слот 0 Смещение 0x0 Длина 0 Длина (физическая) 0

KeyHashValue = (8194443284a0)       

Для краткости я удалил большую часть дампа необработанных страниц из вывода, показанного выше. В конце вывода вы увидите это для ridстолбца:

Слот 0 Колонка 1 Смещение 0x4 Длина 4 Длина (физическая) 4

рид = 1                             

Последняя строка выше, rid = 1 возвращает имя столбца и текущее значение, сохраненное в столбце на странице.

Далее вы увидите это:

Слот 0 Столбец 67108865 Смещение 0xf Длина 0 Длина (физическая) 8000

DROPPED = NULL                      

Выходные данные показывают, что слот 0 содержит удаленный столбец, благодаря DELETEDтексту, где обычно находится имя столбца. Значение столбца возвращается, так NULLкак столбец был удален. Однако, как вы можете видеть из необработанных данных REPLICATE('Z', 8000), для этой колонки все еще существует значение длиной 8000 символов . Это пример той части вывода DBCC PAGE:

0000000000001EDC: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
0000000000001EF0: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
0000000000001F04: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
0000000000001F18: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
Макс Вернон
источник