定价、币种、摊销与时间窗口
月度成本会上出现了三个都能自圆其说的数字:架构团队按公开单价计算出 18 万美元,云账单导出显示应付 12 万美元,财务总账却只确认了 82 万元人民币。平台团队把三列相加、换汇并按服务拆分后,某个业务竟然比整个账号还贵。进一步检查才发现,公开报价没有合同折扣,预付承诺在购买日一次记账,使用明细又承接了摊销成本;美元账单还被先换成人民币,进入财务系统后按结算汇率换了第二次。
这不是小数精度问题,而是把四种成本、三种币种和两条时间轴压成了一列 cost。只要这列数据继续驱动预算、showback 或优化收益,任何漂亮趋势都可能是重复计费、错窗或汇兑漂移的结果。
一条费用记录至少有四个金额视角
FOCUS 1.4 把常见成本视角拆成 ListCost、ContractedCost、BilledCost 与 EffectiveCost。它们不是同一金额在不同页面的别名,而是回答四个不同问题。
ListCost 回答按公开目录价购买这些用量需要多少钱,适合衡量公开折扣空间,不能拿来对发票。ContractedCost 回答应用合同价格后、尚未把承诺购买等覆盖关系摊回使用行时的金额,适合观察议价影响。BilledCost 回答发票出具方在计费币种中实际开列多少,是发票对账的主口径。
EffectiveCost 回答资源被消费或服务被使用时应确认多少权责成本。预付、承诺折扣和覆盖费用会被摊回受益的使用行。
FOCUS 对 EffectiveCost 有一个关键守恒约束:当覆盖费用与被覆盖费用都出现在同一数据集合中时,相关行的 EffectiveCost 总和应与同一覆盖关系下的 BilledCost 总和相等。购买行若用于覆盖后续使用,其 EffectiveCost 应为 0;被覆盖的使用行可以是 BilledCost=0,但必须承接相应 EffectiveCost。直接把购买行和使用行的两个口径相加,正是承诺成本翻倍的根源。
FOCUS 的 Cost Comparison 与 Effective Cost Analysis 给出了这四列的规范语义。规范版本是 1.4,不代表供应商导出已经交付全部 1.4 字段;消费端还要保存 FocusVersion、供应商 schema 版本和扩展列集合。
费率不是成本,数量、单位和价格点必须成组保存
费率模型至少需要保留 PricingQuantity、PricingUnit、ListUnitPrice、ContractedUnitPrice、PricingCurrency、SkuId 与 SkuPriceId。单独保存“每小时 0.1”没有意义:它可能是每实例小时、每 vCPU 小时、每 GB 月或某个阶梯内的边际价格。
最小计算链是:
定价数量 × 适用单位价格
-> 目录成本或合同成本
-> 折扣、承诺覆盖、抵扣、调整
-> 账单成本
-> 按受益期间摊销后的有效成本块定价、阶梯价和最低消费会让 PricingQuantity 与 ConsumedQuantity 不相等。一个对象消费 900 GB,却可能按两个 500 GB 价格块计费。此时用 ConsumedQuantity × UnitPrice 反算账单,会丢失块边界、最低消费和免费层语义。生产模型应保存供应商给出的价格点标识与原始行,而不是只保留计算后的单价。
报价表也会更新。价格表版本、合同版本、区域、购买选项和生效窗口必须进入数据合同;历史成本重算默认使用当时生效的价格快照,不能用今天的目录价回写过去。需要模拟“若按当前价格会怎样”时,应输出独立的情景金额,例如 scenario_cost,不能覆盖账本字段。
三种币种承担三种责任
PricingCurrency 是价格表或价格点使用的币种,BillingCurrency 是细粒度费用与发票使用的计费币种,PaymentCurrency 则可能是最终结算币种。三者相同是常见情况,却不是可以写进架构的假设。
币种字段必须与金额列绑定:
| 金额 | 允许的币种角色 | 主要用途 |
|---|---|---|
ListCost、ContractedCost | 规范化数据中的 BillingCurrency | 同一账单域内比较目录价和合同价 |
BilledCost | BillingCurrency | 发票与账单明细对账 |
EffectiveCost | BillingCurrency | 权责成本、分摊和趋势 |
PaymentCurrencyBilledCost | PaymentCurrency | 最终结算与财务付款对账 |
| 内部管理报表换算值 | 企业报告币种 | 跨币种管理视图,不覆盖原始金额 |
FOCUS 1.4 的 Invoice Detail 数据集通过 PaymentCurrencyInvoiceDetailId 把细粒度账单行指向保存结算币种金额的聚合行。消费端不能把每个子行按自选汇率换算后,再与聚合结算金额相加。正确做法是保留原始 BilledCost + BillingCurrency,另建带汇率来源、类型和生效时间的转换事实。
CREATE TABLE fx_rate (
rate_date DATE NOT NULL,
from_currency VARCHAR NOT NULL,
to_currency VARCHAR NOT NULL,
rate_type VARCHAR NOT NULL,
rate DECIMAL(24, 12) NOT NULL,
source_id VARCHAR NOT NULL,
PRIMARY KEY (rate_date, from_currency, to_currency, rate_type, source_id)
);rate_type 至少区分供应商账单汇率、财务月结汇率和管理报表汇率。同一报表只能选择一种经批准的汇率政策;汇率变化产生的是换算差额,不是资源用量变化。财务结算金额存在时,以发票结算链为准,不能用日均市场汇率替换。
Billing Period 与 Charge Period 是两条时间轴
BillingPeriodStart/End 表示费用被发票归入哪个账期,ChargePeriodStart/End 表示这笔费用在哪个时间段生效。两者都采用起点包含、终点不包含的半开区间 [start, end)。一个在账期最后一秒发生的持续用量属于当前窗口,终点恰好等于下一窗口起点,不应被两个窗口重复计入。
持续使用通常按导出粒度切成小时或日费用期;一次性购买、月度支持费、税费与迟到调整的费用期可能跨越或晚于实际用量。承诺购买可以在一个账期产生 BilledCost,它覆盖的使用却分布在多个费用期。因而:
发票对账按 billing period 和 invoice 聚合 BilledCost。资源趋势、分摊和单位经济按 charge period 聚合 EffectiveCost。现金流预测关注购买与付款窗口。
优化回测必须比较相同 charge window,不能拿一个账期总额与另一个自然月用量相除。
数据仓库统一保存 UTC 时间戳,同时保留供应商账期时区与企业关账时区。展示层可以换成本地时间,但分桶键必须显式指定时区。把无时区字符串按服务器本地时区解析,会在夏令时切换、跨区域账号和月底边界上造成重复或缺失小时。
建立可追溯的成本口径合同
先在仓库中建立口径配置,不让每个仪表盘自行决定金额与窗口:
costView: engineering-accrual
metric: EffectiveCost
currency:
source: BillingCurrency
reporting: CNY
rateType: finance-month-close
timeWindow:
basis: ChargePeriod
timezone: UTC
interval: "[start,end)"
chargePolicy:
usage: include
purchase: include-via-effective-cost
credit: allocate-by-beneficiary
tax: report-separately
adjustment: preserve-charge-class
schema:
focusVersion: "1.4"
providerSchema: "<provider-schema-version>"
policyVersion: "pricing-policy-v3"metric 决定现金或权责视角;rateType 决定汇兑政策;basis 决定按账期还是费用期切窗;chargePolicy 决定抵扣、税费和调整如何进入管理视图。任何改动都生成新版本,不原地修改已关账结果。
接入步骤可以按以下顺序执行:
把供应商原始导出写入不可变 raw 区,保存对象路径、校验和、交付批次、schema 版本与加载水位。在 staging 层把成本列解析为定点十进制,把时间转成带时区时间戳,拒绝未知币种和不合法窗口。保留供应商原始列,再映射到规范列;无法映射的扩展字段进入受控扩展区,不静默丢弃。
分别产出 cash view 与 accrual view,前者以 BilledCost + BillingPeriod 为主,后者以 EffectiveCost + ChargePeriod 为主。最后生成报告币种视图,保存汇率键与换算结果;原始币种金额始终可回溯。
最小质量门禁:
-- 费用期必须为合法半开区间
SELECT COUNT(*) AS invalid_windows
FROM focus_cost_usage
WHERE charge_period_start >= charge_period_end;
-- 同一聚合中不能无提示混币
SELECT billing_account_id, charge_period_start, COUNT(DISTINCT billing_currency) AS currencies
FROM focus_cost_usage
GROUP BY billing_account_id, charge_period_start
HAVING COUNT(DISTINCT billing_currency) > 1;
-- 关键口径不得为空
SELECT COUNT(*) AS incomplete_rows
FROM focus_cost_usage
WHERE billed_cost IS NULL OR effective_cost IS NULL OR billing_currency IS NULL;三条查询都返回 0 才能进入下游。第二条返回结果不一定表示数据错误,但表示聚合必须先按币种分组或经过显式换算。
正向实验:证明承诺摊销不重不漏
在隔离 schema 建立一组最小费用。购买行支付 120 USD,用于覆盖两个费用期;两个使用行各承接 60 USD 的有效成本。另有 -10 USD 抵扣和 5 USD 税费。示例使用语义占位时间,不绑定真实账期:
CREATE TABLE cost_lab (
row_id VARCHAR PRIMARY KEY,
charge_category VARCHAR NOT NULL,
charge_period_start TIMESTAMP NOT NULL,
charge_period_end TIMESTAMP NOT NULL,
billing_currency CHAR(3) NOT NULL,
billed_cost DECIMAL(18, 6) NOT NULL,
effective_cost DECIMAL(18, 6) NOT NULL,
commitment_id VARCHAR
);
INSERT INTO cost_lab VALUES
('purchase-1', 'Purchase', CURRENT_DATE, CURRENT_DATE + INTERVAL '2 month', 'USD', 120, 0, 'commitment-demo'),
('usage-1', 'Usage', CURRENT_DATE, CURRENT_DATE + INTERVAL '1 month', 'USD', 0, 60, 'commitment-demo'),
('usage-2', 'Usage', CURRENT_DATE + INTERVAL '1 month', CURRENT_DATE + INTERVAL '2 month', 'USD', 0, 60, 'commitment-demo'),
('credit-1', 'Credit', CURRENT_DATE + INTERVAL '1 month', CURRENT_DATE + INTERVAL '2 month', 'USD', -10,-10, NULL),
('tax-1', 'Tax', CURRENT_DATE + INTERVAL '1 month', CURRENT_DATE + INTERVAL '2 month', 'USD', 5, 5, NULL);先验证承诺覆盖集合守恒:
SELECT commitment_id,
SUM(billed_cost) AS billed_total,
SUM(effective_cost) AS effective_total,
SUM(billed_cost) - SUM(effective_cost) AS delta
FROM cost_lab
WHERE commitment_id IS NOT NULL
GROUP BY commitment_id;预期 billed_total=120、effective_total=120、delta=0。再按费用期观察权责成本:
SELECT charge_period_start,
SUM(effective_cost) AS accrual_cost
FROM cost_lab
GROUP BY charge_period_start
ORDER BY charge_period_start;第一个费用期为 60,第二个费用期为 55,因为第二期包含 60 的承诺摊销、-10 抵扣和 5 税费。若管理视图要求税费独立展示,就按 ChargeCategory 分组,而不是删除税费行。这个实验同时证明“购买日现金支出 120”和“两个期间各承担 60”可以都正确,但它们不能在同一个总额里相加。
反向实验:一次错误 JOIN 怎样把成本放大
给同一天写入两种汇率来源,再用只按日期与币种连接的错误查询:
INSERT INTO fx_rate VALUES
(CURRENT_DATE, 'USD', 'CNY', 'finance-month-close', 7.10, 'finance-ledger'),
(CURRENT_DATE, 'USD', 'CNY', 'management-daily', 7.08, 'market-feed');
SELECT c.row_id,
c.billed_cost,
r.rate_type,
c.billed_cost * r.rate AS converted_cost
FROM cost_lab c
JOIN fx_rate r
ON DATE(c.charge_period_start) = r.rate_date
AND c.billing_currency = r.from_currency
AND r.to_currency = 'CNY'
WHERE c.row_id = 'purchase-1';预期同一购买行返回两条记录。如果下游直接 SUM(converted_cost),120 USD 被换算两次。正确查询必须固定 rate_type 和批准的 source_id,并对 (cost row, reporting currency, rate policy version) 建唯一键:
SELECT c.row_id,
c.billed_cost * r.rate AS converted_cost,
r.rate_type,
r.source_id
FROM cost_lab c
JOIN fx_rate r
ON DATE(c.charge_period_start) = r.rate_date
AND c.billing_currency = r.from_currency
AND r.to_currency = 'CNY'
AND r.rate_type = 'finance-month-close'
AND r.source_id = 'finance-ledger'
WHERE c.row_id = 'purchase-1';预期只返回一条 852 CNY 的管理换算记录。它仍不是付款凭证;真正存在 PaymentCurrencyBilledCost 时,应沿 Invoice Detail 的关联键对账。
另一个稳定反例是把窗口写成两端都包含。对相邻窗口 T0..T1 和 T1..T2,在 T1 开始的行会被两次选中。所有过滤统一使用:
WHERE charge_period_start >= :window_start
AND charge_period_start < :window_end如果输入行跨越目标窗口,应按持续时间或供应商给出的粒度切分,并保存分割前行 ID;不能只按开始时间把整笔成本塞进一个桶。
Credit、Tax 与 Adjustment 不能靠正负号猜
负数不一定是抵扣,正数也不一定是正常使用。ChargeCategory 区分 Usage、Purchase、Tax、Credit 和 Adjustment,ChargeClass 进一步表达正常行与修正行。团队应为每类费用明确:是否进入预算、是否分摊、按谁受益、何时确认、是否允许回写已关账期。
抵扣通常有适用对象。把账号级 credit 按当月直接成本比例分摊是一种政策,不是供应商事实;政策版本必须随结果保存。税费可能由财务中心承担,也可能按法人与成本中心分配。支持费和 Marketplace 费用也可能缺少资源 ID,不能为了提高“已分配率”伪造资源映射。
迟到费用与修正行尤其危险。流水线需要知道供应商采用 replacement、delta 还是 ledger 风格;在不理解修正语义时无脑 append 再求和,可能重复历史金额。raw 区保存每次交付,规范层按数据生产方的修订规则生成当前视图,关账层则使用追加的调整凭证,避免静默改写已批准结果。
从异常数字反推故障位置
出现“本月成本突然翻倍”时,按下面顺序定位:
比较原始行数、业务主键数和 JOIN 后行数。行数成倍增长通常是汇率、标签历史或价格表的一对多连接。分别求 BilledCost 与 EffectiveCost,再按 ChargeCategory 展开。购买与使用同时出现大额,先检查承诺覆盖关系。按 BillingCurrency 分组。混币后直接求和会得到没有量纲的数字。
分别按 billing period 和 charge period 聚合。只在月底跳变,常见原因是账期与费用期混用或时区边界错误。检查 delivery batch、修正类型与重复键。总额正确但局部异常,可能是 replacement/delta 处理错误。最后再检查小数精度。金额使用定点十进制,禁止用二进制浮点承担发票守恒。
建议持续监控 duplicate_row_count、invalid_window_count、unknown_currency_count、missing_rate_count、mixed_currency_group_count、commitment_conservation_delta、invoice_reconciliation_delta 和数据水位。阈值不能只看全局总额;某个账号多算、另一个账号少算会互相抵消。
权限、凭证和敏感数据边界
价格表、账单、合同折扣、承诺利用率、账号结构和付款币种会暴露采购条件、业务规模与组织关系。raw 数据域只允许账单采集身份写入,转换任务只读取必要账号和列,报表通过授权视图隐藏合同单价、发票 ID、真实标签与资源标识。财务汇率源和管理汇率源由不同 owner 批准,分析人员不能随意替换。
采集优先使用云工作负载身份或短期凭证;长期密钥不得写入 SQL、Notebook、Chart values、日志和导出样例。换汇源、价格快照和政策配置都需要内容校验和、签名或受保护仓库 revision。下载到个人设备的明细同样属于敏感副本,必须受保留和删除策略约束。
审计记录至少保留查询身份、数据范围、口径版本、汇率版本、输出位置和下载行为。展示层隐藏字段不等于后端已授权,API 与仓库必须按账号、法人和成本中心执行行列级权限。
数据容量与查询成本也要进入模型
小时级、多账号、多区域账单会快速累积,标签和供应商扩展列又会扩大列宽。raw、规范层、换算层和关账快照如果各保存完整副本,成本平台本身可能成为新的浪费源。容量模型要估算日行数、平均行宽、修订倍数、保留期、分区数量和常用查询扫描量。
按 charge date、provider 和 billing account 分区通常比按高基数资源 ID 分区稳定;资源、SKU、价格和汇率适合版本化维表。预聚合只能加速已定义口径,不能替代原始证据。关账快照长期保留,临时分析表和中间导出设置较短 TTL;删除前确认审计、争议和法规保留要求。
升级、回滚和退出必须保护历史语义
从旧 schema 升到 FOCUS 1.4 或供应商新导出时,先并行解析同一批数据,比较行数、币种、费用类别、四种成本列、承诺守恒和发票差异。新字段为空不等于 0,供应商 1.2 数据也不能被伪装成已经交付 1.4 的发票与分摊能力。
切换使用独立 view revision。发现差异时,回滚查询指针到旧 view,保留新旧 raw 与转换日志,不删除争议数据。价格、汇率和口径配置使用不可变版本;回滚代码不能回滚已经批准的财务政策,必要时生成调整批次。
退出某个成本工具或仓库前,导出原始账单、规范化事实、价格与汇率快照、口径配置、schema 映射、关账结果和审计记录;随后撤销采集身份、对象存储访问、数据共享、BI token 和临时导出。最后用供应商发票、关账快照和替代平台的同窗结果完成对账,证明旧任务不再写入、旧凭证不能读取、旧存储已按策略核销。
架构决策落在用途,而不是选一列通吃
现金管理与发票核对选择 BilledCost + BillingPeriod;工程分摊、趋势和单位经济选择 EffectiveCost + ChargePeriod;折扣分析同时比较 List、Contracted、Billed 与 Effective;财务付款使用发票结算币种;跨国管理视图则增加受控汇率层。
架构师真正要固定的是“某个决策使用哪组金额、币种、窗口和政策版本”。只要任何图表仍然只有 cost、month 两列,读者就无法判断它在讲报价、发票、现金、权责成本还是换算后的管理数字,也无法在争议发生时还原结论。
