4. 数据模型设计:特征宽表设计、预聚合策略、列式存储与行式存储的选择
在支付风控评分场景中,数据模型设计直接决定了特征计算延迟与系统吞吐量。低延迟评分要求特征读取在毫秒级完成,因此必须围绕“宽表预计算 + 列式存储”的核心思路进行建模。本章从特征宽表设计、预聚合策略、存储选型三个维度展开,并给出可落地的架构方案。
核心原则:将特征计算从“实时计算”前置为“离线/近线预计算”,将多表关联转化为单行读取,将行式扫描转化为列式裁剪。
4.1 特征宽表设计
特征宽表(Feature Wide Table)是将风控评分所需的全部特征字段平铺到一张大表中,每笔交易或每个用户对应一行,避免实时评分时跨表 JOIN。设计要点如下:
- 行粒度:通常以“交易流水号”或“用户-设备指纹”为行键,确保一次查询即可获取所有特征。
- 列分组:按特征来源分为基础属性、历史行为、设备指纹、网络环境、关联风险等组,便于后续列裁剪。
- 版本管理:特征宽表应包含时间戳或版本号字段,支持特征回溯与模型回测。
- 空值处理:对缺失特征使用默认值(如 -1、0 或均值)填充,避免实时评分时出现 NULL 异常。
| 特征分组 | 示例字段 | 数据类型 | 更新频率 |
|---|---|---|---|
| 基础属性 | user_id, amount, currency, pay_type | string, double, int | 实时写入 |
| 历史行为 | 30d_tx_count, 7d_avg_amount, 90d_fail_rate | int, double, double | 每小时预聚合 |
| 设备指纹 | device_id, os_version, screen_resolution | string, string, string | 每日更新 |
| 网络环境 | ip_country, isp, proxy_score | string, string, double | 实时查询+缓存 |
| 关联风险 | device_risk_level, ip_blacklist_flag | int, boolean | 近线流计算 |
避坑提示:宽表并非越宽越好。单行字段数建议控制在 200 以内,超过此阈值应拆分特征组为多张宽表,通过行键前缀路由,避免单行 I/O 过大。
4.2 预聚合策略
预聚合(Pre-aggregation)是将高频使用的统计特征(如近 30 天交易次数、近 7 天平均金额)提前计算并写入宽表,评分时直接读取,无需实时聚合。常见策略包括:
- 时间窗口预聚合:按 1h/6h/24h/7d/30d 窗口,使用滑动窗口或 T+1 批量计算,写入宽表对应字段。
- 分层聚合:对用户级、设备级、IP 级分别预聚合,评分时按优先级读取最细粒度特征。
- 增量更新:使用 Lambda 架构,离线批处理计算全量窗口,实时流处理补偿最近窗口的增量数据。
- 冷热分离:热数据(最近 7 天)使用高频预聚合(每 5 分钟),冷数据使用低频预聚合(每小时),降低计算成本。
-- 示例:预聚合宽表 DDL(ClickHouse 引擎)
CREATE TABLE feature_wide_table (
tx_id String,
user_id String,
event_time DateTime,
-- 基础属性
amount Float64,
currency String,
-- 预聚合特征(每小时更新)
hour_tx_cnt UInt32 DEFAULT 0,
hour_sum_amt Float64 DEFAULT 0.0,
day_tx_cnt UInt32 DEFAULT 0,
day_sum_amt Float64 DEFAULT 0.0,
week_tx_cnt UInt32 DEFAULT 0,
week_avg_amt Float64 DEFAULT 0.0,
-- 设备/网络特征
device_id String,
ip_geo String,
proxy_score Float64 DEFAULT -1.0
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (user_id, event_time);
注意:预聚合字段的更新延迟必须小于评分 SLA 允许的最大等待时间。对于 50ms 以内的评分请求,建议预聚合延迟控制在 1 分钟以内,否则应使用实时流计算补偿。
4.3 列式存储与行式存储的选择
存储引擎的选择直接影响特征读取效率。行式存储(如 MySQL、PostgreSQL)适合 OLTP 场景,列式存储(如 ClickHouse、Parquet)适合 OLAP 场景。在风控评分中,特征宽表通常采用列式存储,原因如下:
| 对比维度 | 行式存储 | 列式存储 |
|---|---|---|
| 读取模式 | 按行读取整条记录 | 按列读取所需字段 |
| I/O 效率 | 读取 200 个字段时需加载全部列 | 仅加载评分模型所需的 20~50 个特征列 |
| 压缩率 | 同列数据类型混杂,压缩率低 | 同列数据类型一致,压缩率可达 5~10 倍 |
| 典型延迟 | 单行 200 字段读取约 2~5ms | 单行 50 字段读取约 0.2~0.5ms |
| 适用场景 | 交易写入、事务更新 | 批量特征读取、分析查询 |
选型建议:
- 特征宽表主存储:使用 ClickHouse 或 Apache Doris 等列式引擎,支持高吞吐写入与低延迟点查。
- 实时特征缓存:对延迟要求 < 5ms 的特征(如设备风险等级),使用 Redis 或 Memcached 作为行式缓存层。
- 混合架构:宽表写入列式存储,同时将高频特征同步到行式缓存,评分时优先读缓存,缓存 miss 再读列存。
4.4 特征宽表分区与排序键设计
列式存储的分区与排序键直接影响查询裁剪效率。设计原则如下:
- 分区键:按时间分区(如天或小时),便于 TTL 过期与冷热数据管理。
- 排序键:将评分查询中最常用的过滤字段(如 user_id、device_id)作为排序键,使列式存储能够快速定位数据块。
- 跳数索引:对高基数特征(如 ip_country)建立跳数索引,跳过不相关数据块,减少扫描量。
-- ClickHouse 排序键与跳数索引示例
CREATE TABLE feature_wide_table (
user_id String,
event_time DateTime,
amount Float64,
device_id String,
ip_country String,
...
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (user_id, event_time)
-- 跳数索引:对 ip_country 建立布隆过滤器
INDEX idx_ip_country ip_country TYPE bloom_filter(0.01) GRANULARITY 4;
性能数据:在 10 亿行宽表上,使用 user_id 排序键 + 时间分区,单行点查延迟稳定在 0.3~0.8ms;未使用排序键时延迟上升至 3~8ms。合理设计排序键可降低 10 倍 I/O。