Программирование SQLИндексы и производительностьИнженер по производительности SQL

В плане запроса одно условие указано как Seek Predicate, а другое — как Predicate. Какое практическое после...

В плане запроса одно условие указано как Seek Predicate, а другое — как Predicate. Какое практическое последствие имеет такое разделение?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

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

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

Постановка проблемы

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

Это приводит к лишним чтениям страниц, обращениям к таблице или кластерному индексу и затратам CPU на фильтрацию. Ошибка в анализе такого плана часто возникает, когда индексный поиск ошибочно воспринимают как доказательство высокой эффективности всего запроса.

Подробное решение

Seek Predicate ограничивает обход B-дерева. Например, в индексе с ключами (Дата, Клиент) условие по дате может задать диапазон поиска, а условие по клиенту не всегда сможет сузить этот диапазон, если оно не соответствует доступному префиксу ключа или имеет неподходящую форму.

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

Разделение не означает автоматически ошибку проектирования. Остаточный предикат может быть вполне приемлемым, если индексный диапазон мал или условие отбрасывает немного строк. Проблема возникает, когда оценка или фактическое количество прочитанных строк значительно больше числа возвращённых строк.

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

Минимальный пример показывает различие между сужением диапазона и последующей фильтрацией:

CREATE INDEX IX_Orders_Date_Customer ON dbo.Orders (OrderDate, CustomerId); SELECT OrderId, Amount FROM dbo.Orders WHERE OrderDate >= '2025-01-01' AND OrderDate < '2025-02-01' AND CustomerId = 42;

В зависимости от СУБД и распределения данных условие по OrderDate может участвовать в поиске диапазона, а условие по CustomerId — проверяться после чтения строк этого диапазона. Индекс с другим порядком ключей может лучше поддержать этот конкретный шаблон, но будет полезен уже для другого набора запросов.

Ситуация из практики

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

Рассматривались три варианта. Полный скан мог быть проще и иногда дешевле индексного доступа, но не решал проблему при более коротких диапазонах. Добавление отдельного индекса по клиенту помогало только запросам, начинающимся с клиента, и могло приводить к дорогим обращениям за столбцами результата. Составной индекс (CustomerId, OrderDate) лучше поддерживал сочетание равенства по клиенту и диапазона по дате, но увеличивал размер индекса и стоимость вставок.

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

Что кандидаты часто упускают

  1. Всегда ли Predicate после Seek означает плохой индекс?

Нет. Если Seek уже возвращает небольшой набор строк, остаточная проверка может быть дешёвой и не требовать изменения схемы. Оценивать нужно фактическое число прочитанных и возвращённых строк, логические чтения, стоимость доступа к таблице и частоту выполнения запроса.

  1. Может ли покрытие индекса устранить остаточный Predicate?

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

  1. Почему оценка оптимизатора важнее одного визуального признака плана?

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