3. 金融数据模型设计与表引擎选型

在 ClickHouse 金融数据面试中,表引擎选型几乎是必问题。面试官真正想确认的不是你能否背出四个引擎的定义,而是你能否把业务语义、数据变更方式、查询模式三者映射到正确的引擎上。金融场景里,行情、订单、成交、持仓、资金流水、风控指标各有不同的“更新语义”,选错引擎往往不是性能问题,而是结果正确性问题。

本章的核心判断链:先问“这条数据会不会被更新或撤销”,再问“查询是明细还是聚合”,最后问“是否需要保留历史版本”。这三问决定了 ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree 的取舍。
数据会更新吗 更新/撤销/追加 保留最新版本 ReplacingMergeTree 需要撤销标记 CollapsingMergeTree 查询是聚合吗 明细 or 指标 Summing / Aggregating 按维度预聚合

一、核心原理:四个引擎解决的是四种“变更语义”

ReplacingMergeTree 解决“同一主键只保留最新版本”。它按 ORDER BY 主键去重,可指定版本列,后台合并时保留版本最大的一行。注意:去重发生在 merge 阶段,查询时若未合并仍可能看到重复,因此必须配合 FINAL 或 argMax 使用。

SummingMergeTree 解决“同一维度多行数值相加”。合并时按主键把数值列求和,非数值列取任意一行。它适合可加性指标,如成交量、成交额、手续费,但不能处理不可加指标,如均价、最大值、去重人数。

AggregatingMergeTree 解决“预聚合复杂状态”。它存储 AggregateFunction 中间状态,配合物化视图写入,查询时用 merge 或 -Merge 组合器还原。适合 UV、分位数、加权均价等不可加指标。

CollapsingMergeTree 解决“行级撤销”。每行带 Sign 标记,1 表示有效,-1 表示撤销,合并时成对抵消。它适合订单撤单、流水冲正等场景,但要求写入严格成对,否则会残留脏数据。

二、方法流程:从业务语义到引擎选型

  1. 识别主键与更新频率:行情快照按 symbol+ts 高频覆盖,订单状态按 order_id 低频变更。
  2. 判断是否需要历史版本:风控审计要留痕,用 Collapsing 或加版本列;实时看板只要最新值,用 Replacing。
  3. 判断指标可加性:可加走 Summing,不可加走 Aggregating。
  4. 确认查询模式:明细查询用 Replacing/Collapsing,聚合查询用 Summing/Aggregating。
  5. 验证写入与合并行为:用 OPTIMIZE ... FINAL 验证合并结果,再决定查询是否必须带 FINAL。
引擎核心语义典型金融场景关键风险
ReplacingMergeTree主键去重保留最新行情快照、客户最新画像查询未合并会重复
SummingMergeTree同维度数值求和成交量、手续费汇总不可加指标会算错
AggregatingMergeTree存储聚合中间状态UV、分位数、加权均价必须配物化视图
CollapsingMergeTreeSign 成对抵消撤销撤单、流水冲正写入不成对产生脏数据

三、案例场景:订单状态表的选型推演

假设一张订单表,字段为 order_id、status、amount、update_time。业务需要查询“当前有效订单总额”,同时保留撤单审计。若直接用 ReplacingMergeTree,撤单只是把 status 改成 canceled,历史仍在,但无法表达“这笔金额不应计入”。更稳妥的做法是:用 CollapsingMergeTree,撤单时写入一行 Sign=-1 的原始记录和一行 Sign=1 的新状态记录,合并后自动抵消。

CREATE TABLE order_flow (
  order_id String,
  status String,
  amount Decimal(18,2),
  update_time DateTime,
  Sign Int8
) ENGINE = CollapsingMergeTree(Sign)
ORDER BY (order_id, update_time);

-- 撤单:先抵消旧行,再写入新状态
INSERT INTO order_flow VALUES
('O1001','paid',1000,'2024-05-01 10:00:00',-1),
('O1001','canceled',1000,'2024-05-01 10:05:00',1);

-- 查询有效订单总额
SELECT sum(amount * Sign) FROM order_flow;

如果业务只关心“最新状态”,且能接受最终一致,ReplacingMergeTree 更简单:

CREATE TABLE order_latest (
  order_id String,
  status String,
  amount Decimal(18,2),
  update_time DateTime
) ENGINE = ReplacingMergeTree(update_time)
ORDER BY order_id;

SELECT order_id, argMax(status, update_time) AS status
FROM order_latest GROUP BY order_id;

四、误区与复盘:面试中最容易踩的坑

误区一:认为 ReplacingMergeTree 查询自动去重。 去重只在后台 merge 发生,查询时可能看到重复行。正确做法是查询加 FINAL,或用 argMax 聚合。

误区二:用 SummingMergeTree 存均价。 均价不可加,合并后求和会得到错误结果。应改用 AggregatingMergeTree 存 sumState 与 countState,查询时相除。

误区三:CollapsingMergeTree 写入不成对。 撤单必须成对写入,否则 Sign 无法抵消,数据会持续膨胀且结果错误。

面试表达模板:“这个场景的数据会按主键更新,且只需要最新值,所以我选 ReplacingMergeTree,并用版本列控制保留顺序;查询侧我会用 argMax 保证结果正确。如果涉及撤单审计,我会换成 CollapsingMergeTree,并强调写入必须成对。”

复盘时可以用一句话收束:Replacing 管“最新”,Summing 管“可加”,Aggregating 管“复杂聚合”,Collapsing 管“撤销”。 把这句话和具体业务场景绑定,面试官就能确认你不仅记得引擎,更懂得为金融数据的正确性负责。