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-тесты.