Ответ
Конечно. Вот пример оптимизации эндпоинта в проекте, похожем на маркетплейс.
Задача: Оптимизировать эндпоинт GET /users/{id}/orders, который возвращал список заказов для конкретного пользователя.
Проблема: Изначальная реализация страдала от проблемы "N+1 запросов". На один запрос к API происходило:
- Один запрос к БД для получения данных пользователя.
- N запросов к БД в цикле для получения каждого из его заказов.
Это приводило к огромному количеству обращений к базе данных и высокому времени ответа (~1200ms для пользователя со 100 заказами).
Решение (в 3 шага):
-
Устранение N+1 с помощью
JOIN: Запрос был переписан для получения всех необходимых данных за одно обращение к БД. Это позволило избежать цикла и множества запросов.-- Вместо множества запросов теперь один SELECT o.*, p.name, p.price -- и другие поля заказа и товаров FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id WHERE o.user_id = $1; -
Добавление пагинации: Чтобы не загружать тысячи заказов одновременно, была добавлена серверная пагинация (
LIMIT/OFFSET). Клиент стал получать данные порциями.-- Добавляем к запросу выше LIMIT $2 OFFSET $3; -
Кеширование: Для часто запрашиваемых данных было внедрено кеширование на уровне приложения с использованием Redis. Ответы для активных пользователей кешировались на 1-2 минуты, что еще больше снизило нагрузку на БД.
Результат:
- Время ответа эндпоинта сократилось с ~1200ms до ~80ms.
- Нагрузка на базу данных значительно снизилась, что повысило общую стабильность сервиса.
Ответ 18+ 🔞
Ну, смотри, как мы тут один эндпоинт на маркетплейсе из говна и палок делали. Задача была простая, казалось бы: GET /users/{id}/orders. Ну, отдай заказы пользователя, че тут думать-то?
А оказалось, блядь, там под капотом такая дичь творилась, что волосы дыбом встают. Классический "N+1 запрос", ёпта! Представь: приходит запрос, мы лезем в базу за юзером. Окей. А потом, сука, в цикле начинаем для КАЖДОГО его заказа по отдельности лезть в базу за данными! Это ж пиздец какой-то! Если у чувака 100 заказов, то это 1 + 100 = 101 запрос к базе. И эндпоинт тупит, как пень, 1200 миллисекунд отвечает. Пользователь уже успевает сходить, кофе сварить, вернуться, а у него ещё спиннер крутится.
Ну, мы не дураки, сели и подумали. Решили по-взрослому.
Шаг первый, самый главный: выпиливаем этот N+1 нахуй. Вместо этой ебалы с циклами, пишем один нормальный, адекватный запрос с JOIN. Пусть база сразу всё отдаёт, одним махом, как швейцарские часы.
SELECT o.*, p.name, p.price
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = $1;
Вот так, блядь. Из 101 запроса сделали один. Уже хорошо.
Шаг второй: пагинация. А че, собственно, мы 10 000 заказов разом-то грузим? А если у него их миллион? Сервер сдохнет, а пользователю первые 10 всё равно. Добавили LIMIT и OFFSET. Клиент теперь порциями данные жрёт, всё цивилизованно.
LIMIT $2 OFFSET $3;
Шаг третий, для полного счастья: кеширование. Ну а че? Если какой-то активный пользователь каждые 10 секунд свои заказы дергает, зачем нам каждый раз в базу лезть? Поставили Redis, ответ на пару минут в кеш пихаем. Следующий запрос — бац, и данные уже из памяти, моментально.
И что в итоге, спросишь ты? А в итоге мы с 1200ms спустились до ~80ms. Представляешь? В 15 раз быстрее, блядь! Нагрузка на базу упала, сервис перестал хрипеть, все довольны. Вот так вот, казалось бы, мелочь, а из-за кривых рук и непонимания основ может всю систему в тапки запереть.