Во что обходится response_model на горячем списочном эндпоинте с 5000 строк?
Каждая строка валидируется и сериализуется повторно: объекты ORM уже собраны, а модель пересобирает их по полям до записи JSON. На больших списках этот проход и определяет время запроса. Сузьте модель, разбейте выдачу на страницы или верните объект ответа напрямую, минуя модель.
- ✗Считать
response_modelразовой ценой при старте, а не ценой на каждую строку - ✗Менять кодировщик JSON на более быстрый, оставив второй проход валидации на месте
- ✗Отдавать неограниченные списки и трактовать результат как проблему сериализации
- →Как доказать, что запрос определяет проход валидации, а не обращение к базе?
- →Чем вы жертвуете, возвращая объект ответа напрямую?
Задача
Списочный эндпоинт отдаёт 5000 строк и держится на десятках миллисекунд запроса к базе, но отвечает сотнями. Куда уходит время и что с этим делать.
Разбор
class UserOut(BaseModel):
id: int
email: str
display_name: str
@app.get("/users", response_model=list[UserOut])
def list_users(session: SessionDep) -> list[User]:
return session.query(User).all() # 5000 объектов ORM уже собраныСтроки проходят сборку дважды. Сначала драйвер и ORM превращают ответ базы в 5000 объектов User. Затем response_model берёт каждый объект, читает объявленные поля, проверяет типы и собирает из них новый экземпляр UserOut — и только после этого пишется JSON. Второй проход линеен по числу строк и по числу полей, поэтому на больших выдачах он и становится основным слагаемым, а не запрос к базе.
⚠️ Быстрый кодировщик JSON здесь не помогает: он ускоряет последний шаг, а не проход валидации перед ним.
Решение
# 1. Не тянуть лишние поля — сужаем и запрос, и модель
rows = session.query(User.id, User.email, User.display_name).all()
# 2. Ограничить выдачу — страница вместо всей таблицы
rows = rows[offset : offset + limit]
# 3. Горячий путь, где форма ответа уже гарантирована — мимо модели
return ORJSONResponse([{"id": r.id, "email": r.email} for r in rows])Ключевые моменты
- Порядок действий: сначала ограничить объём (страницы), затем сузить модель, и только потом снимать
response_model— это последний шаг, а не первый. - ❌ Возврат объекта ответа напрямую отключает фильтрацию полей: защита от утечки
passwordпропадает вместе с проходом валидации, поэтому словарь придётся собирать явно. - Схема OpenAPI строится один раз при старте — она к цене ответа отношения не имеет.
- Доказывать гипотезу нужно замером: время запроса к базе против времени ответа целиком; расхождение и есть цена прохода.