Какая сложность запросов в хранимых процедурах
Что проверяют
Понимаете ли вы, что хранимая процедура сама по себе не делает запрос ни быстрее, ни медленнее: сложность определяет план выполнения. Следом обычно спрашивают, как вы нашли бы медленную процедуру и что такое parameter sniffing.
Как отвечать
Переведите вопрос на язык плана. В SQL Server у операторов понятные оценки:
- Index Seek: спуск по B-дереву, O(log n) плюс число найденных строк;
- Index Scan или Table Scan: чтение всего индекса или таблицы, O(n);
- Nested Loops: для каждой строки внешнего входа поиск во внутреннем. С индексом O(n·log m), без индекса O(n·m);
- Hash Match: хэш-таблица по меньшему входу, O(n + m), но нужна память, при нехватке идёт сброс в
tempdb; - Merge Join: O(n + m) на отсортированных входах, иначе добавляется сортировка.
Особенность процедур: план компилируется при первом вызове под значения его параметров и кэшируется. Если первым пришёл клиент с тремя заказами, а потом клиент с миллионом, план с Nested Loops станет катастрофой. Лечат OPTION (RECOMPILE), OPTIMIZE FOR или разными процедурами под разные сценарии. Второе: курсоры и циклы WHILE превращают один запрос в n запросов, отсюда квадратичное время там, где хватило бы одного JOIN.
Как измерить: фактический план, SET STATISTICS IO ON для логических чтений, Query Store для истории. Вызов из C# через SqlCommand с CommandType.StoredProcedure, Dapper или FromSql в EF Core на сложность не влияет.
Где спотыкаются
Называют O(1) для «выборки фиксированного числа строк». TOP 10 с ORDER BY без подходящего индекса читает и сортирует всю таблицу.
Утверждают, что процедура всегда быстрее параметризованного запроса из EF Core. Планы параметризованных запросов тоже кэшируются, выигрыш процедуры в другом: меньше обмена по сети и отдельные права доступа.
Приводят проценты ускорения без метода замера. Сильнее звучит так: «логических чтений было два миллиона, после замены курсора на JOIN стало сорок тысяч».