Как выполнить отладку медленного SQL-запроса?

Ответ

Отладка медленного запроса — это системный процесс. Вот мой подход, основанный на практике с C# и SQL Server:

  1. Сбор данных:

    • Включаю логирование запросов в EF Core для захвата сгенерированного SQL.
      optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
    • Использую SQL Server Profiler, Extended Events или запросы к динамическим административным представлениям (DMV), например sys.dm_exec_query_stats, чтобы найти самые затратные запросы.
  2. Анализ плана выполнения: Получаю и анализирую Actual Execution Plan в SSMS (Ctrl+M). Ищу:

    • Table/Index Scans вместо Index Seeks.
    • Key Lookups (RID Lookups) — часто указывают на недостающий покрывающий индекс.
    • Высокую стоимость операций сортировки (Sort), хэширования (Hash Match) или удаленного сбора данных (Remote Query).
    • Предупреждения (жёлтые значки) о неверных оценках количества строк или отсутствующих индексах.
  3. Оптимизация: Действия зависят от анализа:

    • Добавление/изменение индексов: Чаще всего проблема здесь. Создаю недостающие индексы, покрывающие условия WHERE, JOIN и SELECT.
      CREATE INDEX IX_Orders_CustomerId_Status ON Orders(CustomerId, Status) INCLUDE (TotalAmount);
    • Рефакторинг запроса: Переписываю сложный запрос, разбиваю на части, устраняю N+1 проблему в EF Core с помощью Include() и Select().
    • Использование возможностей ORM: Применяю AsNoTracking() для операций только для чтения, AsSplitQuery() для избежания избыточных данных в некоторых сценариях.
    • Кэширование: Для редко меняющихся данных рассматриваю кэширование результатов запроса (например, с помощью IMemoryCache или IDistributedCache).
  4. Тестирование: Замеряю производительность до и после изменений с помощью Stopwatch или сравниваю метрики в плане выполнения (логические чтения, затраченное время).

Ответ 18+ 🔞

Ну вот, опять этот запрос тормозит как черепаха в патруле! Ладно, давай по шагам разберём, как я обычно эту хрень чиню. Запоминай, а то потом опять будешь в логах ковыряться, как слепой котёнок.

Первым делом, надо понять, что эта подзатыльниковая SQL-фраза вообще делает. В EF Core это просто — включаешь логирование и смотришь, какую дичь он на сервер отправляет.

optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

Запускаешь — и вуаля, вся эта срань вываливается в консоль. Смотришь на запрос и думаешь: «Ну и кто это так писал, ёпта?» Там обычно сразу видно, если он десять раз к одной таблице лезет или всю базу на SELECT * выгружает.

Дальше идём в святая святых — SQL Server Management Studio. Берём этот кривой запрос, включаем в меню «Включить фактический план выполнения» (Ctrl+M, запомни уже, блин) и запускаем. Он выполнится, и внизу появится вкладка с планом — вот тут-то и начинается цирк.

Смотришь на эту цветную схему и ищешь, где самые толстые стрелки. Где стрелка жирная — там и проблема сидит, ядрёна вошь! Чаще всего видишь три главных косяка:

  1. Table Scan / Index Scan. Это когда он, вместо того чтобы по аккуратному индексу пройтись, начинает читать ВСЮ таблицу с самого начала, как последний распиздяй. Прям вот всю, до последней строчки. Представь, что тебе нужно найти одну бумажку в архиве, а ты начинаешь листать все папки подряд. Вот это оно.

  2. Key Lookup (или RID Lookup). А это вообще шедевр. Он сначала по индексу быстро находит нужные строчки (хорошо!), а потом — бац! — идёт по каждой найденной строчке обратно в кучу, чтобы достать ещё пару полей, которых нет в индексе. Это как найти в оглавлении книги нужную страницу, а потом бегать к полке за каждой главой по отдельности. Задолбаешься.

  3. Жёлтые восклицательные знаки. Это SQL Server тебе лично намекает: «Чувак, тут нихуя не так, смотри!» Наводишь курсор — а там пишет: «Отсутствующий индекс» или «Неправильная оценка количества строк». Сервер сам тебе подсказывает, что создать! Вот же мудя, золотой!

Ну, допустим, нашли проблему. Чаще всего — это индексы, их нет или они кривые. Смотришь, по каким полям в запросе WHERE или JOIN. Берёшь и создаёшь индекс, чтобы он покрыл и поиск, и те поля, которые в результате нужны.

CREATE INDEX IX_Orders_CustomerId_Status ON Orders(CustomerId, Status) INCLUDE (TotalAmount);

Вот смотри: по CustomerId и Status ищем, а TotalAmount просто выводим. Так и запихиваем его в INCLUDE, чтобы lookup'а не было. Иногда после такого индекса запрос начинает летать, просто праздник какой-то.

Если не в индексах дело, значит, сам запрос — говно. Особенно если используешь Entity Framework и не следишь за ним. Он может раздуть один запрос в двадцать маленьких (это N+1 проблема, гугли). Тут спасают Include() и проекции (Select()), чтобы сразу всё нужное подтянуть, а не дергать базу как дурную.

И ещё, если данные только читаешь и не собираешься менять, всегда ставь AsNoTracking(). Это EF-шнику сигнал: «Расслабься, чувак, не следи за этими объектами, не кэшируй их у себя». Работает быстрее, память не засоряет.

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

И главное — не верь на слово! После всех оптимизаций замерь время по-нормальному, через Stopwatch, или сравни логические чтения в том же плане выполнения. Было 100 тысяч, стало 500 — вот это результат! А то бывает, накрутишь индексов, а скорость как была полсекунды, так и осталась, потому что проблема была в другом. В общем, думай головой, а не просто тыкай наугад.