29.12.25

Заблуждения о перестроении индексов (распределение, режим BULK_LOGGED, блокировки)

Автор: Paul Randal, Search Engine Q&A #19: Misconceptions around index rebuilds (allocation, BULK_LOGGED mode, locking)

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

26.12.25

Любопытный случай… немедленной записи и минимально логируемых операций

Автор: Paul Randal, The Curious Case of… eager writing and minimally-logged operations

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

Оказывается, это не так.

25.12.25

Любопытный случай… блокировки BULK_OPERATION при просмотре кучи (heap) с NOLOCK

Автор: Paul Randal, The Curious Case of… the BULK_OPERATION lock during a heap NOLOCK scan

Мне пришёл вопрос по электронной почте: один специалист заметил, что во время выполнения просмотра кучи с использованием NOLOCK на всю продолжительность операции на куче удерживалась блокировка BULK_OPERATION. Вопрос заключался в том, зачем нужна эта блокировка, ведь просмотр с NOLOCK, казалось бы, не должно читать проблемные страницы?

Ответ заключается в том, что эта дополнительная блокировка нужна именно потому, что сканирование с NOLOCK может прочитать проблемную страницу, если в тот же момент над кучей выполняется операция массовой загрузки (bulk operation).

24.12.25

Небольшая ошибка в столбце sample_ms в sys.dm_io_virtual_file_stats

Автор: Paul Randal, Small bug with sample_ms in sys.dm_io_virtual_file_stats

Мне написал бывший студент по поводу странного поведения столбца sample_ms в представлении sys.dm_io_virtual_file_stats. Предполагается, что он должен содержать количество миллисекунд, прошедших с момента запуска экземпляра SQL Server. У него есть экземпляр SQL Server 2016, работающий с августа 2019 года, и там отображается следующее:

  • ms_ticks из sys.dm_os_sys_info: 51915112684 (что соответствует 28 августа 2019 года)
  • sample_ms из sys.dm_io_virtual_file_stats: 375504432 (что составляет около 4,5 дней)

Очевидно, что внутренняя переменная в SQL Server несколько раз переполнилась или сбросилась, вероятно, при достижении 2^31 или 2^32, хотя начиная с SQL Server 2016 тип данных sample_ms должен быть bigint.

23.12.25

Любопытный случай… неубиваемого потока

Автор: Paul Randal, The Curious Case of… the un-killable thread

На прошлой неделе, преподавая курс IEPTO2, я объяснял, почему иногда поток невозможно завершить с помощью команды KILL, и подумал, что это отличная тема для статьи.

22.12.25

Любопытный случай… с object ID 99

Автор: Paul Randal, The Curious Case of… object ID 99

Сегодня я отвечал на вопрос под тегом #sqlhelp в Twitter и упомянул об использовании значения object ID 99, потому что SQL Server никогда не присвоит таблице идентификатор объекта 99. И я подумал, что это станет хорошей темой для небольшой записи в блоге.

21.12.25

Любопытный случай… журнал, генерируемый при выполнении DROP TABLE

Автор: Paul Randal, The Curious Case of… log generated during a DROP TABLE

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

Надеюсь, все вы знаете, что мифом является утверждение, будто операции DROP TABLE и TRUNCATE TABLE не журналируются. Если вы этого не знали, прочитайте мою запись в блоге на sqlperformance.com, где объясняется механизм отложенного удаления (deferred drop). Обе операции полностью журналируются и будут генерировать довольно много записей журнала транзакций.

Основная часть генерируемого журнала возникает из-за необходимости фиксировать в журнале освобождение экстентов и страниц внутри них. Для каждого экстента должен быть сброшен бит на соответствующей странице GAM и странице IAM, а все 8 страниц в экстенте должны быть помечены как освобождённые на соответствующей странице PFS (путем выключения бита 0x40 в каждом байте PFS). Таким образом, получается три записи журнала на каждый распределённый экстент.

Я быстро подготовил тестовый пример, чтобы показать вам записи журнала:

SELECT
[Current LSN],
[Operation],
[Context],
[Log Record Length],
[Description]
FROM fn_dblog (null, null);
Current LSN             Operation       Context  Length  Description
----------------------  --------------  -------- ------- ------------------------------------------------------
.
.
000001eb:00000010:016a  LOP_SET_BITS    LCX_IAM  72
000001eb:00000010:016b  LOP_MODIFY_ROW  LCX_PFS  88      Deallocated 0001:000026f0;Deallocated 0001:000026f1;Deallocated 0001:000026f2;Deallocated 0001:000026f3;Deallocated 0001:000026f4;Deallocated 0001:000026f5;Deallocated 0001:000026f6;Deallocated 0001:000026f7
000001eb:00000010:016c  LOP_SET_BITS    LCX_GAM  72      Deallocated 1 extent(s) starting at page 0001:000026f0
.
.

И этот набор из трёх записей журнала повторяется для каждого освобождаемого экстента.

Одно из обсуждений касалось удаления таблицы размером 20 ТБ. 

20 ТБ = 20 * 1024 * 1024 *1024 * 1024 = 21 990 232 555 520 байт. 

Один экстент — это 65 536 байт, поэтому 20 ТБ — это 21 990 232 555 520 / 65 536 = 335 544 320 экстентов. При 72 + 88 + 72 = 232 записываемых байта на экстент это означает, что удаление таблицы размером 20 ТБ приведёт к генерации как минимум 335 544 320 * 232 = 77 846 282 240 байт = 72,5 ГБ журнала транзакций, или примерно 0,35% от размера таблицы. Периодически генерируется небольшое количество других записей журнала, поэтому для надёжности я бы сказал, что это 0,35–0,4%.

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

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

Суть в том: выполнение DROP или TRUNCATE для таблицы требует генерации в журнале примерно 0,35–0,4% от размера таблицы.



20.12.25

Любопытный случай… карты прерванных XDES

Автор: Paul Randal, The Curious Case of… the aborted xdes map

На прошлой неделе мне по электронной почте задали вопрос о странных сообщениях в журнале ошибок, похожих на эти:

[DbId:12]. Removing xdes id: 0000:8112c9d0 from aborted xdes map.
[DbId:12]. Removing xdes id: 0000:8112b45e from aborted xdes map.
[DbId:12]. Removing xdes id: 0000:8112b403 from aborted xdes map.

Прежде всего, XDES — это структура данных (и класс C++) в механизме хранения (Storage Engine), представляющий транзакцию; это сокращение от «transaction descriptor» (дескриптор транзакции). Идентификатор XDES — это тот же самый идентификатор, который вы видите в столбце Transaction ID в выводе функции fn_dblog для изучения содержимого журнала транзакций (см. примеры здесь).

Эти сообщения безвредны и связаны с внутренней работой функции Ускоренного восстановления баз данных (Accelerated Database Recovery, ADR), которая позволяет мгновенно откатывать транзакции с помощью умного механизма версионности.

19.12.25

Любопытный случай… ограничения размера строки в 8060 байт

Автор: Paul Randal, The Curious Case of… the 8060-byte row size limit

На прошлой неделе мне задали вопрос о том, почему существует ограничение в 8060 байт на размер строки и почему страницы файлов данных иногда показывают более 8060 байт свободного места при просмотре с помощью DBCC PAGE.

Прежде чем объяснять, позвольте мне прояснить, что ограничение в 8060 байт касается только той части записи, которая хранится на странице данных или индекса в единице распределения «внутристроковых данных» (in-row data). Запись может иметь множество столбцов LOB, которые хранятся вне строки в единице распределения «данных LOB» (например, столбцы varchar(max) или FILESTREAM), и/или не-LOB столбцов переменной длины, которые были вытеснены из строки для хранения в единице распределения «переполнения строки» (например, столбцы char(1-8000), nchar(1-4000) или sqlvariant). Таким образом, фактический размер строки практически не ограничен.

Я мог бы перефразировать исходный вопрос так: учитывая, что размер страницы составляет 8192 байта, и только 96 байт используется для заголовка страницы, почему размер внутристроковой записи ограничен 8060 байтами? Куда деваются остальные 36 байт доступного пространства?

18.12.25

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

Автор: Paul Randal, The Curious Case of… restoring a table into a different database

(«Любопытный случай…» раньше был частью нашей двухнедельной рассылки, но мы решили сделать его обычной записью в блоге, чтобы он мог выходить иногда чаще. Он рассказывает о чём-то интересном, с чем столкнулся один из нас при работе с клиентом, проведении тестов или в случайном вопросе от сообщества.)

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

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

17.12.25

Ленивое усечение журнала, или почему VLF могут оставаться со статусом 2 после очистки журнала

Автор: Paul Randal, Lazy log truncation or why VLFs might stay at status 2 after log clearing

Ранее мне прислали интересный вопрос о том, почему человек видит множество VLF (виртуальных файлов журнала) в журнале со статусом = 2 (что означает «активный») после очистки (также известной как «усечение») журнала, при том что log_reuse_wait_desc показывает NOTHING.

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

16.12.25

Новое значение статуса VLF

Автор: Paul Randal, New VLF status value

По крайней мере с тех пор, как я начал работать в команде SQL Server (сразу после выхода версии 7.0) и до недавнего времени, существовало всего два кода статуса VLF (виртуального файла журнала):

  • 0 = VLF не активен (т.е. его можно (повторно) активировать и перезаписать)
  • 1 = не используется, и, кажется, никто не помнит, что это раньше означало
  • 2 = VLF активен, потому что хотя бы одна запись журнала в нём «требуется» SQL Server по какой-либо причине (например, ещё не была включена в резервную копию журнала или не просканирована репликацией)

Несколько недель назад я узнал о новом коде статуса VLF, который был добавлен ещё в SQL Server 2012, но не был широко известен до недавнего времени (по крайней мере, я никогда с ним не сталкивался в реальных условиях). Я провёл несколько обсуждений с другом из Microsoft (Шоном Галларди, PFE и MCM из Тампа), которому удалось покопаться в коде, чтобы выяснить, когда он используется.