Ответ
Самый интересный кейс — это рефакторинг и оптимизация монолитного отчета для финансового департамента, который выполнялся более 40 минут. Запрос представлял собой цепочку из 12 вложенных подзапросов и LEFT JOIN по неуникальным полям.
Задача: Рассчитать помесячную динамику LTV (Lifetime Value) для когорт пользователей, с разбивкой по каналам привлечения, учитывая только тех, кто совершил повторные покупки, и исключая тестовые аккаунты.
Исходная проблема: Полное сканирование огромных таблиц orders и users, множественные временные таблицы на диске, неправильные оценки кардинальности оптимизатором.
Мое решение:
- Разбил логику на четкие CTE (Common Table Expressions) для читаемости и повторного использования.
- Заменил подзапросы в
SELECTиJOINна предварительно агрегированные CTE, чтобы избежать многократного выполнения одной логики. - Ввел оконные функции для расчета накопленного итога внутри когорты, что устранило необходимость в самосоединении (self-join).
- Добавил покрывающие индексы (covering indexes) для колонок, используемых в
WHERE,GROUP BYиJOIN.
Ключевой фрагмент оптимизированного запроса (на PostgreSQL):
WITH user_cohorts AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(created_at)) AS cohort_month,
acquisition_channel
FROM users
WHERE is_test_account = FALSE
GROUP BY user_id, acquisition_channel
),
user_revenue AS (
SELECT
o.user_id,
DATE_TRUNC('month', o.created_at) AS revenue_month,
SUM(o.amount) AS monthly_revenue
FROM orders o
INNER JOIN user_cohorts uc ON o.user_id = uc.user_id
WHERE o.status = 'completed'
GROUP BY o.user_id, DATE_TRUNC('month', o.created_at)
HAVING COUNT(o.id) > 1 -- Только с повторными покупками
)
SELECT
uc.cohort_month,
uc.acquisition_channel,
ur.revenue_month,
SUM(ur.monthly_revenue) OVER (
PARTITION BY uc.cohort_month, uc.acquisition_channel, ur.user_id
ORDER BY ur.revenue_month
) AS cumulative_ltv,
COUNT(DISTINCT ur.user_id) AS active_users
FROM user_cohorts uc
JOIN user_revenue ur ON uc.user_id = ur.user_id
ORDER BY 1, 2, 3;
Результат: Время выполнения сократилось с 40+ минут до ~8 секунд. Это было достигнуто за счет:
- Резкого уменьшения операций ввода-вывода (I/O).
- Перехода от Nested Loops Joins к более эффективным Hash Joins.
- Позволило бизнесу запускать отчет ad-hoc, а не раз в сутки.