Почему сервис может деградировать при синхронном использовании SQLAlchemy при получении большого объема данных
Что проверяют
Понимаете ли, где именно тратятся время и память, когда ORM тянет большую выборку, и отличаете ли проблему занятого воркера от проблемы объёма данных. Заодно смотрят, не считаете ли вы, что «переписать на async» лечит любую деградацию.
Как отвечать
Разложить на три независимые причины.
- Драйвер. По умолчанию клиент вычитывает весь результат в память процесса до того, как вернёт первую строку. Пока идёт вычитка, память приложения растёт вместе с размером выборки.
- ORM. Каждая строка превращается в объект и регистрируется в identity map сессии, поэтому память не освобождается по ходу обхода: объекты живут до закрытия
Session. Накладные расходы на объект заметно больше, чем на кортеж. - Воркер. Синхронный вызов занимает поток целиком. В модели «один воркер на запрос» пул быстро кончается, очередь растёт, и снаружи это выглядит как «сервис лёг», хотя база отвечает нормально.
Дальше про лечение: не выбирать всё сразу. yield_per включает серверный курсор и отдаёт строки порциями:
stmt = select(Order).execution_options(yield_per=1000)
for order in session.scalars(stmt):
...Ещё: пагинация по индексированному ключу вместо OFFSET, load_only и контроль стратегий загрузки связей, агрегация на стороне базы, а для отчётов Core или прямой SQL, чтобы объекты не строились вовсе.
Про асинхронность стоит сказать честно: она убирает простой на ожидании ответа, но не уменьшает объём данных и не ускоряет их разбор. Сборка объектов это работа процессора под GIL, и в асинхронном приложении она блокирует весь цикл событий, то есть соседние запросы страдают сильнее.
Где спотыкаются
Отвечают «добавим asyncio, и станет быстрее». Это ответ про конкурентность, а вопрос про объём: миллион строк останется миллионом строк.
Предлагают LIMIT с OFFSET как пагинацию. База на больших смещениях всё равно читает и отбрасывает пропущенные строки, поэтому последние страницы медленнее первых.
Забывают про N+1: ленивая подгрузка связи внутри цикла по выборке превращает один запрос в тысячи. Со стороны это неотличимо от медленной базы, а лечится жадной загрузкой через selectinload или соединением.