先说我自己的判断:ODS层的数据量比对,是数据仓库日常运维里最不起眼但又最不能省的活儿。你可能觉得“不就数个数吗”,但真正在数仓里待过的人都知道,这张表多了一行、少了一行,背后牵连的是下游几十张报表、几百个接口的数据准确性。我这个主题写的就是一套标准化的比对思路和实操步骤,适合刚接触数仓的开发者,也适合已经写了大量同步任务但还没建立系统化校验机制的数据团队参考。文章里的SQL、脚本思路、排查方法,都是我实际用过、踩过坑之后沉淀下来的东西,直接抄作业是没问题的,关键是搞清楚每一步为什么这么做。
1. 数据量比对到底在比什么
1.1 理解ODS层的定位:比对不是走形式
ODS(Operational Data Store,操作数据存储)在数仓架构里的位置很特殊。它紧贴源系统,做的是数据的“搬移”而不是“深加工”。但从我见过的很多项目来看,问题恰恰出在这个“搬移”上——大家默认同步任务跑完就万事大吉,根本不去验证搬过来的东西对不对。
ODS层的核心使命是忠实还原源系统状态。所谓忠实还原,不只是字段要一致,更关键的是数据量要对得上。源表今天插入了5000条,ODS表就必须也有这5000条;源表删掉了3000条,ODS表哪怕没删干净,也是一种失真。这种失真如果不通过比对及时发现,会顺着ETL链路一路传到DWD、DWS、ADS,等到业务报表出现异常,你再去追溯,定位成本翻倍都不止。
所以说数据量比对不是走形式,更不是给领导看的“过程产物”。它是ODS层数据质量的第一道防线。同步任务本身有没有漏数、有没有重复抽取、有没有在过滤条件上写错,这些都不会报错,但都会体现在数据量的变化上。定期比对,相当于给数据管道装了一个体检仪。
1.2 比对的核心维度:行数、主键、汇总值
很多同学一说到数据量比对,脑子里只有count(1)数行数。这个理解不能说错,但太浅了。实际业务里,数仓比对至少有四个维度要同时关注。
第一个维度是总行数。这是最基础、最直观的指标。源表和ODS表分别count一下,数量对得上说明同步的基本盘没崩。但它也是最容易被“骗”的——如果同步任务把源表的数据全量重复抽了一遍,行数翻倍,count能看出来;但如果是A表的数据被误写进B表,A表行数对了,B表行数也对了,count就完全报不出问题。
第二个维度是主键去重数。数仓表一般都有业务主键或逻辑主键。源表按主键去重后的条数,和ODS表按主键去重后的条数,这两个数字必须一致。这个维度能抓出两类问题:一类是重复抽取导致同一主键出现多行,另一类是源端的删除、更新逻辑没有正确同步。
第三个维度是关键汇总值。比如金额字段求和、订单数量求和、最大ID号、最新时间戳。这类指标虽然名字不叫“数据量”,但本质上反映的是数据的内容量是否一致。我习惯把这类比对叫做“内容指纹比对”,它比纯粹的行数更能校验数据是否被完整搬运。
第四个维度是全字段Hash校验值。对所有关键字段做拼接、计算Hash,比对两边的指纹是否一致。这是最严格的做法,但不建议默认全跑,因为代价比较大,一般只在核心表、大额交易类表上使用。
1.3 表级、分区级、字段级:三种粒度的适用场景
数据量比对在实际落地时,粒度不能一概而论。选错粒度,要么查不出问题,要么浪费计算资源。
表级比对,最简单粗暴,适合小表、数据量稳定且同步逻辑简单的场景。比如配置表、字典表、维表这些,一张表总共几千行,表级count加主键查重,十秒钟出结果,没必要搞复杂。
分区级比对,是数仓场景的主流。ODS层数据通常按天、按小时分区存储,每天同步任务跑完,针对当天新增的分区做比对。这样做的好处是问题定位精确——昨天的分区没问题,今天的分区对不上,那就只需要查今天这个同步任务,排查范围瞬间缩小。
字段级比对,一般不做全字段,而是挑敏感字段做专项校验。比如订单表的订单状态字段,源端从“待支付”变成“已支付”,ODS表是否也跟着变了;再比如时间字段,源端改了更新时间,ODS表是否同步修改。这类比对常见于增量同步场景,因为增量同步最容易漏的是“更新”而不是“新增”。
我个人的经验是,不要一上来就追求全字段大而全的比对方案,那会让开发和维护成本失控。先把表级、分区级的主键、行数、汇总值做扎实,再逐步对核心业务表增加字段级校验。比对的覆盖度可以渐进式提高,但比对的频率和自动化程度必须第一天就设计好。
2. 比对SQL的写法与参数选择
2.1 行数比对:count(1)与count(主键)的取舍
先聊最基础的SQL写法。很多人直接写select count(1) from 源表,然后和select count(1) from ODS表对比,完事。但实际生产环境里,我这边的经验是至少要同时跑两个count:count(1)和count(distinct 主键)。
为什么要区分这两个?因为count(1)反映的是物理行数,count(distinct 主键)反映的是逻辑记录数。如果两个数不一致,说明表里存在主键重复的情况。在源表那边,主键重复可能意味着源系统本身的数据质量问题;在ODS表这边,主键重复大概率是同步任务写重复了。
提示:如果表的主键字段允许为NULL,count(distinct 主键)会漏掉NULL值记录。需要在比对时加上主键IS NOT NULL的前提,或者用count(1)配合主键为NULL的独立统计来兜底。
再补充一个细节:count(distinct 主键)在大表上性能很吃紧。如果源表和ODS表都是亿级数据,每张表都跑一个count(distinct),跑批时间可能拉到二十分钟以上。这种情况下我会选择用“去重插入临时表 + 计数”的方式,比如建一张临时表只保留主键列,INSERT OVERWRITE写入distinct结果,再count临时表。代价是多一次磁盘IO,但比直接count(distinct)走聚合要稳定得多。
2.2 主键去重比对:关联查询替代两次独立count
行数比对用两次独立count,但主键去重比对不建议用独立count再相减的方式,因为那样只能知道总数是否一致,无法快速定位差异数据到底差在哪。更实用的做法是直接用JOIN把差异记录查出来。
SQL示例(以Hive/Spark SQL为例):
-- 找出源表存在但ODS表缺失的记录 SELECT a.主键字段 FROM ( SELECT 主键字段 FROM 源表 WHERE 分区条件 ) a LEFT JOIN ( SELECT 主键字段 FROM ods_源表 WHERE 分区条件 ) b ON a.主键字段 = b.主键字段 WHERE b.主键字段 IS NULL; -- 找出ODS表存在但源表缺失的记录 SELECT b.主键字段 FROM ( SELECT 主键字段 FROM ods_源表 WHERE 分区条件 ) b LEFT JOIN ( SELECT 主键字段 FROM 源表 WHERE 分区条件 ) a ON a.主键字段 = b.主键字段 WHERE a.主键字段 IS NULL;这两条SQL跑完,如果都返回空结果,说明主键集合完全一致。如果返回了记录,那这些记录就是需要重点排查的对象。
用这种方式替代独立count的好处很直接:它不仅告诉你“有没有差异”,还告诉你“差异是什么”。特别是当ODS表和源表行数对不上时,你能直接拿到缺失或者是多余的记录清单,后面排查任务失败还是过滤条件写错,都有据可查。
2.3 汇总值比对:金额字段怎么比才靠谱
光比行数和主键,还是可能漏掉问题。比如源表更新了某条记录的金额字段,但同步任务因为增量逻辑写错没同步这个更新,行数和主键都不变,只有汇总值变了。所以关键金额、数量类字段的汇总比对一定要做。
最基础的写法很简单:
SELECT NVL(SUM(金额字段), 0) AS 总金额 FROM 源表 WHERE 分区条件; SELECT NVL(SUM(金额字段), 0) AS 总金额 FROM ods_源表 WHERE 分区条件;但直接SUM在实际生产中有一个坑:如果源表或ODS表中存在脏数据,比如金额字段出现NULL、出现明显超出业务范围的异常值,SUM的结果会被这些脏数据污染,导致两边对不上,但实际上同步逻辑没有问题。所以我在做汇总比对时,会先加一层过滤条件,把NULL值和异常值排除掉,单独再统计异常记录数。
更进一步的做法是分位数比对。因为SUM是全表累加,个别记录的大额偏差会被整体淹没。比如源表和ODS表都有100万行,SUM都是1亿,看起来一致,但中间可能有一笔单子从1000变成了10000,同时另一笔从10000变成了1000,正负抵消了。要防这种情况,最好把金额字段按某个分组维度(比如天、店铺ID、产品类别)拆开,逐组SUM对比,或者做近似分位数校验。这样虽然麻烦一点,但能防止“巧合的一致”。
2.4 Hash校验比对:什么时候用、怎么用划算
Hash校验是数据量比对里最严格的手段。做法是:将一张表的全部关键字段拼接成一个字符串,再对这个字符串计算Hash值(比如MD5或CRC32),最后对比源表和ODS表的Hash结果。
SQL大体长这样:
-- 源表全字段Hash SELECT MD5(CONCAT_WS('|', NVL(字段1,''), NVL(字段2,''), NVL(字段3,'') )) AS row_hash FROM 源表 WHERE 分区条件; -- ODS表全字段Hash(写法相同) SELECT MD5(CONCAT_WS('|', NVL(字段1,''), NVL(字段2,''), NVL(字段3,'') )) AS row_hash FROM ods_源表 WHERE 分区条件;然后对两张表的Hash结果做FULL OUTER JOIN,找不匹配的记录。
能看出代价了吧?每条记录都要做字符串拼接和Hash计算,如果是大宽表几十个字段,性能吃紧。所以我的建议是,把Hash校验用在高价值核心表上。比如交易流水表、账户余额表、库存表这类直接影响资金和业务决策的,可以每天做Hash校验;一般日志表、行为表,做行数、主键、SUM三层校验就够了。
还有个小技巧:Hash校验可以只挑字段的一个子集,选那些最容易出问题的字段,比如状态字段、金额字段、时间字段,而不是全字段。全字段Hash的覆盖最完整,但子集Hash性价比更高。两者可以根据表的敏感程度灵活切换。
3. 比对脚本的工程化落地
3.1 一个可以直接套用的比对脚本结构
理论说再多,落到工程上才是真的。我自己沉淀了一套标准的比对脚本结构,不一定最炫,但胜在稳妥、扩展性好。
脚本的整体结构分成三步:第一步收集源端指标,第二步收集ODS端指标,第三步做差异识别和告警。
以Shell + Hive SQL为例,伪代码大致这样:
#!/bin/bash # 参数:业务日期 biz_date 和表名 table_name biz_date=$1 table_name=$2 # 1. 清空上一轮比对结果临时表 hive -e "TRUNCATE TABLE tmp_ods_compare_result;" # 2. 运行比对SQL hive -e " INSERT OVERWRITE TABLE tmp_ods_compare_result SELECT '${table_name}' AS table_name, '${biz_date}' AS biz_date, a.src_cnt AS src_cnt, b.ods_cnt AS ods_cnt, a.src_amt AS src_amt, b.ods_amt AS ods_amt, CASE WHEN a.src_cnt = b.ods_cnt AND a.src_amt = b.ods_amt THEN 'PASS' ELSE 'FAIL' END AS compare_status FROM ( SELECT COUNT(1) AS src_cnt, NVL(SUM(金额字段),0) AS src_amt FROM 源库.${table_name} WHERE dt = '${biz_date}' ) a CROSS JOIN ( SELECT COUNT(1) AS ods_cnt, NVL(SUM(金额字段),0) AS ods_amt FROM ods库.ods_${table_name} WHERE dt = '${biz_date}' ) b; " # 3. 判断比对结果并决定是否告警 fail_count=$(hive -e "SELECT COUNT(1) FROM tmp_ods_compare_result WHERE compare_status='FAIL';") if [ "$fail_count" -gt 0 ]; then # 发送告警通知:短信、邮件、IM机器人均可 echo "ODS表 ${table_name} 数据量比对失败,请检查" fi这个脚本的骨架有几个设计要点。
第一,所有比对指标先汇总到一张临时比对结果表,而不是在脚本里直接判断。这样每次比对的历史记录都能留痕,回头看趋势、做问题复盘都很方便。
第二,比对结果表里同时记录源端指标和ODS端指标,而不只是“是否通过”。如果哪天要追溯某张表到底从哪天开始对不上,直接查这张表就行。
第三,状态判断用CASE WHEN而不是脚本里嵌套if,这样即使一批表同时比对,每个表的pass/fail状态都能保留下来。
3.2 调度策略:什么时候比、先比谁后比谁
调度策略直接影响排查问题的效率。我见过一些团队把全量ODS表的比对任务堆在一起凌晨跑,结果有一张表比对失败,所有表都等着,排查时根本不知道先看哪里。
我的建议是给比对任务分优先级、分批跑。
第一批,跑核心交易表。这类表直接影响财务和核心业务报表,必须在每日同步任务完成后的第一时间比对。比如凌晨2点同步完,2点半跑第一批比对。
第二批,跑接口和日志类中间表。这类表体量大但业务敏感度略低,对时效性要求没那么高。
第三批,跑维表和配置表。这类表数据量小,即使有问题影响也小,放在最后处理。
另外,比对任务和同步任务之间一定要设置依赖关系。比对任务不能自己定时间硬跑,而应该依赖同步任务的成功信号触发。同步任务失败时,宁可跳过比对,也不要让比对任务拿着不完整的数据去对比,那样只会产生一堆误报告警。
3.3 差异率阈值和分级告警
比对结果不可能永远完全一致。实际生产里,有些表本身数据变化剧烈,或者源端与ODS端存在几个合法的时差(比如跨时区业务),导致行数和汇总值天然存在微小差异。这时候如果阈值设成0,直接一刀切,你会发现告警一天到晚响个不停,最后大家都麻木了,真正的问题反而被淹没。
合理的做法是分级设阈值。
第一级,硬性差异。比如行数相差超过5%、金额SUM差异超过1%,这种大概率是同步逻辑问题,必须立刻告警并介入排查。
第二级,波动差异。比如差异在0.1%到5%之间,可能是合法的业务变化,也可能是问题苗头。这种降级为“提醒”级别,发送到值班群,人工判断是否需要处理。
第三级,微小差异。比如主键去重后只差一两行,可以先记录不告警,在后续比对中持续观察,如果连续多天出现同样差异就需要深挖。
还有一点很重要:每个表的阈值要允许单独配置。核心交易表的阈值要严,日志表、埋点表可以适度放宽。一张日增10亿行的日志表,行数差个0.1%可能都涉及十万行,但影响范围有限;核心交易表哪怕只差一行,都必须当成重大事件处理。
4. 常见的不一致场景与排查实录
4.1 源端数据更新但同步任务没有感知
这是我排查次数最多的一类场景。源系统支持数据修改,但ODS同步用的是增量抽取,增量字段又只取了“创建时间”没取“更新时间”。那结果就是:源表新增的数据都同步过来了,但修改的数据没有同步。
行数和主键比对通常都是通过的,因为记录数没变。但SUM比对会暴露问题——金额字段被更新了,ODS表里的还是旧值。或者用Hash校验也能抓出来。
排查这类问题的思路很直接:先查源表里更新时间晚于同步时间的记录数,再看ODS表里对应主键的字段值是否一致。确认问题后,有两个改造方向:一是增量同步的增量字段改成“最近修改时间”,保证更新记录能重新被抓取;二是在ODS层增加时间戳字段,记录每次同步时间,便于后续追溯。
4.2 重复抽取导致主键冲突和翻倍
这个场景常发生在任务重跑的时候。某个同步任务写的是全量抽取,但因为调度系统重试机制,同一个分区被连续跑了两次,又没有做“先清空再插入”的操作,导致ODS表里出现重复记录。
从比对结果看,最典型的特征是:count(1)异常翻倍,但count(distinct 主键)正常。只要同时跑这两个指标,问题立刻就定位。排查时先看同步任务的运行日志,确认是否有多次执行记录,然后检查ODS表对应分区的最新插入时间,基本就能锁定。
预防手段是给同步任务加幂等性设计。全量抽取的表,写入前先TRUNCATE分区再INSERT;增量抽取的表,插入前按主键去重。更新逻辑好在哪?当任务失败重跑时,重复执行不会产生脏数据。
4.3 空值与类型的隐式转换问题
这类问题特别隐蔽,我起初也吃了不少亏。
比如源端某个字段是String类型,ODS表建表时字段类型设成了Decimal。源端数据是"12.50",同步工具转成Decimal后变成12.5,字符串ToString再还原就是"12.5"。如果之后的比对用字符串拼Hash,两边就对不上。
还有NULL值的处理。源端NULL和空字符串,在不同同步工具的行为下会变成不同的值。有的工具把NULL转成空字符串,有的会把空字符串转成NULL。如果你在Hash校验里直接用字段拼串而不做NVL处理,原来一样的记录也会被判定为不一致。
排查这类问题的技巧是:不要只看“对不上”这个结果,要抓一条具体的差异记录,把源端和ODS端的同一主键数据拉出来,逐字段对比。差异集中在某个特定类型的字段上,大概率就是类型映射或空值转换的问题。
4.4 分区边界和时区偏移
ODS表按天分区,看起来很简单,但不同源系统的“一天”定义完全不同。有的源库是北京时间0点到24点,有的业务库用的是东八区自然日,还有的日志系统按UTC存储。如果同步任务的分区口径没有统一对齐,每天比对都会差几个小时的增量数据。
这种场景下,行数比对结果往往是每天差异几百条到几千条,金额SUM也会差一小截,而不是整块对不上。排查时要先确认两边的分区定义:源表查的是哪个时间字段、ODS分区用的哪个时间字段、中间有没有做时区转换。
我的建议是在ODS层统一所有时间字段的存储口径,比如全部转成东八区时间。同步任务里写清楚时区转换逻辑,比对任务里也用同一套时间口径选取分区,否则比对脚本每天都在跟时区差异做斗争。
4.5 排查流程:先看维度再看明细
差异出现了,怎么快速定位?我有一套固定的排查顺序,基本能覆盖80%的场景。
第一步,先看差异集中在哪个维度。行数差、主键差、金额差,还是Hash差?不同维度直接指向不同的问题类型。
第二步,确认差异是否有分区规律。只有今天差,还是最近几天都差?只有某一个分区差,还是全表差?分区的规律能帮你排除偶发因素。
第三步,抓差异明细样本。用前面说的LEFT JOIN方法,把差异记录捞出来,看是新增缺失、多余重复,还是同主键不同值。
第四步,结合同步任务日志和源系统变更记录,确认问题根因。
这套流程看起来简单,但实际能做到的人并不多,原因是个别步骤容易被跳过。我就见过有同事比对发现行数差了一万,直接去看源系统权限和任务日志,绕了半小时才发现问题是分区名写错。按流程走,先看差异维度,再定排查方向,效率会高很多。
5. 一些长期维护上的心得
做ODS数据量比对,如果只是写了几个SQL和脚本,那还远远不够。比对的机制要持续运转,水位要不断拉高,关键是建立一套能自我进化的运维习惯。
第一,比对结果要做历史留痕并定期复盘。每周扫一眼比对结果表,哪些表频繁告警、哪些表长期PASS,心里要有数。频繁告警的表往往隐藏着更深层的同步逻辑问题,值得专门花时间治理;长期PASS的表可以降低核对频率,把资源留给真正容易出问题的表。
第二,核心表的新增指标覆盖要跟得上业务变化。源系统加了一个新字段,而且这个字段直接影响下游计算,那ODS比对就该把这个字段纳入汇总校验或Hash校验中来。忽视了这个,等下游报表发现问题再补,代价就大了。
第三,每张ODS表要配上元数据说明,写明比对级别是L1(仅行数)、L2(行数+主键+汇总)还是L3(全字段Hash),以及为什么选这个级别。这样后来接手的同事不需要猜,看一眼元数据就知道这张表的校验水位在哪。
第四,告警渠道要分轻重。L3级别的表的比对失败,一定要打到值班负责人手机上;普通表的比对失败,发到团队群里提醒即可。如果所有告警都用一个渠道、一个级别,没有人能长期保持警惕。
最后再分享一个很实用的习惯:所有比对脚本里,把每次比对的“源端指标”“ODS端指标”“差异明细数量”都记录下来。表面上看多写了几行INSERT语句,但等到你要排查“这张表三天前到底有没有问题”的时候,这个设计能节省数个小时的追溯时间。数据量比对说到底是给数据管道补一双眼睛,让异常尽早暴露、让问题可追溯,这套机制越早建成,后面越省心。