Как сделать Currency Conversion в SQL
EXPLAIN вы видите оценку cost=0.00..431.00. Какой вывод аналитик может сделать безопасно?Содержание:
Зачем нужна конвертация валют
Как только у бизнеса появляются продажи в нескольких странах, выручка приходит в разных валютах — рубли, доллары, евро, фунты. Чтобы посмотреть на бизнес целиком, всё это нужно свести к одной валюте отчётности (обычно USD или EUR). Это и есть currency conversion.
Задача кажется тривиальной — «умножить на курс», — но именно на ней легко ошибиться. Неверный курс тянет за собой неверную выручку, а за ней — неверные решения о том, какие рынки растут, а какие проседают. Отдельная ловушка в том, что «правильного» курса не существует: курс меняется каждый день, а решение, брать курс на дату транзакции или на сегодня, полностью меняет цифру. Ниже — готовые запросы под PostgreSQL для типичных случаев.
Базовый расчёт
Самый простой вариант — единый фиксированный курс. Годится только для черновых прикидок:
SELECT
transaction_id,
amount,
currency,
CASE currency
WHEN 'USD' THEN amount
WHEN 'EUR' THEN amount * 1.10
WHEN 'GBP' THEN amount * 1.27
WHEN 'RUB' THEN amount * 0.011
ELSE NULL
END AS amount_usd
FROM transactions;⚠️ Захардкоженные курсы устаревают уже на следующий день. Как только цифры уходят кому-то в отчёт — переходите на таблицу курсов.
Ежедневные FX-курсы
Правильный способ — хранить курсы в отдельной таблице fx_rates с разбивкой по дате и валюте и джойнить транзакцию с курсом на её дату:
-- Таблица fx_rates: (date, currency, rate_to_usd)
SELECT
t.transaction_id,
t.amount,
t.currency,
t.created_at::DATE AS txn_date,
fx.rate_to_usd,
t.amount * fx.rate_to_usd AS amount_usd
FROM transactions t
JOIN fx_rates fx ON fx.currency = t.currency
AND fx.DATE = t.created_at::DATE -- курс именно на дату транзакции
WHERE t.created_at >= CURRENT_DATE - INTERVAL '30 days';Каждая транзакция берёт курс своего дня — так выручка считается в тех деньгах, что реально были получены. Это historical-подход, и для факта (actuals) он единственно верный.
Мультивалютная выручка
Часто нужно увидеть выручку и в локальной валюте, и в USD одновременно — например, чтобы понять, где рост реальный, а где его «съел» или «надул» курс:
SELECT
DATE_TRUNC('month', t.created_at) AS month,
t.currency,
SUM(t.amount) AS local_total, -- в локальной валюте
SUM(t.amount * fx.rate_to_usd) AS usd_total -- приведённая к USD
FROM transactions t
JOIN fx_rates fx ON fx.currency = t.currency
AND fx.DATE = t.created_at::DATE
WHERE t.status = 'paid'
AND t.created_at >= CURRENT_DATE - INTERVAL '6 months'
GROUP BY 1, 2
ORDER BY 1, 2;Расхождение между динамикой local_total и usd_total по валюте — прямой сигнал влияния курса: локально рынок мог вырасти, а в долларах просесть из-за ослабления валюты.
Свести всё в единую USD-выручку с ARPPU:
SELECT
DATE_TRUNC('month', t.created_at) AS month,
SUM(t.amount * fx.rate_to_usd) AS revenue_usd,
COUNT(DISTINCT t.user_id) AS paying_users,
SUM(t.amount * fx.rate_to_usd) / COUNT(DISTINCT t.user_id) AS arppu_usd
FROM transactions t
JOIN fx_rates fx ON fx.currency = t.currency
AND fx.DATE = t.created_at::DATE
WHERE t.status = 'paid'
GROUP BY 1
ORDER BY 1;Spot vs Historical
Это ключевая развилка, вокруг которой строятся вопросы на собесе. Есть два способа пересчёта, и они отвечают на разные вопросы:
- Historical — каждая транзакция по курсу своего дня. Это фактически полученная выручка в USD. Годится для отчётности по факту.
- Spot — все транзакции по одному сегодняшнему курсу. Это переоценка «сколько это стоит сейчас», удобная для сравнения текущей стоимости, но искажающая историю.
-- Spot (курс на сегодня): единая оценка текущей стоимости
SELECT
t.transaction_id,
t.amount,
t.amount * (
SELECT rate_to_usd FROM fx_rates
WHERE currency = t.currency AND DATE = CURRENT_DATE
) AS amount_spot_usd
FROM transactions t;
-- Historical (по факту): каждая транзакция по своему курсу
-- (запрос уже показан выше в разделе про ежедневные курсы)Правило простое: для отчёта о фактической выручке — historical, для переоценки активов или сравнения «в сегодняшних деньгах» — spot. Переоценивать историческую выручку по сегодняшнему курсу — значит искажать реальность.
Обработка пропущенных курсов
В таблице курсов бывают дыры — выходные и праздники, когда торгов нет и курса на дату просто не существует. Джойн по точной дате в такие дни вернёт NULL и молча потеряет выручку. Решение — цепочка fallback: точная дата, иначе ближайший предыдущий курс:
SELECT
t.transaction_id,
t.amount,
t.currency,
t.created_at::DATE,
-- Цепочка fallback: точная дата → ближайший предыдущий → NULL
COALESCE(
(SELECT rate_to_usd FROM fx_rates
WHERE currency = t.currency AND DATE = t.created_at::DATE),
(SELECT rate_to_usd FROM fx_rates
WHERE currency = t.currency AND DATE <= t.created_at::DATE
ORDER BY DATE DESC LIMIT 1)
) AS rate,
t.amount * COALESCE(
(SELECT rate_to_usd FROM fx_rates
WHERE currency = t.currency AND DATE = t.created_at::DATE),
(SELECT rate_to_usd FROM fx_rates
WHERE currency = t.currency AND DATE <= t.created_at::DATE
ORDER BY DATE DESC LIMIT 1)
) AS amount_usd
FROM transactions t;COALESCE берёт первый непустой курс: сначала пытается найти курс ровно на дату, а если его нет — подставляет последний доступный до этой даты. Так суббота и воскресенье считаются по курсу пятницы, а не теряются.
Влияние курса на рост
Чтобы отделить реальный рост от валютного эффекта, сравнивают локальную и USD-выручку с изменением среднего курса по кварталам:
WITH metrics AS (
SELECT
DATE_TRUNC('quarter', t.created_at) AS quarter,
t.currency,
SUM(t.amount) AS local_revenue,
SUM(t.amount * fx.rate_to_usd) AS usd_revenue,
AVG(fx.rate_to_usd) AS avg_rate
FROM transactions t
JOIN fx_rates fx ON fx.currency = t.currency AND fx.DATE = t.created_at::DATE
GROUP BY 1, 2
)
SELECT
quarter,
currency,
local_revenue,
usd_revenue,
LAG(avg_rate) OVER (PARTITION BY currency ORDER BY quarter) AS prev_rate,
avg_rate,
avg_rate - LAG(avg_rate) OVER (PARTITION BY currency ORDER BY quarter) AS fx_change
FROM metrics
ORDER BY quarter, currency;Оконная функция LAG подтягивает курс предыдущего квартала, а fx_change показывает, насколько он сдвинулся. Если USD-выручка упала, а локальная выросла — рост реальный, но его перекрыло ослабление валюты.
Как это спрашивают на собесе
«Как посчитать выручку в разных валютах в одной цифре?» Сильный ответ: таблица курсов fx_rates, джойн транзакции с курсом на её дату, суммирование amount * rate_to_usd. Не хардкодить курсы — они устаревают.
«Spot или historical — какой курс возьмёте?» Для отчёта о факте — historical (курс дня транзакции), для переоценки текущей стоимости — spot. Перепутать их — исказить либо историю, либо текущую оценку.
«Что делать, если на дату нет курса?» Выходные и праздники дают дыры в таблице. Fallback через COALESCE на ближайший предыдущий курс, иначе транзакция молча потеряется в JOIN.
Частые ошибки
Захардкоженные курсы. Курс EUR/USD меняется ежедневно, а зашитая в запрос константа устаревает мгновенно. Всегда через таблицу курсов с разбивкой по датам.
Единый курс на весь год. Применить январский курс ко всему году — значит скрыть или раздуть валютный эффект. Внутригодовые колебания просто исчезнут из отчёта.
Всё по spot-курсу. Переоценивать историческую выручку по сегодняшнему курсу — искажать реальность: цифра перестаёт отражать то, что реально получили.
Пропущенные даты. В выходные и праздники курса нет, и джойн по точной дате теряет транзакцию. Нужен fallback на последний доступный курс.
Разные источники курсов. ЦБ, ECB, OXR, внутренний банковский курс дают разные значения. Смешивать их в одном отчёте нельзя — выберите один источник и зафиксируйте его в определении метрики.
Связанные темы
- Как посчитать revenue в SQL
- Как посчитать GMV в SQL
- Как посчитать geo distribution в SQL
- Как посчитать gross margin в SQL
FAQ
Откуда брать курсы валют?
Из внешнего источника, который зафиксирован для отчётности: ЦБ РФ, ECB (бесплатный, но евро-центричный), Open Exchange Rates (платный, точный) или внутренний банковский курс компании. Главное — не смешивать источники: у каждого свои значения, и один и тот же день по ЦБ и по ECB даст разные цифры.
Какой курс брать — дневной, месячный или квартальный?
Дневной точнее всего: каждая транзакция считается по курсу своего дня. Средний за месяц — распространённое упрощение, приемлемое, когда дневная гранулярность недоступна или не нужна. Квартальный усредняет слишком грубо и заметно искажает при волатильных валютах.
Spot или historical — что использовать?
Historical для фактической выручки (actuals): каждая транзакция по курсу своего дня отражает реально полученные деньги. Spot нужен для переоценки — «сколько это стоит в сегодняшних деньгах». Путать их нельзя: spot по историческим данным исказит факт, а historical для текущей оценки даст устаревшую цифру.
Что делать с курсами на выходные?
Курса на выходные и праздники обычно нет — торгов не было. Стандартное решение — брать последний доступный курс предыдущего рабочего дня через COALESCE с fallback. Реже применяют интерполяцию между соседними днями, но для большинства задач достаточно «курса пятницы».
Как быть с криптовалютой?
Крипта слишком волатильна, чтобы использовать дневной курс — за день значение может сильно измениться. Обычно берут почасовые курсы или, надёжнее всего, фиксируют курс на момент самой транзакции (snapshot at transaction time), чтобы конвертация отражала реальную стоимость в тот момент.