[Перевод] Индексы в базах данных: сколько индексов — перебор?
Вопрос оптимального количества индексов часто становится предметом горячих дискуссий среди разработчиков и администраторов баз данных. Одни утверждают, что больше индексов означает лучшую производительность, другие предупреждают о рисках избыточности и снижении эффективности операций записи. Но как определить золотую середину?
Далее предлагаем вашему вниманию перевод оригинальной статьи «How Many Indexes Is Too Many?», который подготовила специалист «Автомакона». В статье детально рассматривается данная проблема и приводятся практические рекомендации по выбору подходящего количества индексов для повышения производительности.
Для начала давайте рассмотрим простой эксперимент. Возьмем популярную базу данных Stack Overflow любого размера, уберем все индексы из таблицы Users и запустим удаление одной строки командой DELETE. Это позволит нам наглядно увидеть влияние отсутствия индексов на выполнение данной операции и измерить количество физических операций ввода-вывода с помощью команды SET STATISTICS IO ON:
1 2 3 4 | SET STATISTICS IO ON; GO BEGIN TRAN DELETE dbo.Users WHERE DisplayName = N’Brent Ozar'; |
Мы будем повторять операцию в рамках транзакции, чтобы иметь возможность отменить внесённые изменения и оценить последствия каждого шага:

Мы читаем это справа налево. Первое, что должен сделать SQL Server, — это сканировать всю таблицу Users, чтобы найти строку (или строки) с DisplayName = 'Brent Ozar', потому что у нас нет индекса на DisplayName. После поиска он удалит их из кластеризованного индекса.
В копии Stack Overflow за июнь 2018 года SQL Server должен прочитать 143 670 страниц по 8 КБ каждая, чтобы найти нужные ему строки:

Нам нужен индекс на DisplayName.
Мы хотим, чтобы наше удаление работало быстрее, и нам нужно быстро находить строки, где DisplayName = 'Brent Ozar'. Для этого давайте отменим удаление и создадим индекс:
1 2 | ROLLBACK CREATE INDEX DisplayName ON dbo.Users (DisplayName); |
Then try our delete again:
1 2 | BEGIN TRAN DELETE dbo.Users WHERE DisplayName = N’Brent Ozar'; |
Now, the actual execution plan is simpler:

Рассмотрим пошагово, как SQL Server обрабатывает запрос справа налево: сначала открывается индекс по полю DisplayName, затем точно определяется нужная строка, и только потом осуществляется её удаление непосредственно из основного кластерного индекса. Важно отметить, что визуализация плана выполнения слегка упрощает картину. Если внимательно изучить оператор удаления в кластерном индексе, можно заметить интересную деталь: внизу всплывающего окна отображаются сразу два объекта, а не один, как могло бы показаться изначально.

Внизу говорится: «Объект: PK_Users_Id» (это кластерный индекс) и «DisplayName» (это наш новый некластерный индекс). Тем не менее, теперь объем работы значительно меньше, что подтверждается временем выполнения запроса и логическими чтениями:

Мы сократили количество логических чтений с 143670 до всего лишь 12 страниц!
Этот индекс помог. Мы должны добавить больше!
Когда клиенты обращаются ко мне с проблемами производительности, обычно это одна из двух крайностей: либо они никогда не открывали для себя магию хороших некластерных индексов, либо они чрезмерно увлеклись ими, добавив кучу таких индексов. Давайте добавим несколько дополнительных индексов, а затем снова запустим операцию удаления:
1 2 3 4 | CREATE INDEX Location ON dbo.Users (Location); CREATE INDEX LastAccessDate ON dbo.Users (LastAccessDate); CREATE INDEX Reputation ON dbo.Users (Reputation); CREATE INDEX WebsiteUrl ON dbo.Users (WebsiteUrl); |
Now, our delete«s actual execution plan looks the same, but…

Графический план скрывает секрет, и чтобы раскрыть его, нам нужно навести указатель мыши на оператор «Удаление кластерного индекса», который является злостной ложью:

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

Мы перешли от 12 логических чтений до 24. Теперь, большое ли дело 24 чтения по 8 КБ? Совсем нет! Вы должны чувствовать себя абсолютно комфортно, добавляя горсть индексов к большинству таблиц, когда вашим рабочим нагрузкам этих запросов требуются эти индексы.
Однако, чем больше вы добавляете…
Давайте добавим еще несколько индексов, каждый из которых содержит несколько включенных столбцов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | CREATE INDEX Age ON dbo.Users (Age) INCLUDE (DisplayName, Location, Reputation); CREATE INDEX CreationDate ON dbo.Users (CreationDate) INCLUDE (DisplayName, Location, Reputation); CREATE INDEX DownVotes ON dbo.Users (DownVotes) INCLUDE (DisplayName, Location, Reputation); CREATE INDEX UpVotes ON dbo.Users (UpVotes) INCLUDE (DisplayName, Location, Reputation); CREATE INDEX EmailHash ON dbo.Users (EmailHash) INCLUDE (DisplayName, Location, Reputation); |
And then try our delete again. The actual execution plan still looks simple:

But the logical reads keep inching up:

Это все еще не так уж плохо — и в большинстве случаев ваша рабочая нагрузка и оборудование, вероятно, вполне справляются с 5, 10, возможно, даже 15 или 20 индексами. В конце концов, сохраняйте перспективу: это все равно намного лучше, чем 143670 логических чтений, с которых мы начали!
Так сколько индексов будет считаться перебором?
Это вовсе не катастрофа! Скорее всего, ваши рабочие процессы и техника легко выдержат нагрузку в пять, десять, а то и целых двадцать индексов. Вспомним масштаб проблемы: это гораздо лучше, чем стартовые 143 670 логических операций чтения!
Но какой же предел количества индексов считать разумным? Здесь дело вовсе не в магическом числе, ведь всё зависит от вашего конкретного случая:
Возможно, большая часть ваших запросов сводится лишь к операциям чтения.
Рабочий поток может совершенно спокойно обходиться без параллельных транзакций и конфликтов, выполняя операции последовательно и независимо друг от друга.
Ваш сервер способен справляться с нагрузкой практически мгновенно.
Пользователи абсолютно равнодушны к быстродействию операций добавления, изменения или удаления данных, особенно если такие действия выполняются автоматически фоново.
Итак, забудьте о конкретных цифрах и обратите внимание на следующие тревожные сигналы, свидетельствующие о чрезмерном количестве индексов именно для вашей системы:
У ваших коллег возникают жалобы на замедление операций вставки, обновления или удаления данных.
Программы мониторинга сигнализируют о проблемах взаимоблокировки, либо подобные инциденты замечают сами пользователи.
Уже пробовали настроить уровни изоляции транзакций (RCSI или SI), но проблема осталась нерешённой.
При появлении перечисленных признаков настало время прибегнуть к мощному инструменту — свободному open-source скрипту sp_BlitzIndex. Запустив его без аргументов прямо в нужной базе данных, вы получите чёткий отчёт о возможных проблемных местах:
Избыточных индексах-двойниках или близнецах.
Полностью неиспользуемых индексах, которые никак не задействуются системой.
Индексах с огромным объёмом данных, но редкими обращениями к ним.
Оптимизация индексов является ключевым аспектом эффективной работы любой базы данных. Как показало наше исследование, правильное использование индексов способно значительно повысить производительность приложения, сокращая время обработки запросов и снижая нагрузку на систему.
Однако важно помнить, что создание большого количества индексов также имеет свою цену: оно увеличивает накладные расходы на обслуживание, замедляет операции вставки и обновления данных, а также повышает вероятность возникновения конфликтов блокировки.
Следовательно, баланс между производительностью и поддерживаемостью — это задача, требующая тщательного анализа рабочих нагрузок, структуры данных и особенностей используемого оборудования. Регулярное тестирование и мониторинг позволяют выявить излишне созданные или неэффективные индексы, а инструменты вроде sp_BlitzIndex помогают упростить этот процесс.
Помните: правильный подбор индексов — залог стабильной и быстрой работы вашей базы данных.