2.3.26

Безопасная очистка потерянных записей в Change Tracking (часть 2)

Автор: Mohamed_Baioumy_MSFT, Part 2: Safely Cleaning Orphaned Records in Change Tracking Side Tables

Область применения: База данных SQL Azure (с включенным отслеживанием изменений)

Краткое повторение (Часть 1)

В Части 1 мы рассмотрели, как обнаружить ""Orphaned записи во вспомогательных таблицах отслеживания изменений — строки, чей sys_change_xdes_id больше не имеет соответствующей записи транзакции в таблице фиксации (sys.syscommittab). Эта ситуация часто приводит к неожиданному росту размера отслеживания изменений и симптомам "stuck cleanup", потому что данные сопоставления, необходимые для нормальной очистки, отсутствуют.

Ссылка на Часть 1: Выявление потерянных записей в Change Tracking (часть 1)

Почему нужна Часть 2

Распространенный "корневой шаблон", который мы наблюдаем на практике:

  • Очистка вспомогательных таблиц пытается удалить устаревшие метаданные
  • Некоторые удаления из вспомогательных таблиц завершаются сбоем (блокировки, тайм-ауты, ошибки)
  • Очистка таблицы фиксации всё равно выполняется (или пользовательский рабочий процесс удаляет данные из таблицы фиксации без проверки удаления из вспомогательных таблиц)
  • Оставшиеся строки вспомогательных таблиц теперь ссылаются на значения xdes_id, которые больше не существуют в sys.syscommittab → образуются потерянные записи

В документации Microsoft Learn также подчеркивается, что очистка syscommittab зависит от очистки вспомогательных таблиц — очистка таблицы фиксации должна происходить только после очистки вспомогательных таблиц.

Этот скрипт из Части 2 сосредоточен на удалении потерянных, осиротевших строк из вспомогательных таблиц (и не затрагивает sys.syscommittab), чтобы логика очистки могла снова стабилизироваться.

Важные предварительные требования и ограничения (прочтите это сначала)

1) Доступ к внутренним таблицам в Базе данных SQL Azure

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

2) Примечание о CHECKPOINT (почему он в скрипте)

sys.dm_tran_commit_table предоставляет данные таблицы фиксации и поддерживается sys.syscommittab. В документации Microsoft Learn отмечается, что пользователи, имеющие доступ только для чтения, могут не видеть текущие изменения до выполнения CHECKPOINT.

Вот почему в ваш скрипт включен необязательный комментарий CHECKPOINT перед чтением состояния таблицы фиксации.

3) Поддерживаемые рекомендации против пользовательского исправления

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

Этот скрипт представляет собой целенаправленный шаблон исправления для конкретного режима сбоя (осиротевшие строки вспомогательных таблиц). Используйте его осторожно, сначала протестируйте и следуйте процедурам, принятым в вашей организации.

Что делает этот скрипт (общий обзор)

Скрипт T-SQL, по сути, представляет собой обнаружение из Части 1 + дополнительная необязательная генерация необходимого для удаления кода:

  • Вычисляет "безопасную точку очистки" из периода хранения отслеживания изменений (календарное время → CSN) с помощью sp_changetracking_time_to_csn — та же концепция, что использовалась в Части 1.
  • Перечисляет вспомогательные таблицы отслеживания изменений через sys.internal_tables, где internal_type = 209 (вспомогательные таблицы отслеживания изменений).
  • Для каждой вспомогательной таблицы определяет кандидатные осиротевшие идентификаторы транзакций (sys_change_xdes_id), которые:
    • старше вычисленной границы (@minXdesId, полученной из sys.dm_tran_commit_table), и
    • не имеют соответствующего xdes_id в sys.syscommittab на/до точки очистки
  • Выводит количество осиротевших записей для каждой вспомогательной таблицы с помощью RAISERROR … WITH NOWAIT (удобный для оператора потоковый вывод).
  • Перекрестная проверка безопасности: прерывает работу, если какая-либо "осиротевшая" запись неожиданно существует в sys.syscommittab (защита от "дурака").
  • Генерирует инструкцию DELETE для текущей вспомогательной таблицы (выполнение закомментировано).

Скрипт (Часть 2) — "обнаружение + генерация удаления"

Ниже приведен скрипт T-SQL. Я оставил шаг удаления отключенным по умолчанию, чтобы его можно было безопасно распространять. (Вы можете включить выполнение только после согласования/тестирования.)

-- use <[DBName]> -- переключиться на нужную базу данных


-- сначала выполните checkpoint, чтобы убедиться, что все данные таблицы фиксации в памяти сохранены на диск (syscommittab)
-- checkpoint


SET NOCOUNT ON


-- найти недопустимую версию очистки на основе настроенного периода хранения
DECLARE @time DATETIME, @csn BIGINT = 0, @minCleanupPoint BIGINT = 0
DECLARE @retention_period INT, @retention_period_units NVARCHAR(10)
SELECT @retention_period = retention_period,
@retention_period_units = retention_period_units
FROM sys.change_tracking_databases where database_id = DB_ID()
SELECT @time = CASE WHEN @retention_period_units = 1 then DATEADD(minute, (-1 * @retention_period), GETUTCDATE())
WHEN @retention_period_units = 2 then DATEADD(hour, (-1 * @retention_period), GETUTCDATE())
ELSE DATEADD(day, (-1 * @retention_period), GETUTCDATE()) END

EXEC sp_changetracking_time_to_csn @time = @time, @csn = @csn OUTPUT


SELECT @minCleanupPoint = @csn
SELECT @minCleanupPoint as minCsn -- 688118


-- перебрать все вспомогательные таблицы отслеживания изменений
DECLARE @sideTable SYSNAME;
DECLARE ct_cursor CURSOR FAST_FORWARD FOR
SELECT name FROM sys.internal_tables WHERE internal_type = 209; -- internal_type = 209 для вспомогательных таблиц отслеживания изменений


OPEN ct_cursor;
FETCH NEXT FROM ct_cursor INTO @sideTable;


WHILE @@FETCH_STATUS = 0
BEGIN
-- найти минимальный истекший xdes id
declare @minXdesId BIGINT
SELECT @minXdesId = min(xdes_id) FROM sys.dm_tran_commit_table where commit_ts <= @minCleanupPoint
-- SELECT @minXdesId as minXdes

-- создать временную таблицу для хранения осиротевших xdes id
DROP TABLE IF EXISTS #OrphanedXdes;
    CREATE TABLE #OrphanedXdes
    (
        sys_change_xdes_id BIGINT NOT NULL
    );


DECLARE @sql NVARCHAR(MAX);
SET @sql = N'
    INSERT INTO #OrphanedXdes(sys_change_xdes_id)
    SELECT ct.sys_change_xdes_id
    FROM sys.' + QUOTENAME(@sideTable) + N' AS ct
    WHERE ct.sys_change_xdes_id < @minXdesId
      AND NOT EXISTS
      (
          SELECT 1
          FROM sys.syscommittab AS s
          WHERE s.xdes_id = ct.sys_change_xdes_id AND s.commit_ts <= @minCleanupPoint
      );';


    EXEC sys.sp_executesql
        @sql,
        N'@minXdesId BIGINT, @minCleanupPoint BIGINT',
        @minXdesId = @minXdesId,
@minCleanupPoint = @minCleanupPoint;


DECLARE @orphanedIdsCount BIGINT;
SET @sql = N'
SELECT @cnt = COUNT_BIG(sys_change_xdes_id)
FROM #OrphanedXdes;
';


EXEC sys.sp_executesql
@sql,
N'@cnt BIGINT OUTPUT',
@cnt = @orphanedIdsCount OUTPUT;


-- Вызвать ошибку, если существуют осиротевшие xdes
IF (@orphanedIdsCount > 0)
BEGIN
DECLARE @msg NVARCHAR(4000) =
@sideTable + N' : ' + CONVERT(NVARCHAR(30), @orphanedIdsCount);
RAISERROR (@msg, 16, 1) WITH NOWAIT;

DECLARE @newLine NVARCHAR(10) = CHAR(13) + CHAR(10)
PRINT (@newLine)
END




-- Перекрестная проверка: ни один xdes не должен существовать в syscommittab
-- !!!ВАЖНО!!! Вызвать ошибку и остановить очистку, если это произошло
SET @sql = N'
DECLARE @nonMatchingXdesCount BIGINT;


SELECT @nonMatchingXdesCount = COUNT_BIG(*)
FROM #OrphanedXdes AS ct
WHERE EXISTS (
SELECT 1
FROM sys.syscommittab AS s
WHERE s.xdes_id = ct.sys_change_xdes_id
);


-- SELECT @nonMatchingXdesCount as nonMatchingXdesCount

IF (COALESCE(@nonMatchingXdesCount, 0)  > 0)
BEGIN TRY
DECLARE @msg NVARCHAR(1024);
SET @msg = N''Cleanup aborted: orphan cross-check failed for side table [' + @sideTable + N'].'';
RAISERROR(@msg, 16, 1) WITH NOWAIT;
RETURN;
END TRY
BEGIN CATCH
THROW;
END CATCH
';


EXEC sys.sp_executesql @sql;


IF (@orphanedIdsCount > 0)
BEGIN
-- Подготовить запрос для удаления осиротевших строк из вспомогательной таблицы
SET @sql = N'DELETE ct FROM sys.' + @sideTable + N' ct WHERE EXISTS (SELECT 1 FROM #OrphanedXdes AS o WHERE o.sys_change_xdes_id = ct.sys_change_xdes_id);';


SELECT @sql -- проверить, что запрос на удаление сгенерирован правильно
-- Пример инструкции удаления: DELETE ct FROM sys.change_tracking_1221579390 ct WHERE EXISTS (SELECT 1 FROM #OrphanedXdes AS o WHERE o.sys_change_xdes_id = ct.sys_change_xdes_id);


-- ПРИМЕЧАНИЕ: Раскомментируйте приведенный ниже запрос, чтобы выполнить инструкцию удаления и удалить осиротевшие записи
-- EXEC sys.sp_executesql @sql;
END


DROP TABLE IF EXISTS #OrphanedXdes;


    FETCH NEXT FROM ct_cursor INTO @sideTable;
END


CLOSE ct_cursor;
DEALLOCATE ct_cursor;


SET NOCOUNT OFF

Почему "перекрестная проверка с прерыванием" — это хорошая идея

Обратите внимание на защитную проверку:

  • Вы сначала определяете осиротевшие записи как те, которые не существуют в sys.syscommittab (для горизонта очистки).
  • Затем вы перепроверяете: "Если какая-либо из них появляется в syscommittab, прервать выполнение".

Это предотвращает случайное удаление, если:

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

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

Как интерпретировать вывод

  • Если вы не видите строк RAISERROR, скрипт не обнаружил осиротевших строк в соответствии с заданными критериями.
  • Если вы видите: change_tracking_<id> : <count> это указывает на <count> осиротевших ссылок на транзакции в этой вспомогательной таблице отслеживания изменений. Это тот же стиль, который использовался в Части 1 для длительного потокового вывода прогресса.

Следующие шаги (рекомендуемый порядок)

  1. Подтвердите конфигурацию отслеживания изменений (период хранения + статус AUTO_CLEANUP), используя официальные рекомендации.
  2. Запустите обнаружение из Части 1 / Части 2, чтобы количественно оценить масштаб (какие вспомогательные таблицы, сколько).
  3. Если вам нужно провести исправление:
    • Предпочитайте поддерживаемые меры смягчения, где это возможно (например, отключение/включение отслеживания изменений для таблицы, чтобы очистить метаданные отслеживания для этой таблицы, указано как "самое быстрое средство" в Microsoft Learn для определенных сценариев конфликтов блокировок при очистке).
    • Если отключение/включение на уровне таблицы неприемлемо, используйте подход, основанный на утверждении, для целевой очистки.

Заключение

Осиротевшие записи во вспомогательных таблицах отслеживания изменений — это одно из тех состояний "тихого роста", которое легко пропустить, пока не станет болезненным либо использование хранилища, либо производительность CHANGETABLE. Часть 1 помогает вам обнаружить проблему на ранней стадии; Часть 2 помогает подготовить безопасный, целевой рабочий процесс очистки — с явными защитными проверками и шагом удаления, который остается отключенным по умолчанию.

Ссылки

1.3.26

Выявление потерянных записей в Change Tracking (часть 1)

Автор: Mohamed_Baioumy_MSFT, Identifying Orphaned Records in Change Tracking Side Tables (Read‑Only Health Check)

Когда в SQL Server включено отслеживание изменений (CT, Change Tracking), компонент ядра СУБД сохраняет облегчённые метаданные об изменениях, чтобы приложения могли запрашивать: "что изменилось, начиная с версии X?". Внутреннее устройство CT подразумевает наличие:

  • Вспомогательной таблицы для каждой отслеживаемой пользовательской таблицы. В ней хранятся метаданные изменений на уровне строк (включая идентификатор транзакции).
  • Таблицы фиксации (commit table), в которой хранятся зафиксированные транзакции, затронувшие любую таблицу с включенным отслеживанием изменений. Динамическое административное представление (DMV) sys.dm_tran_commit_table отображает объединение частей этой таблицы фиксации, находящихся в памяти и на диске.

При нормальной работе очистка CT удаляет устаревшие строки из вспомогательных таблиц, а затем — соответствующие более старые строки из таблицы фиксации, основываясь на настроенном периоде хранения. Внутренняя логика очистки использует "безопасную точку очистки", получаемую из настроенного периода хранения, и сопоставляет это астрономическое время с версией CT с помощью системной хранимой процедуры sys.sp_changetracking_time_to_csn.

27.2.26

Как фильтрованные индексы кардинально улучшают производительность запросов

Автор: Luca Biondi, SQL Server Performance Tuning: How Filtered Indexes Drastically Improve Query Performance

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

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

В этой статье мы проанализируем, как фильтрованные индексы в SQL Server могут уменьшить ввод-вывод, оптимизировать планы выполнения и значительно повысить производительность OLTP.

26.2.26

Обнаружение длинных цепочек IAM

Автор: Paul Randal, The Curious Case of… finding long IAM chains

В предыдущей статье Любопытный случай периодического сбоя запроса к крошечной таблице я описал проблему, с которой Джонатан столкнулся у клиента: очень длинные цепочки IAM и обстоятельства, к ним приведшие.

Вопрос заключался в том, как доказать, что некоторые единицы распределения имеют длину цепочки IAM, непропорциональную объёму данных в единице распределения, без утомительного прохода по каждой цепочке IAM, начиная с первой IAM-страницы (чей идентификатор всегда хранится во внутренней таблице sys.allocation_units).

25.2.26

Как оптимизировать переключение реплик групп доступности SQL Server

Автор: Aaron Bertrand , Serializing Deletes From Clustered Columnstore Indexes

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

Сокращение даже нескольких секунд из этого процесса может улучшить взаимодействие с приложением и конечным пользователем; это также может значительно снизить количество оповещений или, по крайней мере, сократить время, в течение которого оповещения должны быть отключены. Существует множество материалов о том, как правильно выполнять переключения в AG (без потери данных), но гораздо меньше тех, которые сосредоточены на сокращении окна прерывания доступности. Разница обычно заключается в некоторой комбинации объёма повторного выполнения (redo), поведения контрольных точек, открытых транзакций и готовности вторичной реплики.

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

24.2.26

Введение в транзакции SQL Server

Автор: Paul Randal, SQL101: Introduction to SQL Server Transactions

Одним из фундаментальных понятий в любой реляционной системе управления базами данных (СУБД), такой как SQL Server, является транзакция. За свою консультационную карьеру я видел множество случаев проблем с производительностью, вызванных тем, что разработчики не понимают, как работают транзакции в SQL Server, поэтому в этом учебном руководстве я объясню, что такое транзакции и почему они необходимы, а также некоторые детали их работы в SQL Server. В использовании всего этого есть нюансы, когда задействовано Ускоренное восстановление базы данных (ADR) — темы для будущих статей.

20.2.26

Журнал транзакций SQL Server, Часть 2: Архитектура журналирования

Автор: Paul Randal, The SQL Server Transaction Log, Part 2: Log Architecture

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

19.2.26

Опровержение мифов о FILESTREAM

Автор: Paul Randal, A SQL Server DBA myth a day: (18/30) FILESTREAM storage, garbage collection, and more

Данные FILESTREAM не могут храниться удалённо

Контейнер данных FILESTREAM (придуманное название для структуры каталогов NTFS, в которой хранятся данные FILESTREAM) должен подчиняться тем же правилам локальности, что и обычные файлы данных и журнала базы данных – т.е. он должен быть размещён на хранилище, 'локальном' для сервера Windows, на котором работает SQL Server. К данным FILESTREAM можно получить доступ, используя UNC-путь, при условии, что клиент связался с локальным SQL Server и получил необходимый контекст транзакции для использования при открытии файла FILESTREAM.

17.2.26

Любопытный случай периодического сбоя запроса к крошечной таблице

Автор: Paul Randal, The Curious Case of… occasional query failure on a tiny table

Это случай, произошедший в прошлом году в системе одного клиента: иногда обычный запрос к крошечной таблице, казалось, «зависал», и его приходилось убивать и запускать заново. В чём же дело?

Рассматриваемая таблица содержала всего несколько миллионов строк данных с максимальным размером строки 60 байт, и запрос обычно выполнялся за несколько секунд, но иногда он «зависал» и либо его приходилось убивать, либо он выполнялся десятки минут. Диагностические средства, запущенные во время возникновения проблемы, не показывали каких-либо необычных ожиданий, давления на сервер не было, а план запроса, генерируемый при долгом выполнении, был практически таким же.

16.2.26

Структуры хранения #3 – In-Memory OLTP

Автор: Hugo Kornelis, Storage structures 3 – Memory-optimized;

После обсуждения традиционного хранения строк на диске (rowstore) в части 1 и колоночных хранилищ (columnstores) в части 2, пришло время обратить наш взгляд в SQL Server на структуры хранения, оптимизированные для памяти.

Оптимизированное для памяти хранение было представлено в SQL Server 2014 в рамках проекта, который имел кодовое название «Hekaton» и позже был переименован в in-memory OLTP. В то время как колоночные индексы были специально нацелены на крупномасштабную аналитическую работу, Hekaton и оптимизированные для памяти таблицы специально предназначены для высоконагруженных OLTP-нагрузок. Полностью устраняя блокировки обычные и краткие и используя предварительно скомпилированный машинный код, где это возможно, время обработки транзакций значительно сокращается, что позволяет достичь пропускной способности, ранее недостижимой.

Название «оптимизированные для памяти» выбрано очень осознанно. Эта функция не просто заменяет дисковое хранилище на хранилище в памяти. Сама структура данных была полностью переработана, чтобы извлечь выгоду из скорости современной памяти и обеспечить безопасный параллельный доступ без использования блокировок или защёлок.

Обратите внимание, что оптимизированные для памяти колоночные индексы, доступные с SQL Server 2016, будут описаны в отдельной статье. Эта статья посвящёна оптимизированным для памяти индексам rowstore.

15.2.26

Разнообразные мифы о контрольных суммах страниц

Автор: Paul Randal, A SQL Server DBA myth a day: (17/30) page checksums

Несколько человек предложили некоторые мифы о контрольных суммах страниц, так что сегодня ещё одно мульти-разоблачительное представление! Ну, по крайней мере, я взволнован :-)

Я подробно описал контрольные суммы страниц в посте в блоге «How to tell if the IO subsystem is causing corruptions?»

14.2.26

Контрольная точка записывает только страницы из зафиксированных транзакций?

Автор: Paul Randal, A SQL Server DBA myth a day: (15/30) checkpoint only writes pages from committed transactions

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

ЛОЖЬ

Этот миф существует целую вечность и связан с непониманием того, как работает общая система журналирования и восстановления. Контрольная точка всегда записывает все страницы, которые были изменены (так называемые «грязные» страницы) с момента последней контрольной точки или с момента считывания страницы с диска. Не имеет значения, зафиксирована транзакция, изменившая страницу, или нет – страница записывается на диск в любом случае. Единственным исключением является tempdb, где страницы данных не записываются на диск в рамках контрольной точки.