Достаточно большое количество проблем производительности в backend-приложениях на самом деле находятся не в коде. За последние пару лет мне несколько раз приходилось разбирать системы, где:
-
API отвечало слишком долго
-
CPU базы был загружен почти на 100%
При этом всем, инфраструктура мощная: достаточное количество RAM, NVMe-диски ну и конечно же CPU последних поколений. Но проблема почти всегда оказывалась в SQL-запросах.
Я хочу поделиться реальным опытом, как мы оптимизировали PostgreSQL в десятки раз
Кейс 1. Индекс, который помог ускорить запрос
Один из самых типичных запросов в системе:
SELECT *
FROM orders
WHERE user_id = 54821
ORDER BY created_at DESC
LIMIT 20;
Таблица "orders" у нас содержит где-то 1 миллион строк, 25 колонок , ну и конечно же активную запись. Запрос выполнялся примерно 3.2 секунды.
План выполнения
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 54821
ORDER BY created_at DESC
LIMIT 20;
Фрагмент плана:
Seq Scan on orders
Filter: (user_id = 54821)
Rows Removed by Filter: 17984521
Многие могут понять, что это значит. PostgreSQL прочитал почти всю таблицу.
Есть решение. который ускорит запрос.
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
После такого уже новый план:
Index Scan using idx_orders_user_created
Примерное время выполнения 25 мс! Это почти в 100 раз ускорение.
Кейс 2. SELECT *, который вечно долго думал
Один из сервисов отдавал список заказов:
SELECT *
FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000;
Тут была проблема не в SQL-времени, а в объёме данных. Таблица содержала JSON-колонку "metadata", текстовые поля и некоторое количество больших колонок. Примерный размер строки 6кб, а 1000 строк примерно 6мб. Запрос выполнялся за 400мс.
Решением такой проблемы послужило следующее решение:
SELECT id, user_id, total_price, created_at
FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000;
После такого изменения новый размер ответа составил где-то 140кб и время ответа составило 40мс. Ускорилось в 10 раз.
Кейс 3. JOIN, который создавал бомбежку всех строк
Один из аналитических запросов выглядел так:
SELECT *
FROM users
JOIN orders ON users.id = orders.user_id
JOIN payments ON orders.id = payments.order_id
JOIN reviews ON users.id = reviews.user_id;
Проблема заключается в кардинальности.
У пользователя есть 20 заказов, 20 платежей и 10 отзывов. Рузельтат 20 × 20 × 10 = 4000 строк. Это называется join explosion - объём результатов запроса с операцией JOIN значительно больше, чем ожидалось.
Решение следующее. Агрегируем данные до JOIN:
WITH payments_agg AS (
SELECT order_id, SUM(amount) total
FROM payments
GROUP BY order_id
)
SELECT *
FROM orders
JOIN payments_agg
ON orders.id = payments_agg.order_id;
Инструменты, которыми я пользуюсь и которые могу помочь
Мой совет, если вы работайте с PostgreSQL, то обязательно используйте:
-
EXPLAIN ANALYZE которые показывает реальные строки. Вез него оптимизация SQL будет мучением.
-
pg_stat_statements который показывает медленные запросы, частые запросы и время их выполнения.
Мои личные правила для оптимизации SQL
-
Индекс должен соответствовать запросу
-
Не использовать OFFSET на огромных таблицах
-
Избегать " SELECT *", лучше выбирать только нужные поля
-
Всегда использовать " EXPLAIN ANALYZE" для лучше оптимизации
Автор: Razor00913
