大数据汇总体系设计:口径统一、幂等重跑与排障实践
做数据这行待久了,你会发现团队里最容易起争执的地方,往往不是写代码的环节,而是“这个数为什么和我那边对不上”。同一个订单量,业务那边拉出来是 12.3 万,你这边跑出来是 12.1 万,差的那两千条既不是脏数据也不是重复,而是两边对“有效订单”的定义不一样。这类问题十有八九出在大数据汇总这一层——明细数据大家都认,一旦往上做聚合、做沉淀,口径、维度、时间窗口的定义就开始各说各话。
这篇东西想聊的就是大数据汇总这件事,从整体思路到落地细节,再到我在实际项目里踩过的坑。它不针对某个特定行业,电商、内容、物流、制造业的设备数据都能套。适合两类人看:一类是刚开始接手汇总层开发、被“数对不上”折磨的同学;另一类是已经在做数据链路、想把汇总体系做得更稳一点的从业者。看完你至少能搞清楚,一次靠谱的汇总该分几层、口径该怎么定、任务怎么保证可重跑,以及线上出问题时从哪儿下手查。
1. 大数据汇总到底在汇总什么:先想清楚再动手
1.1 汇总不是把明细 group by 一下就完事
刚入行那会儿我对汇总的理解特别朴素:不就是把明细表按维度聚合嘛,一个group by加几个sum、count就完事了。真上手做了一年多才发现,group by只是整件事里最不值钱的那部分,写 SQL 谁都会,难的是让这个结果在接下来的半年里持续可信、可复用、可回溯。
我把大数据汇总拆成三件必须同时成立的事。第一件是粒度收敛,把动辄上亿行的明细压到几十万、几百万行的汇总结果,让下游查询从扫几十 GB 变成扫几十 MB,这是最直观的价值。第二件是口径统一,把所有关于“什么算有效”“什么算成功”“金额含不含运费”的判断,从几十个报表 SQL 里收拢到汇总层这一处,改一次全链路生效。第三件是时间沉淀,汇总结果一旦按天分区落下来,它就成了一份可追溯的历史快照,上游明细因为生命周期到期被清掉之后,这些汇总数还在,还能回答“去年这个时候是什么水平”。
这三件事里,第一件是技术活,第二件是沟通活,第三件是设计活。绝大多数汇总层做崩,问题都出在第二件和第三件上——技术上跑得通,但口径散落在各处、历史分区被反复覆盖导致对不上账。所以我现在的习惯是,动手建表之前先把口径文档和分区保留策略定下来,SQL 反而是最后写的。
还有一个认知要摆正:汇总层不是越细越好,也不是越粗越好。见过有团队怕丢信息,把汇总做到只比明细少一两个字段,结果存储没省多少,查询也没快多少,白白多维护一套链路。也见过反过来,直接从明细一把聚合到最终的看板宽表,中间没有任何缓冲,结果任何一个指标口径调整都要重跑全量历史,回刷一次要好几个小时。合理的做法是中间留一层轻度汇总,把最常用的维度组合先固化下来,上层再基于它做二次聚合。
1.2 三种汇总形态,先认清你属于哪一种
同样是叫汇总,业务诉求差别其实很大,我一般把它们分成三类,选型逻辑完全不同。
| 汇总形态 | 典型时效 | 数据量级 | 主要诉求 | 常见实现 |
|---|---|---|---|---|
| 离线批量汇总 | T+1,凌晨跑 | 全量历史累积 | 报表、对账、经营分析 | 分布式 SQL 引擎按分区覆盖写 |
| 准实时微批汇总 | 分钟级 | 滑动窗口,只留近期 | 实时看板、大屏 | 微批任务按窗口聚合后 upsert |
| 明细级预聚合 | 小时级 | 明细的 1/10 到 1/50 | 支撑多维即席查询 | 预聚合 cube 或宽表 |
第一类是绝大多数团队的起点,也是这篇的重点。它的特点是允许延迟,但对准确性要求极高,因为结果要拿去对账、要进财务报表,错一条都要解释。第二类主要解决“看板不能等一晚上”的问题,通常只保留最近 7 天或 30 天的数据,历史仍然靠离线链路兜底。第三类的思路不太一样,它不是把数据变少,而是把“可能要用的维度组合”提前算好,代价是维度一多组合就爆炸——四个维度各 20 个枚举值,笛卡尔积就是 16 万种组合,再乘上日期,很容易算出比明细还大的表。
选型上我的判断顺序是这样的:先问下游能不能接受 T+1,能接受就别上实时,实时链路的维护成本是离线的三倍以上;再问查询的维度是不是相对固定,如果 90% 的查询都集中在那五六个维度上,老老实实做几张固定维度的汇总表最省心;只有当维度组合真的发散、下游需要自由探索时,才考虑预聚合 cube 那种方案。我见过太多项目一上来就追求“什么维度都能查”,最后做出一套没人敢动的复杂结构,维护成本高到自己都嫌烦。
1.3 汇总链路的分层设计:为什么中间那层不能省
标准的分层大概是 ODS(原始)、DWD(明细)、DWS(轻度汇总)、ADS(应用层)四层。理论文章里讲得很清楚,但实际项目里最容易被砍掉的就是 DWS 这一层——老板觉得多一层多一次落盘、多一次调度,为什么不直接从 DWD 出 ADS?
我做过一个对比实验,同一个业务域,A 方案从明细直接出 8 张报表,B 方案先出一张 DWS 再派生 8 张报表。结果是 B 方案的总体计算耗时反而少 30%,因为那张 DWS 被复用了 8 次,而 A 方案里同样的聚合逻辑被重复计算了 8 遍,还因为不同人写的 SQL 细节差异,8 张报表之间的数字彼此对不上。更要命的是后面口径变更,A 方案要改 8 处,改漏一处就埋一个雷;B 方案改一处即可。
DWS 层的设计原则我总结成一句话:按“最细粒度 + 最常用维度组合”落表。所谓最细粒度,是指这一层还不能丢失下钻能力,比如按天、按商品、按渠道汇总,但不要在 DWS 里就把商品合并成类目,那是 ADS 该干的事。所谓最常用维度组合,是指把下游查询频率最高的那几组维度先固化,剩下的组合让 ADS 从 DWS 再聚合一次。这样既保证了复用,又保留了灵活性。
分层的另一个好处是故障隔离。明细层出问题(比如上游投递重复),只需要重跑 DWD 和它下游的 DWS;DWS 出问题,就只重跑 DWS 和 ADS。层次清晰,回刷的范围就可控。如果全部揉在一起,一次回刷就得把整条链路从头跑到尾,那滋味不太好受。
2. 核心细节解析:口径、维度与幂等这三根柱子
2.1 指标口径:一份能真正落地的定义长什么样
口径这事儿,最怕的就是写成“有效订单指用户实际支付成功的订单”这种话。看着挺清楚,实际全是模糊地带:支付成功后退款了算不算?部分退款算不算?0 元购算不算?测试账号算不算?跨天的订单算在哪一天?
我后来形成了一套写法,每个指标至少回答六个问题,写成一张固定格式的表格,谁加指标谁填:
- 业务含义:一句话说清楚这个数在业务上代表什么
- 计算逻辑:落到 SQL 级别的表达式,写
sum(case when ... then ... end)这个粒度 - 过滤条件:哪些数据要排除,逐条列出来
- 时间归属:按下单时间、支付时间还是完成时间归属到哪一天
- 去重主键:用什么字段保证不重复计数
- 特殊场景:跨天、退款、拆单、合并支付怎么处理
举个具体的例子,“支付金额”这个指标,如果只写“用户支付的金额”,下面这些问题全都无解:运费算不算?优惠券抵扣前还是抵扣后?积分抵扣算不算?退款后要不要冲减?我见过一个团队因为“运费算不算”这个点,两拨人各自维护了一套报表,半年后做同比分析才发现两边的口径差了 3 个百分点,追溯下来要重跑半年的数据。
提示:口径文档一定要和汇总 SQL 放在同一个代码仓库里,跟着版本走。写在外部文档系统里的口径说明,三个月后基本就没人更新了,最后变成一份和代码完全脱节的“历史文献”。
还有一个细节,时间归属这件事比想象中重要。下单时间和支付时间跨天的订单在电商里非常常见,晚上 11 点 50 分下单、次日 0 点 5 分支付,这笔该算昨天还是今天?没有绝对的对错,关键是全公司统一。我的建议是跟财务口径对齐——如果财务按支付时间确认收入,那汇总就按支付时间归属,避免后面做对账时反复解释。
2.2 维度设计里的坑:退化维度、缓慢变化与空值
维度这块,最典型的问题是维度退化。所谓退化维度,是指某个维度属性原本应该是独立的维度表,但为了省事直接塞进事实表里当普通字段。比如商品类目,正常应该有一张商品维度表维护商品和类目的映射关系,但很多团队直接在明细里存一个类目 ID 了事。后果是商品换类目之后,历史数据的类目归属跟着变了——如果你从明细重新聚合,得到的历史数据和当初落下来的汇总数就对不上。
解决办法有两个。要么在明细层就把当时的类目快照下来(叫快照维度),要么用缓慢变化维的方式管理维度表,给每条映射关系加上生效时间和失效时间,聚合时按时间关联。前者简单粗暴、存储成本高一点;后者规范但实现复杂。中小团队我一般推荐前者,因为实现简单、不容易出错,存储那点成本在这个数据量级下真不算什么。
空值处理是另一个高频雷区。sum(null)的结果是null而不是 0,如果你的汇总 SQL 里没做coalesce,某个维度下所有值都是空的时候,这个格子出来就是空,前端展示成“-”,业务方一看就以为数据丢了。所以我在汇总层有个硬性规矩:所有度量字段出厂前必须套一层coalesce(x, 0)。
还有count的坑。count(字段)会跳过 null,count(1)和count(*)不会。如果一个订单表里 user_id 允许为空(比如游客下单),那count(user_id)得到的是“有 user_id 的订单数”而不是“订单总数”。这类错误在开发阶段很难发现,因为它不报错,只是数字偏小。我的做法是汇总层统一用count(1)或者明确写成count(distinct 主键),并且在代码评审时专门盯这一类写法。
维度基数的估算也得提前做。一个维度如果基数特别大(比如用户 ID),把它放进汇总的 group by 里,汇总表的行数基本就等于明细行数了,汇总的意义就没了。经验上,汇总维度的基数控制在十万级以内比较合理,超过这个量级就要考虑是不是应该拆成两张表,或者把高基数维度降到它的上层属性(用户 ID 降到用户等级、注册渠道)。
2.3 幂等与可重跑:汇总任务的生命线
这条是我认为整个汇总体系里最重要的工程约束,没有之一。幂等的意思是同一个任务跑一遍和跑十遍,结果必须完全一样。听起来是常识,但实际项目里破坏幂等的写法遍地都是。
最常见的破坏方式是用insert into往分区里追加数据。任务第一次跑成功,第二次因为某种原因重试了,数据就翻倍。这种问题往往不会立刻暴露,等到某天有人发现“昨天的订单量怎么是平时的两倍”,才开始排查,而这个时候已经错了半个月。
正确做法是用覆盖写代替追加写。Hive 系可以用insert overwrite ... partition(dt='xxx'),或者开启动态分区覆盖;数仓类引擎一般提供merge into或者带主键的 upsert。核心思想是:任务对某个分区的写入是“替换”而不是“追加”,重跑多少次,那个分区里永远只有一份正确数据。
第二个破坏幂等的场景是依赖了非确定性函数。比如 SQL 里用了current_date()、rand()、uuid()这类函数,同一个任务在不同时间跑出来的结果就不一样。汇总层里最常见的是用current_date做默认日期,一旦补历史数据时忘了传参,就会把历史数据写成今天的分区。我的规范是:所有日期必须从调度参数传入,代码里禁止出现任何取当前时间的函数。
第三个是上游数据漂移。上午 10 点跑和下午 4 点跑,源表里可能又多了一批迟到数据,结果自然不同。这个严格来说不算破坏幂等,但会造成“同样的任务跑两次结果不一样”的困惑。处理方式有两个:一是给上游数据加一个“就绪水位”,只有确认某个时间点之前的数据都到齐了才开始跑;二是锁定输入快照,跑之前先把输入数据固化到临时分区。
注意:可重跑能力要在设计阶段就考虑,别等到线上出事才想起来补。判断标准很简单——随便挑一个历史日期,把整条链路重跑一遍,结果和当初落下来的数据能不能完全对上。对得上,说明体系是健康的;对不上,那就是埋了一颗随时会响的雷。
回刷机制也得提前设计。真实场景里,口径调整、上游修数、代码 bug 都会导致需要重跑历史。如果代码写死了只跑昨天,那回刷就得手工改代码,很容易改出事故。我的做法是整条链路参数化,日期范围通过调度参数传入,再写一个简单的补数脚本按日期循环调用。回刷时还要注意并发控制,几十个日期同时跑容易把资源打满,一般控制在 3 到 5 个并发比较稳妥。
3. 实操过程:一次日级汇总的完整落地记录
3.1 表结构设计与分区、存储的参数估算
拿一个虚构的交易场景举例,假设明细表每天新增约 6000 万行订单明细,平均每行 12 个字段,未压缩行宽约 180 字节。单日未压缩数据量是 6000 万 × 180 字节 ≈ 10.8 GB,用列式存储加压缩之后,实测压缩比在 6 到 8 倍之间,落在磁盘上大约 1.5 GB 一天。一年下来 550 GB 左右,这个量级单机都能扛,但对汇总来说还是太大了——一次全表扫描要几十秒到几分钟。
汇总表的设计目标是把单日数据压到 100 MB 以内。按天、商品、渠道、地域四个维度聚合,商品维度基数约 8 万,渠道 15 个,地域 300 个,理论上限是 8 万 × 15 × 300 ≈ 3.6 亿,但实际有交易的组合远小于这个数,实测每天输出的行数在 120 万到 180 万之间。行宽控制在 100 字节以内(度量字段用 decimal 而不是 double,避免精度问题也省空间),单日压缩后约 25 MB。从 1.5 GB 降到 25 MB,压缩了 60 倍,下游查询从分钟级降到秒级,这就是汇总最直接的价值。
建表时有几个参数需要留意。分区字段选dt(日期),不要用时间戳做分区,否则分区数量会爆炸。分区粒度选天而不是小时,因为 T+1 的报表需求占九成以上,小时级的需求走另一条链路。存储格式选列式(ORC 或 Parquet 都行,看团队习惯),开启压缩。分桶一般不建,除非明确知道要按某个字段做 join。
create table dws_trade_summary_day ( item_id string comment '商品ID', channel_id string comment '渠道ID', region_id string comment '地域ID', order_cnt bigint comment '订单数', buyer_cnt bigint comment '买家数', pay_amt decimal(18,2) comment '支付金额', refund_amt decimal(18,2) comment '退款金额', net_amt decimal(18,2) comment '净成交金额' ) comment '交易日汇总' partitioned by (dt string comment '日期分区') stored as orc tblproperties ('orc.compress'='snappy');字段类型上有个经验:金额一律用decimal(18,2),别用double。double在做累加时会产生浮点误差,几百万行累加下来尾数可能差几毛钱,对账时这两毛钱能让你查一整天。decimal是精确计算,代价是稍微慢一点点,但汇总层的数据量级完全感受不到这个差异。
3.2 增量汇总的 SQL 写法与执行计划
汇总 SQL 的骨架其实很固定,核心是“过滤 + 关联维度 + 聚合 + 计算派生指标”。但细节上有很多讲究,我把关键部分拆开说。
先说过滤条件放在哪儿。理想情况下尽可能在子查询里就把不需要的数据过滤掉,减少参与 join 和 shuffle 的数据量。比如只处理已支付状态的订单,就在最内层子查询里加where order_status in ('PAID','SHIPPED','FINISHED'),而不是等到 join 完再过滤。这一步能把数据量砍掉 30% 到 50%,对整体耗时影响很大。
再说聚合的写法。多个度量字段用一次group by算完,不要写多个子查询再 join,那样会多次 shuffle。派生指标(比如净成交金额 = 支付金额 - 退款金额)直接在同一个 select 里算,不要拆成两趟。
insert overwrite table dws_trade_summary_day partition(dt='${bizdate}') select d.item_id, d.channel_id, d.region_id, count(1) as order_cnt, count(distinct d.buyer_id) as buyer_cnt, coalesce(sum(d.pay_amt), 0) as pay_amt, coalesce(sum(d.refund_amt), 0) as refund_amt, coalesce(sum(d.pay_amt), 0) - coalesce(sum(d.refund_amt), 0) as net_amt from ( select item_id, channel_id, region_id, buyer_id, pay_amt, refund_amt from dwd_trade_order_detail where dt = '${bizdate}' and order_status in ('PAID','SHIPPED','FINISHED') and is_test = 0 ) d group by d.item_id, d.channel_id, d.region_id;执行计划这块,几个参数值得关注。并行度方面,输入 10.8 GB 未压缩数据,按单 task 处理 128 MB 估算需要约 84 个 task,实际设置 shuffle 分区数时我会给到 200 左右,留出余量应对数据倾斜。设太小会导致单个 task 处理数据过多、容易 OOM;设太大则会产生大量小文件、增加调度开销。经验公式是分区数 ≈ 数据量GB × 10,再按集群核数上下调整。
count(distinct)是个性能敏感点。如果 buyer_id 的基数很大(比如几百万),去重会触发一次额外的 shuffle,耗时可能是普通聚合的两三倍。优化手段是先按维度预聚合去重、再在外层求和,或者改用近似去重的位图方案(如果业务能接受 1% 以内的误差)。对账类场景不能用近似,那就老老实实精确去重,接受这个开销。
写入时要注意小文件问题。如果 shuffle 分区数设得很大,每个分区写一个小文件,几百分区就是几百个几 MB 的文件,后续查询打开文件的元数据开销会盖过实际计算开销。解决办法是写入前做一次合并,或者开启引擎的小文件合并参数,把目标文件大小控制在 128 MB 左右。
3.3 调度编排、数据就绪检查与结果验收
SQL 写完只是开始,真正让它每天稳定跑起来的是调度。一条完整的汇总链路大概长这样:上游数据就绪检查 → DWD 明细加工 → 数据质量校验 → DWS 汇总 → 汇总结果校验 → 下游 ADS 任务 → 通知。
就绪检查这一步千万别省。我吃过一次大亏:上游投递任务因为网络抖动延迟了 40 分钟,我们的汇总任务准点启动,读到了一份只到了一半的数据,跑出来数字偏低,还顺利“成功”了,直到第二天业务方看报表发现异常才暴露。后来加了一个检查:查上游表当天分区的行数,和过去 7 天均值比较,低于 80% 就阻塞等待,超过阈值时间就告警并挂起。这个简单的检查拦下了后面至少五六次类似的故障。
结果校验分两类。一类是技术校验,比如汇总表行数是否在合理区间、主键是否唯一、金额字段是否有负数(不该有负数的地方出现负数说明逻辑错了)。另一类是对账校验,把汇总表的总金额和明细表的总金额对一遍,差异超过万分之零点五就告警。对账校验是最有效的一道防线,因为不管哪个环节出了逻辑错误,最终都会体现在总数对不上。
# 补数脚本的核心逻辑,按日期循环调用调度 for d in $(seq 0 6); do dt=$(date -d "-${d} day" +%Y%m%d) echo "start backfill ${dt}" sh run_job.sh --bizdate=${dt} --mode=overwrite if [ $? -ne 0 ]; then echo "failed at ${dt}, stop" exit 1 fi done补数脚本要注意两点。一是失败即停,不要吞掉错误继续跑,否则会出现一部分日期补了、一部分没补的中间状态,排查起来非常麻烦。二是加并发限制,一次只跑一个日期最稳,虽然慢但不会出问题;如果时间紧,控制在 3 个并发以内,跑之前先确认集群资源够用。
验收阶段我一般会做三件事:抽样对比(随机挑 10 个维度组合,手工从明细算一遍核对)、总量对比(汇总总数 vs 明细总数)、趋势对比(和上周同期、上月同期比,波动超过 20% 就要解释原因)。第三项经常能发现一些逻辑校验发现不了的问题,比如某个维度突然消失了、某个渠道的数据突然归零,这些都是业务上的异常信号。
4. 常见问题与排查技巧实录
4.1 数字对不上:六个高频原因逐个拆
“数对不上”是汇总层最高频的问题,我把它归结为六类,按出现频率排序。
第一类是 join 膨胀。明细表关联维度表时,如果维度表里同一个主键有多条记录(比如商品换过类目,维度表里存了两条),join 之后明细行数就翻倍了。表现是汇总数比明细数大,而且大得很有规律(通常是整数倍)。排查方法是 join 前先对维度表做主键去重,或者加时间条件只关联当前有效的记录。
第二类是过滤条件位置不对。比如buyer_id is not null这个条件写在 where 里和写在 join 的 on 里,结果完全不同——写在 on 里对左表不起过滤作用。还有在子查询里过滤和在子查询外过滤,如果涉及外连接,结果也会不一样。排查方法是把 SQL 拆成几步,每一步单独 count 一下,看在哪一步数字开始偏。
第三类是去重逻辑缺失。上游因为重试机制投递了重复数据,明细里同一个订单出现两次。汇总时如果直接count(1),数字就偏大。防这个要么在明细层就按主键去重,要么在汇总的 SQL 里用count(distinct order_id)。前者性能好,后者保险。
第四类是空值参与计算。sum遇到 null 返回 null,如果下游再拿这个 null 去做加减,整个结果都变成 null。表现是某些维度下数据为空。解决就是前面说的coalesce。
第五类是时间窗口错位。汇总用的日期分区和明细用的日期分区不是同一个含义。比如明细按创建时间分区,汇总却按支付时间过滤,那跨天的订单就会漏掉或者重复。这类问题最隐蔽,因为数字偏差不大,往往只差零点几个百分点,容易被当成正常波动忽略掉。
第六类是并发写入冲突。两个任务同时往同一个分区写数据,后写的覆盖先写的,或者产生一堆小文件导致读取时数据不完整。排查方法是检查调度依赖,确保同一个分区只有一个任务负责写入。
提示:排查“数对不上”时,最快的路径不是看汇总 SQL,而是从明细往回倒推。先确认明细层当天的数据量是否正常,再逐层往上加聚合,每一步都核对总数。哪一步开始出现差异,问题就在那一步。
4.2 任务变慢与数据倾斜的现场处置
汇总任务从 8 分钟涨到 50 分钟,一般不会是 SQL 写错了,而是数据分布变了。最常见的表现是数据倾斜——某个 key 的数据量远超其他 key,导致一个 task 扛了 80% 的数据,其他 task 早就跑完了在等它。
判断方法很简单,看执行详情里各个 task 的处理时长,如果出现几个 task 跑了 40 分钟、其余 task 都是 1 分钟,那就基本确定了。再看那个慢 task 处理的 key 是什么,通常是某个头部商家、某个热门商品、或者某个默认值(比如region_id为空的记录全被分到同一个 reduce)。
解决办法看倾斜的程度。轻度倾斜(头部 key 占比 20% 以内)可以开引擎的倾斜处理参数,让它自动拆分大 key。中度倾斜(占比 30% 到 50%)用两阶段聚合:先给 key 加随机前缀打散,聚合一次,再去掉前缀聚合第二次。
-- 两阶段聚合处理倾斜 select item_id, channel_id, region_id, sum(order_cnt) as order_cnt, sum(pay_amt) as pay_amt from ( select split(concat_rand_key, '_')[0] as item_id, channel_id, region_id, count(1) as order_cnt, sum(pay_amt) as pay_amt from ( select concat(item_id, '_', cast(rand()*10 as int)) as concat_rand_key, channel_id, region_id, pay_amt from dwd_trade_order_detail where dt = '${bizdate}' ) t group by split(concat_rand_key, '_')[0], channel_id, region_id ) t2 group by item_id, channel_id, region_id;重度倾斜(某个 key 占比超过一半)就得考虑业务上是不是有问题了——是不是有刷单、是不是有默认值兜底导致的脏数据。这时候比起优化 SQL,先把数据问题解决掉更划算。
除了倾斜,还有两个常见的变慢原因。一是小文件太多,分区里几千个几 MB 的文件,光是打开文件的时间就够呛,解决办法是定期合并或者调整写入并行度。二是上游数据量突增,比如大促当天数据量翻五倍,任务自然变慢,这种属于预期内的,提前扩容就行,不用改代码。
4.3 常见问题速查表
把上面这些整理成一张表,出问题时对着查会快很多。
| 现象 | 最可能的原因 | 快速验证方法 | 处理方式 |
|---|---|---|---|
| 汇总数大于明细数 | join 膨胀或重复计数 | 检查维度表主键是否唯一 | 维度表去重或改用 distinct |
| 汇总数小于明细数 | 过滤条件位置错误或时间窗口错位 | 逐层加聚合对比总数 | 调整过滤位置、统一时间口径 |
| 某些维度数据为空 | 空值未处理 | 查该维度的度量字段是否为 null | 度量字段套 coalesce |
| 部分格子数字为负 | 退款超过支付或逻辑错误 | 检查是否有冲减逻辑 | 补业务约束或修正计算顺序 |
| 任务耗时突然翻倍 | 数据倾斜或小文件 | 看各 task 处理时长分布 | 两阶段聚合或合并小文件 |
| 重跑后数据翻倍 | 用了追加写 | 查写入语句类型 | 改为覆盖写 |
| 历史数据和重跑结果不一致 | 维度发生缓慢变化 | 对比维度表历史版本 | 引入快照维度或生效时间 |
| 补数后部分日期为空 | 补数脚本中途失败 | 查补数日志 | 失败即停,重新补缺失日期 |
5. 汇总体系的长期维护:成本、治理与起步方案
5.1 存储与计算成本的控制手段
汇总表最容易失控的地方是数量。一开始只有三五张,半年后变成几十张,一年后可能上百张,每张都有人用、但每张都只有一两个人用。存储成本倒在其次,真正贵的是计算成本——一百张表每天跑一遍,集群资源很快就不够分。
控制手段有几个。第一是生命周期管理,明细数据保留 90 天到 180 天,汇总数据保留 2 到 3 年,超期的自动清理。这个策略要在建表时就定好,别等到存储告警了才动手,那时候谁也不敢删。第二是冷热分层,最近 3 个月的汇总放热存储(查询快、成本高),3 个月以上的转冷存储(查询慢、成本低),大部分历史查询的频率其实很低,没必要占用高性能资源。
第三是汇总表的准入和退出机制。新建一张汇总表要说明用途和预期使用方,上线后统计访问日志,连续 60 天没有任何访问的表自动下线。这个机制听起来有点强硬,但确实有效——我见过一个团队靠这个把汇总表数量从 130 多张压到 40 张,集群资源一下就宽松了。
第四是避免重复建设。两张表如果维度组合和度量字段高度重合,就应该合并。判断方法看表的字段相似度,超过 70% 重合基本可以合成一张,用不同的过滤条件区分使用场景。
5.2 小团队的最小可行汇总方案
不是所有团队一开始就有完整的数仓体系。如果你所在的小团队数据量不大(日增千万行以内)、人手有限,我建议的方案是别急着上重型组件,先用一套最简单的结构跑通闭环。
最小可行方案的构成大概是这样的:明细数据落成按天分区的列式文件(Parquet),用单机的分析引擎(比如 DuckDB 这类嵌入式方案)直接查这些文件做汇总,汇总结果再写成按天分区的文件。这套方案跑在单台配置好点的机器上,处理千万级数据基本是秒级到分钟级,没有任何分布式组件的运维负担。等到数据量涨到单机扛不住,再迁移到分布式引擎,迁移成本也不高,因为 SQL 逻辑基本通用。
另一个建议是汇总的维度先少后多。一开始只做两三个核心维度(时间 + 一个业务主维度),跑稳了再往上加。我见过太多团队一上来就设计十几个维度,结果每个维度都没打磨好,最后整套东西没人敢用。先把一两个最核心的指标做准,让业务方建立起信任,后面加维度就是顺水推舟的事。
还有一个容易被忽略的点:汇总结果要有地方给人看。数据做得再准,如果没有一个方便查询的入口,业务方还是会自己导 Excel 算。所以最小方案里也得包含一个简单的查询界面或者定时推送的报表,哪怕就是每天早上把几张核心汇总表发到群里。有了使用场景,汇总体系才有持续迭代的动力。
最后再分享一个我在实际项目里的体会:汇总体系做得好的团队,往往不是技术最强的,而是口径治理最认真的。技术方案可以抄,架构可以照搬,但口径文档能不能持续维护、变更能不能及时同步、出错能不能快速定位到根因,这些靠的是流程和习惯。我现在的做法是每次口径调整都当成一次小版本发布,改代码、改文档、发通知、跑对账,四步一步都不能少。前期显得繁琐,但半年后回头看,省下的排查时间远多于投入的时间。
这个领域后续还有很多可以展开的方向,比如多源汇总结果的一致性校验、汇总链路的数据血缘追踪、以及流批两条链路的口径对齐,每一个都够单独写一篇。如果这篇能帮你在动手之前多想清楚几个问题,那它的价值也就到了。