SQL-запросы для финансового аналитика
Чем финансовая аналитика отличается от продуктовой
Если продуктовый аналитик смотрит, что делают пользователи, то финансовый аналитик следит, как эти действия превращаются в деньги на счетах компании. Это звучит похоже, но в работе разница значительная. В продуктовой аналитике вы много работаете с event-данными, эксперименты двигают метрики, а точность «плюс-минус процент» часто допустима.
В финансах данные чистые (транзакции в ERP), обновляются медленнее (часто раз в месяц закрытие), но требования к точности намного выше. «Примерно» не прокатывает, когда речь о P&L и отчётности перед CFO или инвесторами.
Финансовый аналитик обычно работает с ERP-системой (1С в российских компаниях, SAP в крупных корпорациях), DWH и BI-инструментом. SQL здесь — для выгрузки и преобразований данных под отчёты.
Revenue по месяцам
Самый базовый отчёт — динамика выручки. На первый взгляд простой, но есть нюансы вроде refunds, отложенного признания доходов, мультивалютных транзакций.
SELECT
DATE_TRUNC('month', transaction_date)::DATE AS month,
currency,
SUM(amount) FILTER (WHERE transaction_type = 'revenue') AS gross_revenue,
SUM(amount) FILTER (WHERE transaction_type = 'refund') AS refunds,
SUM(amount) FILTER (WHERE transaction_type = 'revenue') +
SUM(amount) FILTER (WHERE transaction_type = 'refund') AS net_revenue
FROM transactions
WHERE status = 'completed'
GROUP BY 1, 2
ORDER BY month, currency;Refunds обычно записываются с отрицательным знаком, поэтому простого суммирования достаточно. Если refund хранится как положительное число с признаком — логика меняется.
Gross margin по продуктам
Gross margin — это revenue минус cost of goods sold (COGS). Для SaaS это плата за серверы, а для e-commerce — закупочная цена товара и логистика.
WITH product_revenue AS (
SELECT
product_id,
SUM(amount) AS revenue
FROM transactions
WHERE transaction_type = 'revenue'
AND transaction_date >= '2026-01-01'
GROUP BY product_id
),
product_cogs AS (
SELECT
product_id,
SUM(cost) AS cogs
FROM cost_allocations
WHERE period = '2026'
GROUP BY product_id
)
SELECT
p.name,
r.revenue,
c.cogs,
r.revenue - c.cogs AS gross_profit,
ROUND((r.revenue - c.cogs) * 100.0 / r.revenue, 2) AS gross_margin_pct
FROM products p
JOIN product_revenue r USING (product_id)
JOIN product_cogs c USING (product_id)
ORDER BY gross_margin_pct DESC;Когда видно, что какой-то продукт имеет отрицательный gross margin, это сразу повод для discussion с бизнесом: почему мы теряем деньги на каждой продаже?
Прокачать тему на реальных задачах удобно в боте @kariernik_bot — база вопросов собрана с собеседований в Яндексе, Авито, Ozon, Тинькофф.
Cash flow по месяцам
Revenue показывает, что мы заработали, cash flow — реально поступившие деньги. В B2B с отсрочкой платежа разница может быть огромной.
SELECT
DATE_TRUNC('month', payment_received_at)::DATE AS month,
SUM(amount) AS cash_in,
SUM(amount) FILTER (WHERE payment_type = 'subscription') AS subscriptions,
SUM(amount) FILTER (WHERE payment_type = 'one_time') AS one_time
FROM payments
WHERE payment_received_at IS NOT NULL
GROUP BY 1
ORDER BY 1;Для полной картины cash flow нужны ещё outflow (зарплаты, payments поставщикам, налоги), но это обычно в отдельной системе учёта.
Accounts receivable aging
Сколько денег компания ещё не получила от клиентов, с разбивкой по срокам просрочки.
SELECT
CASE
WHEN CURRENT_DATE - due_date <= 0 THEN '0_current'
WHEN CURRENT_DATE - due_date <= 30 THEN '1-30_days'
WHEN CURRENT_DATE - due_date <= 60 THEN '31-60_days'
WHEN CURRENT_DATE - due_date <= 90 THEN '61-90_days'
ELSE '90_plus_days'
END AS aging_bucket,
COUNT(*) AS invoices_count,
SUM(amount_due) AS total_due
FROM invoices
WHERE paid_at IS NULL
GROUP BY 1
ORDER BY 1;Invoices в категории «90+ days» обычно проблемные — либо клиент забыл, либо отказывается платить. Финансовая команда должна с этим разобраться отдельно.
Budget vs actual
Сравнение плановых и фактических значений по подразделениям.
WITH actuals AS (
SELECT
department,
category,
SUM(amount) AS actual_spent
FROM expenses
WHERE period = '2026-Q1'
GROUP BY department, category
),
budgets AS (
SELECT
department,
category,
amount AS budgeted
FROM budget_allocations
WHERE period = '2026-Q1'
)
SELECT
b.department,
b.category,
b.budgeted,
COALESCE(a.actual_spent, 0) AS actual,
b.budgeted - COALESCE(a.actual_spent, 0) AS remaining,
ROUND(COALESCE(a.actual_spent, 0) * 100.0 / b.budgeted, 1) AS utilization_pct
FROM budgets b
LEFT JOIN actuals a USING (department, category)
ORDER BY b.department, utilization_pct DESC;Категории с utilization_pct > 100% означают превышение бюджета — финансовой команде нужно разбираться, откуда взялись дополнительные расходы.
MRR / ARR для SaaS
Для подписочных бизнесов — одна из самых важных метрик.
WITH active_subs AS (
SELECT
customer_id,
plan_id,
started_at,
ended_at,
monthly_amount
FROM subscriptions
WHERE (ended_at IS NULL OR ended_at > CURRENT_DATE)
AND started_at <= CURRENT_DATE
)
SELECT
COUNT(DISTINCT customer_id) AS active_customers,
SUM(monthly_amount) AS mrr,
SUM(monthly_amount) * 12 AS arr
FROM active_subs;Обычно MRR разбивают по компонентам: new MRR (от новых клиентов), expansion MRR (от upgrade), churn MRR (от отменённых), resurrected MRR (от вернувшихся).
На собесе такие штуки часто спрашивают. Быстрый способ довести до автоматизма — тренажёр в Telegram с задачами из реальных интервью.
Customer Lifetime Value по когортам
Для финансового прогнозирования важно понимать, сколько денег приносит клиент за своё время на платформе.
WITH cohort AS (
SELECT
customer_id,
DATE_TRUNC('month', first_purchase_at)::DATE AS cohort_month
FROM customers
),
revenue AS (
SELECT
c.cohort_month,
SUM(t.amount) AS total_revenue,
COUNT(DISTINCT c.customer_id) AS customers
FROM cohort c
JOIN transactions t USING (customer_id)
WHERE t.transaction_type = 'revenue'
GROUP BY c.cohort_month
)
SELECT
cohort_month,
customers,
total_revenue,
total_revenue / customers AS ltv_to_date
FROM revenue
ORDER BY cohort_month;Для предсказания полного LTV используется более сложный подход с экстраполяцией retention curve, но для internal reporting часто хватает cumulative по когортам.
Конверсия валют
Если бизнес работает в нескольких валютах, все финансовые отчёты обычно приводятся к одной (в РФ — к рублям).
WITH rates AS (
SELECT
DATE,
currency,
rate_to_rub
FROM exchange_rates
)
SELECT
t.transaction_date,
t.currency,
t.amount AS amount_original,
t.amount * r.rate_to_rub AS amount_rub
FROM transactions t
JOIN rates r ON r.DATE = t.transaction_date AND r.currency = t.currency;Нужно понимать, какой курс использовать — курс на дату транзакции или средний курс за период. В отчётности для внешних пользователей обычно курс ЦБ на последний день месяца.
Читайте также
FAQ
Какой SQL учить финансовому аналитику?
Базовый уровень плюс оконные функции и CTE. ClickHouse обычно не нужен, PostgreSQL или MySQL хватает.
Какая специфика?
Точность важнее скорости. Один неправильный символ в отчёте инвесторам может стоить работы.
Excel vs SQL?
SQL для выгрузки больших объёмов. Excel для финального оформления отчёта.
Можно ли перейти из финансов в продуктовую аналитику?
Да, это частый переход. SQL и базовые метрики переносятся, нужно выучить статистику и A/B-тесты.