В куче обновление строк переменной длины резко замедлило чтение по некластеризованному индексу. Какой механизм это объясняет?
В куче SQL Server увеличение строки, которому не хватает места на исходной странице, может переместить строку на другую страницу и оставить на старом месте forwarding pointer. Некластеризованный индекс продолжает ссылаться на исходный RID, поэтому чтение по индексу требует дополнительного перехода к новой записи.
Это увеличивает число логических и физических обращений к страницам, ухудшает локальность данных и может сделать индексные lookup-операции значительно дороже.
Куча хранит строки без кластеризованного индекса, а некластеризованные индексы используют физический RID как указатель на строку. Такой подход удобен для простых загрузок и таблиц, которым не нужен заданный порядок хранения.
Проблема возникает при обновлении строки переменной длины: если новая версия не помещается на прежнюю страницу, перемещение всей строки потребовало бы изменять ссылку в каждом некластеризованном индексе. Forwarding pointer позволяет сохранить прежний RID и избежать массового обновления указателей.
После серии обновлений varchar- или varbinary-полей план запроса может остаться прежним, но фактическое выполнение замедлится. Особенно заметен эффект у запросов, которые находят немного строк по некластеризованному индексу и затем выполняют для каждой строки lookup в кучу.
Неверно считать, что наличие индексного поиска гарантирует низкую стоимость. Если многие найденные RID ведут сначала к forwarding pointer, один логический поиск превращается в несколько обращений к страницам.
При перемещении строки SQL Server оставляет на прежнем месте специальную запись-переадресацию, а полную строку размещает на другой странице. Некластеризованный индекс по-прежнему указывает на старую позицию, поэтому подсистема хранения сначала читает forwarding pointer, затем переходит к фактической строке.
Пример сценария для SQL Server:
Само перемещение зависит от свободного места на страницах, поэтому пример не гарантирует появление forwarding pointer для каждой строки. Диагностировать проблему можно по счётчикам forwarded records и характеристикам таблицы, используя средства конкретной СУБД, например динамические представления SQL Server.
Основные варианты исправления:
Компромисс выбирают по характеру нагрузки. Для часто изменяемых строк превращение кучи в таблицу с подходящим кластеризованным индексом обычно стабильнее, а для редких обновлений достаточно обслуживания кучи и пересмотра дорогих lookup-операций.
В таблице событий был некластеризованный индекс по идентификатору, а полезная нагрузка хранилась в изменяемом поле. После обогащения событий размер этого поля вырос, и запрос по небольшому диапазону идентификаторов стал выполнять множество lookup-операций с дополнительными переходами по forwarding pointers.
Рассматривались перестроение некластеризованного индекса, периодическое обслуживание кучи и создание кластеризованного индекса. Перестроение некластеризованного индекса само по себе не удаляет forwarding records, а регулярное обслуживание временно исправляет состояние и создаёт дополнительную нагрузку.
Выбрали кластеризованный индекс по стабильному идентификатору, потому что таблица часто читалась по этому ключу, а его значения почти не изменялись. Это устранило зависимость от forwarding pointers; цена решения — дополнительное место и необходимость учитывать стоимость поддержания кластерной структуры при вставках.
Устраняет ли перестроение некластеризованного индекса forwarding records?
Нет. Некластеризованный индекс может быть перестроен, но forwarding pointers являются свойством хранения строк в куче. Для их устранения требуется перестроить саму кучу либо изменить организацию таблицы, например создать кластеризованный индекс.
Почему forwarding records особенно вредны для lookup, но не всегда одинаково заметны при полном сканировании?
Lookup часто обращается к отдельным страницам для каждой найденной строки, поэтому дополнительный переход ухудшает случайный доступ и увеличивает число чтений. При полном сканировании страницы кучи и так последовательно просматриваются; дополнительная запись может быть прочитана в рамках этого процесса, хотя перемещение строк всё равно ухудшает локальность и увеличивает объём работы.
Может ли заполнение страниц предотвратить forwarding records?
Наличие свободного места на странице уменьшает вероятность перемещения строки при её расширении, но не гарантирует его отсутствия. Слишком большое резервирование пространства увеличивает размер таблицы и объём чтения, а дальнейшие обновления могут превысить доступный запас. Поэтому настройку заполнения следует рассматривать вместе с характером обновлений, размером строк и реальными планами выполнения.