Как разбивать большие запросы на более мелкие части для оптимизации
Что проверяют
Понимаете ли вы, почему большой запрос бывает медленным или опасным: блокировки, раздувание журнала транзакций, память приложения, таймауты. Следом обычно спрашивают, как удалить миллион строк без простоя или как выгрузить таблицу через EF Core и не упасть по памяти.
Как отвечать
Сначала уточните, что значит «большой»: результат, массовое изменение или сложный запрос. Решения разные.
- большой результат: не
ToList()на всё, а потоковое чтение черезAsAsyncEnumerable()сAsNoTracking()или постраничная выгрузка. Для глубоких страниц вместоOFFSET ... FETCHберут keyset-пагинациюWHERE Id > @lastId ORDER BY Id, потому чтоOFFSETвсё равно читает и отбрасывает пропущенные строки; - массовое изменение: в EF Core 7 и новее есть
ExecuteUpdateAsyncиExecuteDeleteAsync, которые не тянут сущности в память. На миллионах строк это ещё режут на пачки:DELETE TOP (5000) FROM ...в цикле до нуля затронутых строк. Короткие транзакции не держат блокировки и не доводят SQL Server до эскалации на всю таблицу; - много соединений: несколько
Includeпо коллекциям дают декартов взрыв строк,AsSplitQuery()разбивает загрузку на отдельные запросы; - длинный список id: у SQL Server предел 2100 параметров на команду. Современный EF Core при
Containsпо коллекции этот предел обходит сам, а в Dapper и сырых запросах список режут черезChunk()или передают табличным параметром; - сложная логика: промежуточный результат кладут во временную таблицу
#tmp. У неё есть статистика, и оптимизатор строит следующий шаг по реальному числу строк.
Параллелить части можно, но DbContext не потокобезопасен: каждому параллельному запросу нужен свой контекст.
Где спотыкаются
Считают CTE способом разбить запрос на этапы. В SQL Server CTE не материализуется: оптимизатор подставляет его текст, и план тот же, что у одного большого запроса.
Выгружают всю таблицу в фоне через Skip/Take. На последних страницах запрос тратит время на пропуск строк, а без стабильного ORDER BY страницы ещё и перекрываются.
Режут на пачки, но выполняют всё в одной транзакции. Блокировки и журнал растут как у одного запроса, выигрыша нет.