Две incremental-загрузки положили в ADS уже испорченные crypto-суммы: raw amount не был поделен на 1e8. Scale берётся из currencies_dictionary.decimals; в bad rows фактически применился decimals=0 / scale 1. Затем global_transactions__ads и product_health__dm честно агрегировали завышенные значения.
Crypto хранится в raw units. Например, 4,350,000,000 raw USDT должно стать 43.5 USDT, затем примерно 37.9 EUR.
В плохих строках normalized_amount = amount. Поэтому 4,350,000,000 raw USDT превратилось в 4.35B USDT и примерно 3.79B EUR.
Главный вывод: product_health не является первичной причиной. Он агрегирует amount_eur, который уже был испорчен выше — в ads.premium_transactions__ads, а затем протёк в ads.global_transactions__ads.
Scale физически не хранится как отдельная колонка. Он выводится из currencies_dictionary.decimals: scale = pow(10, decimals). Для crypto ожидаем decimals=8, значит scale 100000000. В плохих строках effective scale был 1, то есть как будто decimals=0.
mysql_bank_premium.transactions
mysql_bank_premium.currencies_dictionary
mysql_bank_luxury.currencies_dictionary
Source definitions: models/raw/platform/prod/*/plbank/sources.yml.
ods.premium_plbank_transactions__ods
ods.premium_plbank_currencies_dictionary__ods
ods.luxury_plbank_currencies_dictionary__ods
Dictionary columns: id, name, code, decimals, is_fiat.
ads.premium_transactions__ads
ads.global_transactions__ads
dm.product_health__dm
В ADS уже persisted normalized_amount и amount_eur.
| Объект | Сервис / DAG | Конфиг | Режим |
|---|---|---|---|
mysql_bank_premium.transactions → ods.premium_plbank_transactions__ods |
Airflow generated ingestion DAG ingest_premium_plbank_ods, owner data-platform |
dags/ingestion/sources/premium_plbank.yaml |
integer incremental by id, schedule 15 * * * * |
mysql_bank_premium.currencies_dictionary → ods.premium_plbank_currencies_dictionary__ods |
Airflow generated ingestion DAG ingest_premium_plbank_ods |
dags/ingestion/sources/premium_plbank.yaml |
full_refresh, columns id, name, code, decimals, is_fiat |
mysql_bank_luxury.currencies_dictionary → ods.luxury_plbank_currencies_dictionary__ods |
Airflow generated ingestion DAG ingest_luxury_plbank_ods |
dags/ingestion/sources/luxury_plbank.yaml |
full_refresh, columns id, name, code, decimals, is_fiat |
dags/ingestion/dag_factory.py читает YAML из dags/ingestion/sources/ и создаёт PythonOperator на каждый task.
Task вызывает run_ingestion из dags/ingestion/run_ingestion.py.
dags/ingestion/clickhouse_sink.py для full_refresh делает TRUNCATE target local table и затем INSERT ... SELECT из external MySQL database.
Connections: mysql_bank_premium / mysql_bank_luxury → sys_ch_dbt_conn.
ADS запускается отдельным Airflow DAG через Cosmos/dbt:
/home/ubuntu/src/data/airflow_repository/dags/premium_platform_plbank__ads.py OWNER = 'frolov' schedule=[PREMIUM_PLBANK_ODS] select=['premium_transactions__ads']
Модель расчёта:
/home/ubuntu/src/data/airflow_repository/dbt_project/models/ads/platform/prod/premium/plbank/premium_transactions__ads.sql
Формула:
t.amount / pow(10, c.decimals) AS normalized_amount, r.currency_rate * (t.amount / pow(10, c.decimals)) AS amount_eur
Критичный dependency mismatch: premium-модель берёт transactions из premium ODS, rates из premium ODS, но currency dictionary из luxury ODS:
transactions: FROM ods.premium_plbank_transactions__ods currencies: FROM ods.luxury_plbank_currencies_dictionary__ods rates: FROM ods.premium_plbank_exchange_rates_history_eur__ods
_loaded_at = 2026-06-17 06:15:17
Затронуто: LTC и USDT.
_loaded_at = 2026-06-22 10:15:17
Затронуто: BTC и USDT.
У всех плохих строк effective scale был round(abs(amount / normalized_amount)) = 1, а для crypto ожидался 100000000.
| Дата | Валюта | Тип | Строк | Искажение |
|---|---|---|---|---|
| 2026-06-22 | USDT | BONUS_ACTIVATED | 1 | 3.79B EUR |
| 2026-06-22 | USDT | REFILL | 1 | 2.53B EUR |
| 2026-06-22 | BTC | BET | 63 | 2.32B abs EUR |
| 2026-06-22 | BTC | REFILL | 1 | 1.97B EUR |
| 2026-06-22 | BTC | WIN | 3 | 741M abs EUR |
| 2026-06-17 | LTC | BONUS_ACTIVATED | 1 | 210.9M EUR |
| 2026-06-17 | LTC | BET | 1 | 200.5M abs EUR |
| 2026-06-17 | LTC | REFILL | 1 | 41.1M EUR |
| 2026-06-17 | USDT | BONUS_ACTIVATED | 2 | 27.0M EUR |
Сырые premium plbank transactions содержали raw crypto amount.
premium_transactions__ads записал строки с normalized_amount = amount.
global_transactions__ads взял эти строки как готовый upstream.
product_health__dm агрегировал испорченный amount_eur.
В dashboard появились миллиардные deposits / bonuses / bets.
На момент проверки ODS dictionaries для main, luxury и premium показывают decimals=8 для BTC, LTC, USDT, USDC, SOL, TRX, TON, XRP, BCH.
Те же transaction_id, если пересчитать сейчас через текущий dictionary, превращаются в нормальные суммы: десятки EUR, а не миллиарды.
transaction_id=1096204022 · USDT · amount=4,350,000,000 stored amount_eur = 3,790,155,000.0000005 recomputed normalized = 43.5 recomputed amount_eur = 37.90155 transaction_id=1096203466 · BTC · amount=35,600 stored amount_eur = 1,970,075,520 recomputed normalized = 0.000356 recomputed amount_eur = 19.7007552
В коде premium_transactions__ads.sql premium-модель берёт currency dictionary из luxury ODS:
FROM {{ ref('luxury_plbank_currencies_dictionary__ods') }}
Это не доказывает, что именно luxury dictionary был плохим в момент batch-а. Но это явная точка риска: premium transactions зависят от non-premium dictionary. Если словари временно расходятся или ODS отдаёт неполный/не тот snapshot, можно получить такие batch artifacts.
premium_transactions__ads. Автор по git: Vlad / Vladislav Frolov, commit 30582c03.product_health__dm. Автор по git: Vlad, commit 15ec42a2.1. Рядом в query_log: 06:15:46 TRUNCATE TABLE ods.luxury_plbank_currencies_dictionary__ods_local SYNC, затем 06:16:42 INSERT INTO ads.premium_transactions__ads_local. bad batch1. Query log: 10:15:17 INSERT INTO ods.premium_plbank_transactions__ods FROM mysql_bank_premium.transactions, затем 10:16:35 INSERT INTO ads.premium_transactions__ads. bad batchsystem.query_log показывает dbt incremental insert в ads.premium_transactions__ads от пользователя loader, node model.dbt_truelabel.premium_transactions__ads.Если поправить только downstream, источник останется плохим. Нужно идти сверху вниз: premium ADS → global ADS → product_health.
premium_transactions__ads_local — ReplicatedMergeTree, не Replacing. Просто вставить corrected duplicates опасно.
_cluster='premium'brand_id=3002026-06-17, 2026-06-22USDT, BTC, LTCround(abs(amount / normalized_amount)) = 1ads.premium_transactions__ads.ads.global_transactions__ads.dm.product_health__dm.SELECT
toDate(created_at) AS d,
transaction_currency_code,
transaction_type,
is_bonus,
count() AS rows,
uniqExact(user_id) AS users,
max(_loaded_at) AS max_loaded,
sum(abs(amount_eur)) AS sum_abs_eur,
max(abs(amount_eur)) AS max_abs_eur
FROM ads.global_transactions__ads
WHERE brand_id = 300
AND transaction_currency_code IN ('USDT','BTC','LTC','BNB','ETH','USDC','SOL','TRX','TON','XRP','BCH')
AND amount != 0 AND normalized_amount != 0
AND round(abs(amount / normalized_amount)) = 1
GROUP BY d, transaction_currency_code, transaction_type, is_bonus
ORDER BY d DESC, sum_abs_eur DESC;