PAYATHON 2026

面向 SSAS 的 Data Warehousing 费用模型设计

支付小周

结论

不要把 Payment 与 Fee 在业务上的多对多关系原样搬进 SSAS。更合适的模型是:

  • 按“某笔 payment 中的一条 fee”建立费用事实表 FactPaymentFee。
  • 每条费用事实记录只对应一个 Payment 和一个 Fee,因此在事实表中都是普通的一对多关系。
  • 费用的实际金额存入事实表,费用名称、分组等描述属性存入 DimFee。
  • 如果还要分析 payment 本身的金额、数量和状态等指标,再建立 payment 粒度的 FactPayment,作为另一个 measure group。
  • 只有需要按 Fee 分析 payment 粒度指标时,才使用 SSAS many-to-many relationship。使用前还要解决 payment 的重复计算问题。

因此,PaymentFees 不是一个需要消除的中间表。它本身就是费用事实表。

先确定两个事实表的粒度

这个模型包含两类业务事件。

Payment 事实

FactPayment 的粒度是:

每笔 payment 一行。

适合存放:

  • Payment amount
  • Payment count
  • Payment date
  • Currency
  • Payment status
  • Customer
  • Merchant
  • Payment channel

Payment Fee 事实

FactPaymentFee 的粒度是:

每笔 payment 中的每条 fee 一行。

适合存放:

  • Fee amount
  • Tax amount
  • Fee count
  • Payer
  • Payee
  • Fee definition
  • Fee charged date

例如,一笔 payment 包含 transaction fee、SMS fee 和 setup fee,那么 FactPayment 中有一行,FactPaymentFee 中有三行。

采用这种模型后,DimFee 到 FactPaymentFee 是普通的一对多关系,不需要使用 SSAS 的 many-to-many 功能。

推荐的星型模型

逻辑结构可以设计为:

                    DimFee
                      |
DimDate ---- FactPaymentFee ---- DimParty(Payee)
                      |
                  DimPayment
                      |
                  FactPayment
                      |
             DimCustomer / DimChannel / DimCurrency

实际项目未必需要单独建立一个宽大的 DimPayment。如果 payment 只是用于钻取和追踪的业务编号,可以把 PaymentNumber 作为 degenerate dimension,直接保存在事实表中。

更常见的物理结构如下:

FactPayment
-----------
PaymentKey
PaymentNumber
PaymentDateKey
CustomerKey
ChannelKey
CurrencyKey
PaymentAmount
PaymentCount

FactPaymentFee
--------------
PaymentFeeKey
PaymentKey
PaymentNumber
FeeKey
FeeDateKey
PayerPartyKey
PayeePartyKey
CurrencyKey
FeeAmount
FeeCount

DimFee
------
FeeKey
FeeBusinessKey
FeeName
FeeGroup
EffectiveFrom
EffectiveTo
IsCurrent

PaymentKey 可以作为 FactPaymentFee 与 payment 的关联标识,但不建议在 SSAS 中直接把两个事实表配置成普通维度关系。SSAS measure group 通常通过一致维度关联。需要让筛选跨事实表传播时,应明确建立 bridge 或 many-to-many relationship。

SQL 表结构示例

下面是简化后的表结构:

CREATE TABLE dbo.DimFee
(
    FeeKey          int           NOT NULL PRIMARY KEY,
    FeeBusinessKey  varchar(50)   NOT NULL,
    FeeName         nvarchar(200) NOT NULL,
    FeeGroup        nvarchar(100) NULL,
    EffectiveFrom   datetime2     NOT NULL,
    EffectiveTo     datetime2     NULL,
    IsCurrent       bit           NOT NULL
);

CREATE TABLE dbo.FactPayment
(
    PaymentKey       bigint         NOT NULL PRIMARY KEY,
    PaymentNumber    varchar(50)    NOT NULL,
    PaymentDateKey   int            NOT NULL,
    CustomerKey      int            NOT NULL,
    ChannelKey       int            NULL,
    CurrencyKey      int            NOT NULL,
    PaymentAmount    decimal(19, 4) NOT NULL,
    PaymentCount     int            NOT NULL DEFAULT 1
);

CREATE TABLE dbo.FactPaymentFee
(
    PaymentFeeKey    bigint         NOT NULL PRIMARY KEY,
    PaymentKey       bigint         NOT NULL,
    PaymentNumber    varchar(50)    NOT NULL,
    FeeKey           int            NOT NULL,
    FeeDateKey       int            NOT NULL,
    PayerPartyKey    int            NULL,
    PayeePartyKey    int            NULL,
    CurrencyKey      int            NOT NULL,
    FeeAmount        decimal(19, 4) NOT NULL,
    FeeCount         int            NOT NULL DEFAULT 1,

    CONSTRAINT FK_FactPaymentFee_DimFee
        FOREIGN KEY (FeeKey)
        REFERENCES dbo.DimFee (FeeKey)
);

数据仓库的分析通常不依赖事实表之间的数据库外键。因此,是否为 FactPaymentFee.PaymentKey 创建指向 FactPayment 的物理外键,应根据 ETL、分区加载和性能要求决定。逻辑上的 payment 标识仍要保持一致。

Fee 定义的历史版本

“Fee 定义可以更新”要区分两种情况。

如果只是修正拼写,例如将费用名称从旧拼写改为新拼写,而且不需要保留旧名称,可以直接覆盖更新 DimFee,也就是 Slowly Changing Dimension Type 1。

如果费用分组、分类、计费规则或其他属性发生变化,并且历史报表需要按当时的定义展示,则应使用 Slowly Changing Dimension Type 2:

FeeKey | FeeBusinessKey | FeeName        | FeeGroup | EffectiveFrom | EffectiveTo | IsCurrent
101    | TXN_FEE        | Transaction Fee| General  | 2025-01-01    | 2025-06-30  | 0
184    | TXN_FEE        | Transaction Fee| Card     | 2025-07-01    | NULL        | 1

ETL 加载 FactPaymentFee 时,应根据业务键和费用发生时间查找对应的 FeeKey:

SELECT f.FeeKey
FROM dbo.DimFee AS f
WHERE f.FeeBusinessKey = @FeeBusinessKey
  AND @FeeDate >= f.EffectiveFrom
  AND (
        f.EffectiveTo IS NULL
        OR @FeeDate < f.EffectiveTo
      );

费用的实际金额不能存入 DimFee。即使系统中有标准费率,它也只是规则或定义。最终收取的 FeeAmount 属于具体业务事件,必须存入事实表。

在 SSAS 中配置 measure group

使用 SSAS Multidimensional 时,可以建立两个 measure group。

Payment measure group

数据来源是 FactPayment,包含:

  • Payment Amount
  • Payment Count

它与以下维度建立 Regular relationship:

  • Date
  • Customer
  • Channel
  • Currency

Payment Fee measure group

数据来源是 FactPaymentFee,包含:

  • Fee Amount
  • Fee Count

它与以下维度建立 Regular relationship:

  • Fee
  • Fee Date
  • Payer
  • Payee
  • Currency
  • Payment(如果创建了 payment 维度)

用户浏览费用数据时,主要使用 Payment Fee measure group,例如:

  • 按 Fee Group 汇总 Fee Amount
  • 按 Payee 汇总 Fee Amount
  • 按月份比较 Transaction Fee
  • 统计每种 Fee 的收费次数

这些分析都不需要 SSAS many-to-many relationship。

什么时候才需要 many-to-many

当用户需要执行下面的查询时,才会遇到多对多问题:

按 Fee 类型汇总 Payment Amount。

假设 payment P1001 的金额为 2,000,并包含两个费用:

P1001 -> Transaction Fee
P1001 -> SMS Fee

如果把 2,000 同时归入两个 Fee,报表可能得到:

Transaction Fee -> 2,000
SMS Fee         -> 2,000
Total           -> 4,000

但真实的 payment amount 只有 2,000。问题不在于 SSAS 无法建立关系,而在于一个 payment 被归入多个 fee 后,payment amount 不再具有可加性。

如果业务确实需要这种分析,可以把 FactPaymentFee 或单独的 bridge 表作为中间 measure group:

DimFee
   |
BridgePaymentFee
   |
DimPayment
   |
FactPayment

bridge 表可以设计为:

CREATE TABLE dbo.BridgePaymentFee
(
    PaymentKey        bigint         NOT NULL,
    FeeKey            int            NOT NULL,
    AllocationWeight  decimal(19, 8) NULL,

    PRIMARY KEY (PaymentKey, FeeKey)
);

随后在 SSAS Dimension Usage 中进行以下配置:

  • DimFee 与 bridge measure group 建立 Regular relationship。
  • FactPayment measure group 通过 bridge measure group 与 DimFee 建立 Many-to-Many relationship。

采用这种设计前,必须先明确指标的业务含义。

防止重复计算的三种方式

只分析费用事实

这是最简单、也最可靠的方式。按 Fee 分析时只展示:

  • Fee Amount
  • Fee Count

不要展示可直接加总的 Payment Amount。

使用分摊权重

如果 payment amount 可以在多个 fee 之间合理分摊,可以在 bridge 中保存 AllocationWeight。例如,一笔 payment 有两个费用,各分配 50%:

PaymentKey | FeeKey | AllocationWeight
1001       | 10     | 0.50000000
1001       | 20     | 0.50000000

分摊后的金额为:

AllocatedPaymentAmount = PaymentAmount × AllocationWeight

权重通常应满足:

Σ AllocationWeight = 1

分摊规则必须来自明确的业务定义,不能只是为了让 cube 的汇总结果“看起来正确”而平均分配。

使用 distinct count

如果需求是统计“包含某类费用的 payment 数量”,可以对 PaymentKey 使用 distinct count,而不是直接统计 bridge 的行数。

例如:

Distinct Payment Count by Fee

SSAS Multidimensional 的 distinct count measure group 会额外影响分区、处理时间和查询性能。具体影响取决于数据量、基数和分区方式,需要用实际数据验证。

Payer 和 Payee 的建模

如果 payer、payee 可能是客户、商户、代理机构或内部账户,可以建立统一的 DimParty,并在费用事实中保存不同角色的外键:

PayerPartyKey
PayeePartyKey

在 SSAS 中,把同一个物理维度定义为两个 role-playing dimensions:

  • Payer
  • Payee

如果“谁承担费用”只有少量固定分类,例如 Customer、Merchant、Platform,也可以建立较小的 DimFeeBearer。不要把频繁重复的文本直接写入事实表。

需要特别注意的事项

不要混合事实粒度

不要随意把 payment amount 和 fee amount 放入同一行,除非事实表明确采用 fee 行粒度。否则,payment amount 会按 fee 行数重复。

例如,一笔金额为 2,000 的 payment 有三条 fee。如果每条费用行都保存 PaymentAmount = 2000,直接求和就会得到 6,000。

明确日期角色

Payment date、fee assessed date 和 fee settled date 可能不是同一天。可以复用 DimDate,并在 SSAS 中配置为不同的 role-playing dimensions:

  • Payment Date
  • Fee Assessed Date
  • Fee Settled Date

处理未识别的维度成员

加载事实数据时,可能遇到尚未进入 DimFee 的费用业务键。通常应准备一个 Unknown 成员,不要使用 NULL,也不要丢弃这条事实记录。

金额需要货币维度

如果系统支持多币种,就不能在未换算的情况下跨币种汇总 FeeAmount。可以同时保存:

FeeAmount
CurrencyKey
ReportingFeeAmount
ExchangeRate

reporting amount 使用哪个时间点和哪种汇率进行换算,必须由业务规则决定。

保留源系统业务编号

PaymentNumber 通常适合作为 degenerate dimension,便于用户从 cube 钻取到具体交易。内部 surrogate key 和源系统业务编号应分别保存。

最终建议

模型应采用两个事实粒度:

FactPayment:每笔 payment 一行
FactPaymentFee:每笔 payment 的每条 fee 一行
DimFee:费用定义及其历史属性

日常费用分析直接使用 FactPaymentFee。在这个粒度下,Fee 与事实表之间是标准维度关系,SSAS 不需要处理业务层面的多对多。

只有报表必须通过 Fee 筛选或汇总 payment 粒度指标时,才引入 bridge measure group 和 many-to-many relationship。与此同时,还要明确这个指标采用重复归属、distinct count,还是按权重分摊。需要先解决的是跨粒度汇总时各项指标的业务含义,而不是 SSAS 是否支持 many-to-many。

备注:内容仅供参考。