13.8.26

Фантомы в журнале транзакций


Автор: Martyn Jones, A Better Fire Alarm Is Still a Fire

В прошлой статье было показано, как включить и отключить задачу очистки фантомных записей (ghost cleanup) и как фантомные записи представлены на странице данных. В этой статье мы переходим к тому, как процесс удаления, включая пометку фантомных записей, регистрируется в журнале транзакций.

Если вы не читали предыдущие статьи, рекомендуется сделать это, поскольку в них вводятся основные понятия и терминология, а также закладывается понимание, развиваемое на протяжении всей серии. Код демонстрации также используется и дополняется для примеров в этой статье:

Как используется журнал транзакций

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

В демонстрационном коде используется недокументированная функция fn_dblog(), которая обеспечивает четкое представление внутренней последовательности событий, выполняемых SQL Server во время операции удаления, чтобы можно было изучить отдельные физические изменения, необходимые для реализации фантомов и последующей очистки.

Ниже приведены некоторые ключевые операции, которые покажет демонстрация:

Ключевые операции журнала транзакций во время удаления:

  • LOP_BEGIN_XACT | LCX_NULL
    Начинает транзакцию и регистрирует начало единицы работы в журнале транзакций.
  • LOP_DELETE_ROWS | LCX_MARK_AS_GHOST
    Логически удаляет строку, помечая её как фантомную запись для последующей очистки.
  • LOP_SET_BITS | LCX_PFS
    Устанавливает флаги состояния PFS для страницы, указывая, что страница содержит фантомные записи.
  • LOP_COMMIT_XACT | LCX_NULL
    Фиксирует транзакцию и регистрирует, что зарегистрированные изменения завершены и долговечны.
  • LOP_EXPUNGE_ROWS | LCX_CLUSTERED
    Физически удаляет фантомные строки из структуры данных во время очистки.

Демонстрация

Код в этой статье предназначен для использования в тестовой среде, а не в рабочей.

Предварительные требования

Перед запуском кода из этой статьи необходимо создать базу данных (см. «Настройка демонстрации» в «Статье 2 – Наблюдение за работой процесса очистки фантомов»), затем повторно выполнить код, использованный в «Статье 3 – Дайте фантомам задержаться» в разделе «Viewing ghost records on the data page»; это гарантирует, что данные в журнале транзакций находятся в активной части журнала транзакций. Если требуется больше времени для изучения страниц между шагами, рассмотрите возможность отключения процесса очистки фантомов, как описано в «Статье 3 – Дайте призракам задержаться».

Fn_dblog()

Как уже упоминалось, демонстрационный код будет использовать недокументированную табличную функцию fn_dblog(), которая позволяет читать активную часть журнала транзакций.

Функция принимает два параметра, представляющих начальный и конечный LSN. Передача NULL для обоих параметров возвращает все доступные записи из активного журнала транзакций, а не ограничивает вывод конкретным диапазоном LSN.

Результат содержит одну строку для каждой зарегистрированной операции и включает такую информацию, как:

  • Transaction ID – Идентифицирует транзакцию, сгенерировавшую запись журнала.
  • Begin Time – Время начала транзакции.
  • Transaction Name – Имя транзакции (например, INSERT, DELETE).
  • Transaction SID – Кто её начал (используйте функцию SUSER_SNAME для отображения понятного имени).
  • Operation – Тип выполненной операции (например, LOP_INSERT_ROWS, LOP_DELETE_ROWS или LOP_COMMIT_XACT).
  • Context – Тип затронутой страницы или структуры (например, LCX_CLUSTERED, LCX_HEAP или LCX_PFS).
  • Page ID – Идентификатор физического файла и страницы (номер страницы в шестнадцатеричном формате).
  • Slot ID – Запись на странице.
  • Current LSN – Уникальный LSN, идентифицирующий запись журнала.
  • Description – Дополнительные сведения, описывающие зарегистрированное изменение.

Запрос к журналу транзакций

Для этой демонстрации процесс очистки фантомов отключен перед запуском кода из Статьи 3. Поэтому мы увидим записи, отмеченные для удаления, и у нас будет время просмотреть журнал транзакций и то, как это связано со страницами.

/* Запустите код из Статьи 2 */ /* Отключите процесс очистки фантомов */ DBCC TRACEON(661, -1); /* Запустите код из Статьи 3 */ /* Просмотр журнала транзакций */ USE GhostDemo; DROP TABLE IF EXISTS #TLog; SELECT [Transaction ID] , [Begin Time] , [Transaction Name] , SUSER_SNAME ([Transaction SID]) AS [Started By] , [End Time] , [Operation] , Context , CONVERT(int, CONVERT(varbinary(4), SUBSTRING([Page ID], 6, 8), 2)) AS [Decimal Page ID] , [Slot ID] , [Current LSN] , [Description] INTO #TLog FROM fn_dblog (NULL, NULL) as tl WHERE SUSER_SNAME ([Transaction SID]) NOT IN ('sa', 'NT SERVICE\SQLTELEMETRY') OR [Transaction SID] IS NULL; /* Показать все записи журнала в журнале транзакций, упорядоченные по Current LSN */ SELECT * FROM #TLog ORDER BY [Current LSN];

Демонстрация показывает, что удаленные записи помечены как фантомные:


Изображение: результаты запроса, показывающие LOP_BEGIN_XACT, LOP_DELETE_ROWS | LCX_MARK_AS_GHOST, LOP_SET_BITS | LCX_PFS, LOP_COMMIT_XACT

Изображение: результаты запроса, показывающие LOP_BEGIN_XACT, LOP_DELETE_ROWS | LCX_MARK_AS_GHOST, LOP_SET_BITS | LCX_PFS, LOP_COMMIT_XACT

Это показывает:

  • Начало транзакции (LOP_BEGIN_XACT).
  • Первая запись помечается как фантомная (LOP_DELETE_ROWS | LCX_MARK_AS_GHOST).
  • Страница PFS обновляется; это отображается в другом контексте транзакции, поскольку SQL Server обновляет метаданные выделения с использованием внутренних системных операций. (LOP_SET_BITS | LCX_PFS) – столбец details показывает изменение, указывая, что GhostBit будет записан для страницы.
  • Транзакция фиксируется (LOP_COMMIT_XACT).

Каждая запись существует в слоте в массиве слотов на странице данных. Каждая запись, помеченная как фантомная, имеет Slot ID; это ссылка на физическое расположение страницы, а не постоянный идентификатор. Поскольку мы удалили 11 записей, отображается 11 уникальных Slot ID.

Примечание: Page ID, возвращаемый fn_dblog(), является шестнадцатеричным значением; код выше преобразует его в десятичное для более легкого сравнения с DBCC PAGE и метаданными выделения; в описании (Description) по-прежнему будет отображаться шестнадцатеричная версия.

Просмотр страницы

Используя DBCC PAGE (показано в прошлой статье), мы можем просмотреть страницу 352 и увидеть изменение:

/* Просмотр данных страницы */ DBCC PAGE(GhostDemo, 1, 352, 3);

 

Изображение: вывод DBCC PAGE, показывающий m_slotCnt = 12 и m_ghostRecCnt = 11

Изображение: вывод DBCC PAGE, показывающий m_slotCnt = 12 и m_ghostRecCnt = 11

Заголовок показывает количество слотов (m_slotCnt = 12), которое теперь можно связать с Slot ID в журнале транзакций. На данный момент количество фантомных записей в заголовке страницы (m_ghostRecCnt = 11) соответствует количеству строк, помеченных как фантомные в журнале транзакций LCX_MARK_AS_GHOST, записанных для 11 слотов. Мы также видим используемую страницу PFS (1:1).

Как показано в прошлой статье, записи, которые стали фантомными, теперь помечены как таковые на странице данных рядом со своей строкой:

Изображение: фантомные записи на странице данных

Изображение: фантомные записи на странице данных

Страница PFS

DBCC PAGE также можно использовать для просмотра данных на странице PFS:

DBCC PAGE(GhostDemo, 1, 1, 3);

Как видно в журнале транзакций, запись PFS для страницы 352 обновлена, и установлен GhostBit, указывающий, что страница содержит фантомные записи.

Изображение: вывод DBCC PAGE для страницы PFS с установленным GhostBit

Изображение: вывод DBCC PAGE для страницы PFS с установленным GhostBit

На данный момент…

11 записей были удалены, и это можно увидеть как в журнале транзакций, так и на страницах данных и PFS.

Включение очистки фантомов

/* Когда все необходимые страницы просмотрены, включите процесс очистки фантомов обратно */ DBCC TRACEOFF(661, -1);

Повторный просмотр журнала транзакций покажет LOP_EXPUNGE_ROWS | LCX_CLUSTERED — это процесс очистки фантомов, физически удаляющий данные строк со страницы 352 (в данном примере это кластерный индекс). Также будет показано обновление PFS, где флаг GhostBit для страницы снимается.

Изображение: записи LOP_EXPUNGE_ROWS и обновление PFS в журнале транзакций

Изображение: записи LOP_EXPUNGE_ROWS и обновление PFS в журнале транзакций

Полный запуск с включенной очисткой фантомов

Обычная операция удаления следует тому же процессу, но физическое удаление строк может произойти быстрее, в зависимости от активности системы и времени выполнения задачи очистки фантомов. В малонагруженной демонстрационной системе очистка, скорее всего, будет быстрой.

Демонстрация полного жизненного цикла

Следующая демонстрация обновляет базу данных, затем добавляет и удаляет записи, показывая полный жизненный цикл процесса.

/* Освежим базу данных */ USE master; GO ALTER DATABASE GhostDemo SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE GhostDemo; GO /* Создадим демонстрационную базу данных, таблицу и вставим данные */ CREATE DATABASE GhostDemo; GO USE GhostDemo; GO CREATE TABLE [dbo].[ViewGhost]( [ID] [int] IDENTITY(1,1) NOT NULL, [Name] [varchar](100) NULL ) ON [PRIMARY]; GO CREATE CLUSTERED INDEX PK_ViewGhost ON dbo.ViewGhost (ID); GO /* Создаем фантомы */ USE [GhostDemo]; /* Выводим результаты в окно запроса */ DBCC TRACEON(3604); GO /* Вставляем образцы строк */ INSERT INTO dbo.ViewGhost (Name) VALUES ('RowText') GO 12 /* Логически удаляем все строки, кроме одной */ DELETE FROM dbo.ViewGhost WHERE ID > 1; GO

После удаления записей мы можем просмотреть журнал транзакций. В зависимости от активности системы задача очистки фантомов может выполниться быстро или быть отложена. Если удаление записей не видно сразу, подождите немного и попробуйте снова; в конечном итоге они будут физически удалены.

/* Получение данных журнала транзакций */ DROP TABLE IF EXISTS #TLog; SELECT [Transaction ID] , [Begin Time] , [Transaction Name] , SUSER_SNAME ([Transaction SID]) AS [Started By] , [End Time] , [Operation] , Context , CONVERT(int, CONVERT(varbinary(4), SUBSTRING([Page ID], 6, 8), 2)) AS [Decimal Page ID] , [Slot ID] , [Current LSN] , [Description] INTO #TLog FROM fn_dblog (NULL, NULL) as tl WHERE ( SUSER_SNAME ([Transaction SID]) NOT IN ('sa', 'NT SERVICE\SQLTELEMETRY') OR [Transaction SID] IS NULL ) AND ( ( [Transaction Name] NOT LIKE '%QDS%' AND [Transaction Name] <> 'UpdateQPStats' ) OR [Transaction Name] IS NULL ); /* Показать все записи журнала в журнале транзакций, упорядоченные по Current LSN */ SELECT * FROM #TLog ORDER BY [Current LSN];

Сначала мы видим появление фантомов…

Изображение: записи LOP_DELETE_ROWS, LOP_SET_BITS, LOP_COMMIT_XACT

Изображение: записи LOP_DELETE_ROWS, LOP_SET_BITS, LOP_COMMIT_XACT

…затем физическое удаление, которое не является частью пользовательской транзакции DELETE; оно выполняется асинхронно задачей очистки фантомов.

Изображение: записи LOP_EXPUNGE_ROWS и LOP_SET_BITS для снятия GhostBit

Изображение: записи LOP_EXPUNGE_ROWS и LOP_SET_BITS для снятия GhostBit

Резюме

В этой статье был показан полный жизненный цикл операции удаления, использующей механизм фантомных записей SQL Server, и то, как каждый этап представлен внутри системы.

Используя недокументированную функцию fn_dblog(), мы смогли увидеть, что операция удаления не является немедленным физическим удалением строк. Вместо этого SQL Server сначала выполняет логическое удаление, помечая строки как фантомные:

  • LOP_BEGIN_XACT — регистрирует начало транзакции удаления.
  • LOP_DELETE_ROWS | LCX_MARK_AS_GHOST — регистрирует каждую строку, логически удаляемую путем пометки её как фантомной записи.
  • LOP_SET_BITS | LCX_PFS — обновляет метаданные PFS, устанавливая GhostBit для затронутой страницы, чтобы указать на наличие фантомных записей.
  • LOP_COMMIT_XACT — завершает пользовательскую транзакцию, оставляя фантомные записи физически присутствующими на странице данных.

Используя DBCC PAGE, мы затем смогли сопоставить записи журнала транзакций с физической страницей данных. Slot ID из журнала транзакций идентифицировал отдельные затронутые записи, в то время как заголовок страницы показывал увеличение m_ghostRecCnt и подтверждал, что строки оставались на странице в фантомном состоянии.

Страница PFS предоставила еще один взгляд на то же изменение: GhostBit был установлен для записи страницы данных, позволяя SQL Server идентифицировать страницы, содержащие фантомные записи, требующие очистки.

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

  • LOP_EXPUNGE_ROWS | LCX_CLUSTERED — показывающее физическое удаление фантомных записей.
  • LOP_SET_BITS | LCX_PFS — снимающее GhostBit, как только страница больше не содержала фантомных записей.

Полный процесс демонстрирует разделение между логическим удалением и физическим удалением.



12.8.26

Лучшая пожарная сигнализация — всё равно пожар


Автор: Pinal Dave, A Better Fire Alarm Is Still a Fire

Многие инциденты с SQL Server, по которым меня вызывают, можно было предотвратить с помощью средств, уже имеющихся в среде заказчика. Вот аудит, который я хотел бы видеть выполненным до того, как прозвучал пейджер, — с полным T-SQL.

11.8.26

Почему большие столбцы могут не влиять на логические чтения


Автор: Brent Ozar, Database Animations: Why Big Columns May Not Affect Logical Reads

Со временем таблицы — как и наша талия — имеют свойство увеличиваться. Мы постоянно добавляем всё новые и новые столбцы, один за другим, чтобы удовлетворить потребности приложений. Добавить «ещё один столбец» проще, чем выносить что-то в отдельную таблицу.

Когда вы работаете лишь с несколькими строками за раз, например, при вставке, обновлении, удалении или выборке одной строки по идентификатору, накладные расходы на дополнительные столбцы незначительны. SQL Server может нырнуть в эту одну строку и просто извлечь её, и поскольку она всё равно находится на одной странице размером 8 КБ, количество столбцов не влияет на операции с одной строкой.

Однако, когда вам нужно прочитать много строк, чем больше строк вы читаете, тем сильнее дополнительные столбцы влияют на накладные расходы операции.

10.8.26

SQL Server 2025 показывает высокую загрузку процессоров!

Автор: Rob Farley, SQL 2025 showing crazy-high CPU;

Да, вам стоит подумать о переходе на SQL Server 2025. Но если вы используете учётные записи SQL Server, сначала проверьте это (а если вы здесь, чтобы понять, почему вдруг всё замедлилось, читайте дальше).

В большинстве случаев обновление до SQL Server 2025 проходит довольно гладко. Я бы даже сказал, что обновления до других версий SQL Server тоже проходили гладко, хотя многие пострадали от проблем с производительностью из-за изменения модели оценки кардинальности при обновлении до SQL 2014 (точнее, уровня совместимости 120). «Простым» решением тогда было вернуть уровень совместимости на 110, а затем разобраться, что именно вызывало проблемы. Но в SQL Server 2025 есть ещё одна болевая точка, обойти которую не так просто.

7.8.26

Полное руководство по xEvents

Автор: Vivek Johari, Extended Events (xEvents) in SQL Server & Azure SQL: Complete Guide;

Каждому администратору баз данных рано или поздно приходится сталкиваться с этим: приходит заявка в службу поддержки с сообщением «приложение работает медленно» и без каких-либо других подробностей, и ваша задача — выяснить, какая из тысячи вещей, происходящих внутри SQL Server, на самом деле является виновником. Это была блокировка? Плохой план? Шквал входов в систему? Чей-то ситуативный запрос, в котором забыли условие WHERE и который сейчас сканирует одиннадцать миллионов строк?

Долгое время ответом на вопрос «давайте посмотрим, что происходит в реальном времени» был SQL Server Profiler, работающий поверх SQL Trace. Он работал, но работал так, как работает прожектор, когда на самом деле вам нужен был фонарик — он захватывал всё без разбора, выполнялся на стороне клиента и мог заметно замедлить загруженный рабочий сервер просто фактом своего включения. Достаточно много администраторов баз данных имеют историю о том, как благонамеренная трассировка Profiler ухудшала состояние и без того перегруженного сервера, что в конечном итоге привело к тому, что Microsoft перестала рекомендовать его вообще.

Расширенные события (Extended Events, xEvents) пришли на смену, и это не просто незначительное обновление, — это принципиально иная архитектура, и это инструмент, который экзамен DP-300 требует знать досконально. Эта статья описывает, что это такое, почему он превосходит старые инструменты и — что действительно важно в повседневной работе — как использовать его для отслеживания блокировок, взаимоблокировок, медленных запросов, таймаутов, высокой загрузки ЦП, сбоев входа в систему и статистики ожиданий.

6.8.26

Почему одни статистики остаются устаревшими, в то время как другие обновляются автоматически

Автор: Jose Manuel Jurado (MICROSOFT), Lessons Learned #547:Some SQL DB Statistics Remain Outdated While Others Are Automatically Updated;

В ходе анализа одного инцидента производительности SQL Server мы заметили интересный шаблон обновления статистики в большой таблице. Несколько статистик были недавно обновлены в разное время, в то время как группа автоматически созданных статистик _WA_Sys_ не была обновлена, эти статистики показывали

  • Более старую дату last_updated.
  • Высокое значение modification_counter.
  • Количество строк, значительно меньшее, чем текущее количество строк в таблице.

На первый взгляд это могло бы указывать на то, что AUTO_UPDATE_STATISTICS работает некорректно. Однако более детальный анализ показал, что такой шаблон может полностью соответствовать ожидаемому поведению SQL Server.

5.8.26

Два дополнительных элемента управления при включённой автоматической коррекции планов

Автор: Erin Stellato (MICROSOFT), Two additional controls with Automatic Plan Correction enabled;

Несколько недель назад я опубликовала свой первый «Пятничный отзыв» о Хранилище запросов (Query Store), и один из ответов ссылался на хранимую процедуру, которую я раньше не использовала: sp_configure_automatic_tuning. Было высказано предположение, что эта хранимая процедура не документирована и работает не так, как ожидалось. Зная, как сильно я люблю Хранилище запросов, автоматическую коррекцию планов и документацию, я отправилась на поиски. Если вы не знакомы с этой хранимой процедурой, читайте дальше.

4.8.26

Структуры хранения #6 – JSON-индексы

Автор: Hugo Kornelis, Storage structures 6 – JSON indexes;

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

Microsoft представила ограниченную поддержку JSON в SQL Server 2016. Однако только в SQL Server 2025 появились собственный тип данных json и JSON-индексы. Итак, давайте посмотрим, как они работают «под капотом».

3.8.26

Как и когда сжимать файлы журналов SQL Server: рекомендации

Автор: Steve Stedman, How and When to Shrink SQL Server Log Files: Best Practices

Управление файлами журналов транзакций SQL Server часто ставит перед командами администраторов баз данных неожиданные проблемы при принятии решения о том, как и когда сжимать файлы журналов SQL Server, особенно когда быстрый рост угрожает доступному дисковому пространству. Эти файлы играют жизненно важную роль в записи каждого изменения в базе данных, однако их расширение может быстро обогнать первоначальные ожидания, если не решены основные проблемы активности или конфигурации.

Многие администраторы прибегают к команде сжатия как к немедленному решению, но выполнение этого действия без полного понимания контекста может привести к повторяющимся циклам роста и снижению производительности. Знание правильных условий, при которых сжатие становится уместным, помогает избежать ненужных рисков, одновременно решая насущные проблемы с хранилищем.

В этой статье описаны обстоятельства, оправдывающие сжатие файлов журналов SQL Server, а также проверенные методы безопасного выполнения этого процесса и снижения вероятности будущих проблем.

19.7.26

Диагностика конкуренции за tempdb

Автор: Paul Randal, The Accidental DBA (Day 27 of 30): Troubleshooting: Tempdb Contention

Одна из самых распространённых проблем производительности, существующих в экземплярах SQL Server по всему миру, известна как конкуренция за tempdb. Что это означает? Конкуренция за tempdb относится к узкому месту для потоков, пытающихся получить доступ к страницам распределения, находящимся в памяти; это не связано с вводом-выводом.

18.7.26

Производительность DBCC CHECKDB и индексы на вычисляемых столбцах

Автор: Paul Randal, DBCC CHECKDB performance and computed-column indexes

[Примечание 2016 г.: Команда разработчиков «исправила» проблему в SQL Server 2016, отключив проверку согласованности этих индексов, если не используется параметр WITH EXTENDED_LOGICAL_CHECKS.]

Ни для кого не секрет, что производительность DBCC CHECKDB имеет некоторые особенности, зависящие от схемы проверяемой базы данных и различных видов повреждений. Недавно я проводил тестирование масштабируемости DBCC CHECKDB для статьи в блоге и обнаружил довольно неприятную проблему производительности, существующую во всех версиях SQL Server, начиная с SQL Server 2005. Это не ошибка, это просто особенность работы.

Проблема возникает, когда существует некластерный индекс, в котором вычисляемый столбец является частью ключа индекса или одним из включённых столбцов (INCLUDE), и влияет на DBCC CHECKDB, DBCC CHECKFILEGROUP и DBCC CHECKTABLE.

17.7.26

Наиболее распространённые классы кратких блокировок и их значение

Автор: Paul Randal, Most common latch classes and what they mean

Я проводил опрос о распространённых кратких блокировках (их ещё называют защёлками - latch) на экземплярах SQL Server по всему миру. Я получил информацию почти с 600 серверов, и если вы помните, я дал вам код для вывода основных нестраничных защёлок, ожидаемых во время ожиданий LATCH_XX. Нестраничные защёлки — это те, которые не являются ни PAGELATCH_XX (ожидание доступа к копии страницы файла данных в памяти), ни PAGEIOLATCH_XX (ожидание чтения страницы файла данных с диска в память).