news 2026/10/28 13:59:45

数据仓库分层、维度建模与拉链表:历史快照与增量抽取实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据仓库分层、维度建模与拉链表:历史快照与增量抽取实践

简介:这份《数据仓库工程师面试题.docx》面向准备数据仓库岗位面试的初中级工程师,也可作为日常知识梳理与自查清单。文档围绕数仓核心概念展开,涵盖数据仓库的主题性、集成性、稳定性、时变性四大特点,ETL的抽取、转换、加载流程及其在数据集成中的作用,ETL执行日志、错误日志、总体日志的分类与告警发送方式,以及STG、ODS、DW、Presentation四层模型各自的定位、作用与开发步骤。内容还延伸到CDC策略选择、mapping与物理模型设计、数据质量检查、数据清洗与规范化、缓慢变化维和代理键等面试常见考点,便于读者按知识模块复习并查漏补缺。资源包共1个docx文件,大小约31KB,轻量易读,适合通勤或面试前快速过一遍。目前已有1711人学习下载,可作为数仓面试准备的入门与进阶参考。

1. 数据仓库不是“更大的数据库”:四层模型里为什么到处是重复数据

刚接触数据仓库知识体系的人,最容易产生的误解是:数据仓库就是“一个更大的数据库”。真做过一轮建设就会发现,这里到处都是重复——同一份订单在 STG 存一份、ODS 再存一份、DW 里拆成事实表和维度表又存一份;一个客户只是改了个手机号,维度表就多出一行;一张订单表存了五年,真正“当前有效”的只有结束日期为 9999-12-31 的那一行。这些放在业务库里要被 DBA 骂设计失误的操作,在数据仓库里全是刻意的。它们换来的是两样东西:数据出错时能回溯到源头,以及报表查询时不用反复 join 业务库。搞懂这两件事为什么需要“浪费”存储,就搞懂了数仓分层、缓慢变化维、拉链表这一整套设计的共同动机。

2. 数仓分层:STG、ODS、DW、DM 为什么值得把同一份数据存四遍

分层的本质是隔离责任。业务系统一旦出问题,DBA 的第一反应是“把数据改回去”;数仓出问题,第一反应是“重跑一遍”。这两种反应要求的数据形态完全不同——前者要求唯一真相,后者要求过程可追溯。四层模型就是把这个要求逐级落地的结果:STG 负责“先原样存下来”,ODS 负责“存全历史”,DW 负责“洗干净并对齐口径”,DM 负责“按业务问题预聚合”。每一层都在替上一层扛住一类变化,代价就是存储和延迟。

原文对四层作用的概括很直接:划分 ETL 阶段工作重心,便于管理。这句话听着像废话,但展开之后,每一层其实都在解决一个具体到能写出工单的问题。

2.1 STG 层存在的理由:先存下来,再讨论怎么处理

STG 层的定义是“与源系统模型一致,增量或全量抽取,不做数据转换,只添加审计列”。四个作用分别是:减轻源系统压力、数据备份支持重跑、便于数据问题跟踪、数据质量检查。这四条里最值钱的是第二条。

想象一个场景:源库是一套跑了十年的老系统,某天凌晨跑批到一半,清洗逻辑写错了一个 case when,把某个状态码映射错了。如果数仓直接从源库读、洗完直接进 DW,那这一批数据就是永久错的——源库的数据早就被业务改过几轮了,你连“原始长什么样”都拿不回来。STG 存在的意义就是那颗后悔药:原始数据在 STG 里躺了 7 天到一个月,这段时间内任何下游逻辑翻车,都能从 STG 重跑,不用再去骚扰业务库。

开发步骤上,原文给的顺序是:确定 CDC 策略 → 设计 mapping 文档 → 设计物理模型 → 抽取 → 加载。物理模型这块有三条硬规矩值得单独拎出来:一是包含源系统所有字段加审计字段;二是去掉主键、索引、默认值这类约束;三是源库和目标库类型不同时,字段长度要扩充、类型收敛到少数几种固定长度。第三条是血泪经验——源库一个 varchar(20) 的字段,业务悄悄塞了 25 个字符进去,抽取时直接截断,等你发现时下游报表已经跑了三个月。

-- STG 层物理模型的典型形态:字段原样保留,约束全部去掉 create table stg_order ( order_id varchar(64), -- 源库长度 32,异构同步统一扩到 64 cust_id varchar(64), order_status varchar(32), -- 源库是 char(2),这里统一成 varchar amount decimal(18, 4), -- 金额精度统一,避免各源库小数位不一致 -- 以下为审计列,四列固定 src_sys_name varchar(32), -- 源系统名,用于血缘追溯 src_table_name varchar(64), -- 源表名 load_time timestamp, -- 本批加载时间 load_type varchar(16) -- 全量 full / 增量 incr );

这段 DDL 里没有主键、没有 not null、没有 default,是故意的。STG 层的职责是“尽量别丢东西地接住数据”,任何约束都可能在源数据脏的时候让整批加载失败,而这时候你恰恰最需要把脏数据先落下来看看长什么样。审计列那四列是全链路排查的锚点:一条数据从哪来、哪张表、什么时候进的、怎么进的。

2.2 CDC 策略与增量抽取:时间戳法为什么经常会漏数据

CDC(变更数据捕获)策略是 STG 开发的第一步,也是后面所有增量逻辑的地基。常见的实现思路大致分四类:全量抽取(每次整表拉)、时间戳增量(按业务系统的更新时间字段取大于上次最大值的记录)、基于日志的增量(读数据库的变更日志)、全表比对(拉全量后和目标表逐行比)。原文没有展开这几种的技术细节,但选型逻辑很清晰——看源系统的数据状况。

时间戳法是应用最广的一种,也是最容易翻车的一种。写法上大概是这样:

-- 增量抽取:以 ODS 中已有的最大时间为水位线 insert into stg_order select order_id, cust_id, order_status, amount, 'order_sys' as src_sys_name, 't_order' as src_table_name, current_timestamp as load_time, 'incr' as load_type from src_order where update_time > ( -- 水位线:上一批已抽取的最大更新时间 select coalesce(max(update_time), '1970-01-01 00:00:00') from ods_order );

这段 SQL 的问题藏在两个地方。第一,update_time必须由业务程序在每次更新时可靠维护,一旦某个老接口不走这个字段,那批变更就永远抽不到;第二,水位线用>而不是>=,当同一秒内有多条记录、而上一批恰好只抽走了其中一部分时,剩下的会被永久跳过。我一般会这么处理:水位线往前回溯一段时间(比如 5 分钟)做重抽,配合下游按主键去重,用一点重复换不漏数。这就是典型的“宁可重复,不可丢失”取舍——数仓里的重复可以在 DW 层用主键消掉,丢失则没有任何下游逻辑能补回来。

2.3 ODS 层:逻辑删除位和增量时间戳在补什么洞

ODS 和 STG 的模型大体一致,但多了两个关键字段:逻辑删除标记和增量时间戳。这两个字段看着不起眼,实际是在补两类结构性缺陷。

逻辑删除位补的是“物理删除”这个洞。很多源系统确实有逻辑删除标记,但后台运维、数据订正脚本、甚至某些历史遗留的存储过程,会绕过应用层直接 delete 数据。数仓如果是全量同步,这一行就凭空消失了;如果 ODS 只保留“当前有效数据”,那你连“这里曾经有过一条记录”都不知道。加上删除标记之后,每次加载做一次比对:源库有的、ODS 没有的,标为上删除;源库没有的、ODS 里还是有效的,打上删除标记但数据留着。这样 ODS 才真正做到了“全量存储源系统的数据”。

增量时间戳补的是“无法判断新旧”这个洞。源系统不一定有可靠的更新时间字段,ODS 层自己维护一个加载时间戳,至少能保证“这条记录是哪一批进来的”可查。

ODS 的三个作用原文写得很明确:全量存储源系统的数据、支持下游系统实时查询服务、数据质量检查。第二条值得多说一句——ODS 的模型和源系统一致,意味着它是唯一一个“业务方看得懂”的层。很多临时取数需求、对账需求,直接从 ODS 出比从 DW 出快得多,也少一层口径解释成本。

2.4 DW 与 DM:清洗、规范化、聚合分别落在哪一层

DW 层是真正开始“变形”的地方:维度模型、增量抽取、数据清洗、规范化、添加审计列。这里的关键判断是“什么该在 DW 做,什么不该”。

清洗和规范化放 DW,原因是这两件事都需要跨源系统视角。原文举的例子很典型:性别字段在不同源系统里可能是 0/1,也可能是 man/woman,规范化就是建一张映射表,统一成 m/f。这种统一只能在一个能看到全部数据源的层做,放在 STG 就变成了“每个源各洗各的”,最后汇总时依然对不齐。规范化的三个步骤——数据标准化、删除重复数据、数据共存(同一客户号在两个系统都有联系方式时按业务规则选一个)——都依赖这个全局视角。

DM 层则相反,它是“按问题预聚合”。原文给了三种建聚合表的方法:增量加载聚合表(直接从基础事实表增量算)、聚合导航(用户请求时由报表工具把基础事实表自动改写成聚合查询)、物化视图(定时刷新)。还提到要建“缩小维度表”——因为聚合事实表的粒度和基础事实表不同,得配一张同粒度的维度表,只保留聚合需要的字段。

这里有个容易忽略的约束:聚合表的粒度必须和它的维度表粒度严格一致。见过不止一次,聚合表按“月 + 产品”聚合,维度表却还是“日 + 产品”粒度,关联之后数据被放大数倍,报表数字看着“挺合理”,实际全错。

2.5 审计列与 ETL 日志:出问题时拿什么查

审计列在四层里是贯穿的:源系统名、源表名、加载时间、加载方式。这四列看着重复,实际是数据血缘的最小可用集。出问题时排查路径大致是:报表数字不对 → 找到 DM 聚合 → 找到 DW 事实表 → 找到 ODS 对应记录 → 用审计列定位到 STG 的批次 → 找到源系统。

ETL 日志分三类,这个分级方式直接决定了排障效率。执行过程日志是流水账:每一步的起止时间、影响行数;错误日志只记录出错时间、出错模块、出错信息;总体日志最粗,只记开始时间、结束时间、是否成功。三类日志的读者不同——总体日志是给值班监控看的,错误日志是给开发定位的,执行过程日志是给性能优化用的。如果只留一类,一定是留执行过程日志,因为它能被降维成另外两类。

告警机制上,原文提到出错时给系统管理员发邮件附上错误信息。这事儿的坑在于告警的粒度:按批次告警会漏掉“批次成功但某一步处理了 0 行”这种隐性失败,按步骤告警又会淹没在正常信息里。常见的做法是给每一步配一个“预期行数区间”,偏离区间就告警,比单纯判断成功/失败有效得多。

3. 维度建模:事实表、维度表、粒度这三件套是怎么咬合的

维度建模的目标只有一个:让业务人员能用自己的语言问出问题。业务问的是“上个月华东区某品类卖了多少”,模型里就得有“时间、地区、品类”三个维度围着一个“销售额”事实转。星型模型之所以叫星型,就是因为事实表在中间、维度表在四周,一眼能看出查询路径。这套结构的每一个设计决定,追到根上都是在回答同一个问题:粒度的边界划在哪。

3.1 粒度是建模的第一个决定

原文对粒度的定义是:数据仓库的数据单位中保存数据的细化或综合程度的级别,细化程度越高粒度级越小。这句话的技术含义是,粒度决定了事实表一行代表什么。

粒度定错,后面全是返工。定得太细(比如订单行级 + 每次状态变更),事实表行数爆炸,查询慢;定得太粗(比如按天按品类汇总),一旦业务想看单笔订单,数据就没了。常见的实现思路是先按业务过程的最细原子粒度建,聚合需求交给 DM 层解决——因为从细到粗可以随时聚合,从粗到细永远补不回来。

配套的概念是层次(Hierarchy),它描述明细数据的层级关系,比如“年 → 月 → 日”“国家 → 省 → 城市”。层次本身不是粒度,它是维度表内部的组织方式,决定了钻取的路径。粒度和层次的关系,可以理解成:粒度决定事实表一行多“细”,层次决定维度表能往上“卷”几层。

3.2 事实表的四种形态与各自的 ETL 时点

原文把事实表分成四类,这个分类的价值在于每类的 ETL 加载时机完全不同。

事务粒度事实表(可加事实)是在特定时间、空间点上的一次瞬间测量,最典型的是销售单。它的来源伴随交易事件产生,ETL 时以原子粒度直接迁移,不做任何聚合。这类表可以直接 sum、count。

周期快照事实表(半可加事实)表现的是一段时间或规律性重复,最典型的是账户余额、财务报表。它按固定时间间隔生成累计数据,常见粒度是月。注意“半可加”这个词:余额可以跨账户加总,但不能跨时间加总——把 1 月到 12 月的余额加起来毫无意义。

累积快照事实表(不可加事实)描述有明确开始和结束的过程,比如合同履行、保单受理、工单流转。它的粒度是一个实体从创建到当前状态的完整历史,ETL 时随着业务步骤推进逐步补全同一行记录,不是每次新增一行。这类表最容易和事务表搞混——工单创建是一条事务,工单完结不是另一条事务,而是同一行上的字段被填满。

无事实事实表没有具体测量值,只记录“某件事发生了”,比如“某用户某天访问了某商品页”。它存在的意义是支撑多对多关系分析,行数往往巨大但没有一个可加的度量。

原文里有一句必须记牢:一张事实表只能有一个粒度,不能把不同粒度的事实塞进同一张表。混粒度之后,任何 sum 都失去意义,因为不同行代表的东西根本不是一回事。

3.3 代理键与 -1 记录:事实表怎么找到维度

维度表的主键分两种:业务主键(源系统里的 ID)和代理键(数仓自己生成的、无业务含义的整数)。事实表里存的必须是代理键。

理由不复杂:业务主键会变,而且不同源系统的同一实体可能有不同编码(原文举的例子是同一个供应商在结算系统是 xx0001、在 CRM 是 yy0001)。代理键是一个和业务完全解耦的自增整数,宽度小、join 快,而且天然支持缓慢变化维——同一个业务主键的多条历史记录,各自有独立的代理键。

代理键的生成有两种做法:ETL 工具或数据库的自增长字段。原文提到的“首次加载时要有一条代理键为 -1 的默认记录”是个很容易被忽略的细节。这条记录的作用是兜底:当事实表里的某个维度值在维度表里找不到对应记录时(迟到数据、脏数据、维度加载顺序出错),关联会落空,代理键变成 null。null 参与 join 和聚合会带来一堆难以预料的行为,用一个明确的 -1 表示“未知”,至少能让报表里出现一个可统计的“未知”分类,而不是让整行数据悄悄消失。

-- 代理键查找:用查找维表把业务主键映射成代理键 -- 查找维表只存业务主键和代理键的映射,且只存当前有效版本 insert into dw_fact_sales (order_id, cust_sk, prod_sk, sale_amt, dt) select o.order_id, coalesce(c.cust_sk, -1) as cust_sk, -- 找不到就打 -1,不让 null 进事实表 coalesce(p.prod_sk, -1) as prod_sk, o.amount, o.dt from ods_order o left join dim_cust_lookup c on o.cust_id = c.cust_biz_id and c.end_date = '9999-12-31' -- 只取当前有效版本,否则一单变多单 left join dim_prod_lookup p on o.prod_id = p.prod_biz_id and p.end_date = '9999-12-31';

这段里面and c.end_date = '9999-12-31'这个条件是整条 SQL 的命门。少了它,一个客户如果经历过三次地址变更,维度表里有三条历史记录,关联之后一笔订单会被复制成三笔,销售额凭空翻三倍。这个错误在数据量小的时候看不出来,上了量之后报表数字会“稳定地错”,非常难查。

3.4 星型与雪花:冗余换来的是哪一部分性能

星型模型是所有维度表直接连到事实表,维度表不再往外挂表。雪花模型是维度表还连着维度表,形成层次。两者的区别只有一句话:维度表是直接连事实表,还是通过其他维度表连过去。

星型是反规范化的结构,维度数据有冗余,但正因为冗余,统计时不需要做外部关联查询,效率通常比雪花高。雪花减少了冗余,代价是每次查询都要多 join 几层。

原文给了一句判断标准:在冗余可接受的前提下使用星型模型。这句“可接受”怎么量化?我一般这么理解:如果维度表的冗余会导致更新异常(改一处要改很多行、且容易漏改),那就雪花;如果维度是相对静态的,比如产品分类、地区层级,几年才动一次,那冗余带来的维护成本几乎为零,直接用星型。雪花模型一般在处理“大的且相对静态的层次”时使用,这个描述很准确——层次深、变化少,才值得为它多 join 几次。

3.5 大维、迷你维、退化维的边界

这三种维度类型解决的是不同规模下的性能问题。

大维度指记录数极大、字段极多的维度,比如客户维度上百万记录、上百个字段。它的问题不是存储,是浏览性能:不带限制条件地查维度属性、带多个限制条件地查、以及事实表关联大维度后的查询,都会变慢。优化手段是高效的设计方法 + 正确的索引。

迷你维是把大维度里最常用的少数字段单独拆出来形成的小维度表,查询时先用迷你维过滤,命中之后再回大维度取详细信息。这个设计的本质是把“高频访问的少量属性”和“低频访问的大量属性”分开,减少每次查询的 IO。

退化维是最有意思的一种:它本身是一个维度,但不单独建表,而是直接作为一个字段留在事实表里,最典型的是订单号。订单号确实是分析维度(可以按订单号查、按订单号去重计数),但它没有描述性属性,单独建一张一行一个订单号、没有其他字段的维度表毫无意义,于是它退化到事实表里。

维度还有几种组织形态值得区分:普通维基于单个维表,用不同列表示不同级别;雪花维基于多个维表,外键关联,分别存储不同级别的成员;父子维基于同一个维表的两列(成员键和父键)表达隶属关系,用于组织架构这类层次不固定的场景。父子维有个特殊约束——因为自引用关系复杂,如果按缓慢变化维的全历史方式处理,逻辑关系会非常混乱(一个节点改名,下面所有节点的路径都要跟着变),通常按缓慢变化维的覆盖方式处理,只判断新增还是修改。

4. 缓慢变化维与拉链表:历史快照怎么存才不炸

维度会变,这是无法回避的现实。员工升职、客户换地址、产品改价格,每一次变化都带来一个选择:新值覆盖旧值,还是保留旧值?覆盖的话历史无法回溯,保留的话表会一直变大。缓慢变化维的三种类型和拉链表,本质上是同一个问题在不同数据量级下的三种答案。

4.1 SCD Type 1/2/3 的取舍本质

Type 1 是直接覆盖:新值替换旧值,表结构不变,行数不变。优点是简单、查询快、没有冗余;缺点是历史彻底丢失,而且这个丢失是静默的——没人会知道这条记录曾经是什么。

Type 2 是增加新行:把当前行的状态置为 off、打上结束时间戳,同时新增一行状态为 on、开始时间戳为上一行结束时间 +1。优点是完全保留历史,支持任意时间点回溯;缺点是行数随时间线性增长,而且所有下游查询都必须带时间过滤条件,漏一个就数据翻倍。

Type 3 是增加新列:给表加一列存新值,原列保留。优点是能同时看到“当前”和“上一个”两个状态,行数不变;缺点是只能保留有限次数的历史,想追三次以上的变化就得继续加列,表结构会越来越宽。

三者的选择其实是在回答:你需要回滚几个版本。只要一个版本,Type 3 够;需要任意时间点,只能 Type 2;完全不需要历史、只要当前状态,Type 1。实际项目里 Type 1 和 Type 2 最常见,而且经常混用——比如客户的手机号用 Type 1 覆盖(错号改了就是改了,没有分析价值),客户的等级用 Type 2 保留(等级变化是有分析价值的)。

4.2 拉链表的两条时间链

拉链表是 SCD Type 2 在数据量大的场景下的具体实现形态。它的结构很简单:一张历史表(比如 dw_order_his)带 start_date 和 end_date 两个字段,外加一张 ODS 层的增量变更表(ods_order_update),只存发生变化的数据(insert 和 update)。

核心操作只有两个,而且必须成对出现。

封链:拿增量变更表和拉链表做交集,命中的记录把 end_date 从 9999-12-31 改成当前日期。这条旧链就关闭了,它代表的“那段时间的状态”被固化成一段封闭区间。

开链:把增量数据整体插入历史表,start_date 设为当前日期,end_date 设为 9999-12-31。这条新链就打开了,代表“从今天起的状态”。

理解了这两步,拉链表的所有特性都能推出来。查询某一时刻的状态,条件是start_date <= 目标日期 and end_date > 目标日期;查询当前状态,条件就是end_date = 9999-12-31。存储上,一个每天变一次、存了五年的实体,占 1800 多行;如果用全量快照,是 1800 × 实体数 行。这就是它“兼顾历史与空间”的全部含义。

4.3 一段可复现的封链开链 SQL

-- 拉链表 dw_order_his:order_id, cust_id, order_status, start_date, end_date -- 步骤一:封链。把即将发生变化的旧链关掉 insert overwrite table dw_order_his select a.order_id, a.cust_id, a.order_status, a.start_date, -- 命中了当天的增量,就以今天关链;没命中说明这条链继续有效 case when b.order_id is not null then '${bizdate}' else a.end_date end as end_date from dw_order_his a left join ( -- 增量表里同一主键可能有多条,先去重,避免一链被封两次 select order_id from ods_order_update where dt = '${bizdate}' group by order_id ) b on a.order_id = b.order_id where a.end_date = '9999-12-31'; -- 只处理开着的链,已封的绝不能再动 -- 步骤二:开链。增量数据整体以新链形式插入 insert into table dw_order_his select order_id, cust_id, order_status, '${bizdate}' as start_date, '9999-12-31' as end_date from ods_order_update where dt = '${bizdate}';

这段 SQL 里有三个地方决定成败。第一,where a.end_date = '9999-12-31'限制了封链范围,已经封掉的链绝不能再改,否则历史区间会互相覆盖,出现一段时间的状态有两个版本。第二,增量表做了 group by 去重,因为同一订单一天内可能更新多次,如果不去重,同一个主键会命中一次,封链只封一次没问题,但开链会插入多行——这有时是想要的(保留一天内的多次变更),有时不是,得看粒度定义。第三,开链时没有做“全字段比对”,严格来说应该先排除那些“字段完全没变、只是被增量同步带过来”的记录,否则每天都在无意义地开关链,表会虚胖得很快。

原文提到“全字段对比源数据和目标表得出真正的增量数据,这里全字段不包含 start_date 和 end_date”。这条规则很关键:判断是否变化时,时间字段必须排除在比对之外,否则每条记录都是“变化的”。

4.4 拉链表变大之后:索引、裁剪与快速变化维

拉链表跑了五年之后,查询性能会成为新问题。原文给了两条思路:在查询引擎里对 start_date 和 end_date 做索引;保留部分历史数据,比如底层存全量、对外暴露一张只提供近 3 个月数据的拉链表。

第一条思路的有效性取决于引擎——支持索引的引擎上效果明显,纯列存扫描的引擎上收益有限,更实际的办法是按日期或业务线做分区裁剪,让查询先落在少数分区里。第二条思路本质上是冷热分离,把“回溯五年”这种低频需求和“看近三个月”这种高频需求分开,成本和体验都能兼顾。

还有一类特殊场景需要单独处理:快速变化维。当某个维度的变化频率极高(比如商品的实时价格、用户每次搜索的关键词),Type 2 会让行数迅速膨胀。原文给了三档处理方式:维度表字段少、数据量小的,Type 2 就够;字段多、数据量大的,Type 2 会显著降低效率,改用 Type 3 只保留前一值和当前值;最糟糕的情况是字段又多、数据量又大、且只有一部分字段频繁变化,这时候要把相对稳定的字段和频繁变化的字段拆开,频繁变化的独立成新维度表。这一招叫维表分离,判断标准很清晰:变化频率不一致的字段,本来就不该住在同一张表里。

5. 常见问题与排查:面试答得出来、上手就翻车的五个地方

5.1 拉链表重跑一次,链断了或者数据翻倍

现象:任务失败后手工重跑同一批次,拉链表里同一个 order_id 出现两条 end_date 为 9999-12-31 的“当前记录”,或者某个时间区间直接缺失。

原因:封链和开链不是一个原子操作。重跑时如果只执行了开链没执行封链(或者反过来),状态就不一致了。更隐蔽的一种是封链操作基于当日增量表重新算了一遍,而增量表在重跑前已经被清空。

解决:把封链和开链包在同一个事务或同一个任务节点里,全成功或全回滚;重跑前先按批次回滚——用备份分区或按 start_date 等于批次日期删除后重算。我给所有拉链表任务都加了一条硬规矩:开链的插入必须用left anti join排除掉已存在的同批次同主键记录,把幂等写在 SQL 里,而不是靠调度系统保证。

5.2 事实表关联维度后查不到数据,或者出现大量 -1

现象:报表里出现大量“未知客户”,或者某些订单在事实表里有、关联维度后消失了。

原因:三种可能。维度表缺少代理键为 -1 的默认记录,导致 null 兜底失败;查找维表没有覆盖全部历史版本,某个早期业务主键找不到映射;或者维度加载晚于事实加载,事实表先跑完,维度还没进来。

解决:初始化时强制插入 -1 默认记录,这是建表脚本的一部分;加载顺序严格按“子维度表 → 维度表 → 桥接表 → 事实表”来(这个顺序由主外键关系决定,属于总线架构的基本约束);事实表加载完成后跑一次覆盖率检查,count(cust_sk = -1) / count(*)超过阈值就告警。

5.3 STG 层“顺手”做了转换,出问题时没有后悔药

现象:下游发现某字段口径错误,想回原始数据重算,结果 STG 里存的已经是转换后的值。

原因:开发图省事,把清洗逻辑提前到 STG,理由是“少存一份中间数据”。

解决:STG 层严格不做数据转换,只加审计列。原文对 STG 的定义里“不做数据转换”是硬约束,破坏它的代价不是当下可见的,而是在第一次重大口径调整时才暴露。清洗放 ODS 之后、DW 之内,转换放 ODS 到 DW 的过程中——这是原文给的分工:从业务系统到 ODS 做清洗过滤脏数据,从 ODS 到 DW 做业务规则的转换和聚合。

5.4 增量抽取漏数,而且漏得毫无规律

现象:每天的对账差异不大不小,集中在某几个时段,补跑一次能补回来一部分。

原因:时间戳水位线用>而不是>=,同一秒内的多笔变更只取到了其中一部分;或者源系统的 update_time 在某些批量操作里没有被更新;或者长事务的提交时间晚于抽取开始时间,导致这一批数据“当时还没出现”。

解决:水位线回溯一段时间做重叠抽取,下游按主键去重;对源系统的关键表做一次完整性抽检,比对源表行数和 ODS 行数;长事务问题只能靠延后抽取窗口或者改用日志级 CDC 解决。

5.5 大数据量下排序和全表扫描把跑批时间拖到天亮

现象:任务平时跑 20 分钟,某天开始跑 6 小时还不结束。

原因:增量逻辑失效退化成全表扫描;或者某个聚合步骤对事实表做了全局排序;或者小文件太多导致每个 task 的启动开销吃掉了大部分时间。

解决:原文给的 ETL 优化清单基本覆盖了这块——关联查询前先过滤掉无效数据、只查需要的列、大数据量尽量避免排序、加载时关闭日志、用分区和并行、能增量就别全量、复杂查询拆成多个子任务、把多个查询共用的结果集单独建临时表。这里最有效的一条是“关联前先过滤”:把过滤条件推到 join 之前,能让参与关联的数据量少一个数量级,比任何引擎参数调优都直接。

6. 设计取舍:所有方案都在做同一笔交易

把这套东西从头看一遍,会发现分层、代理键、缓慢变化维、拉链表、星型雪花,做的其实是同一笔交易:用存储和复杂度,换可追溯性和查询效率。

STG 多存一份原始数据,换的是重跑能力;ODS 存全量历史加删除标记,换的是“数据曾经存在过”这个事实不被抹掉;SCD Type 2 让维度表行数线性增长,换的是任意时间点的状态可回溯;星型模型接受维度冗余,换的是少几层 join;拉链表把每天全量快照压缩成区间链,是在存储和查询复杂度之间找了一个折中点。没有哪个方案是“更先进”的,只有“这笔交易在你这个场景里划不划算”。

判断划不划算,最实际的办法是定期跑一遍自检。拉链表最关键的一致性检查是这条:

-- 检查 1:同一业务主键在任意时间点只能有一条开着的链 select order_id, count(*) as cnt from dw_order_his where end_date = '9999-12-31' group by order_id having count(*) > 1; -- 检查 2:链区间必须首尾相接,不能有空洞也不能重叠 select order_id, start_date, end_date, lead(start_date) over (partition by order_id order by start_date) as next_start from dw_order_his having end_date <> coalesce(next_start, '9999-12-31');

这两条 SQL 跑出任何一行,说明历史已经不可信了,越早发现越好修——等到业务方拿着报表来问“为什么上个月的数这个月变了”,回溯成本就是几十倍。

从那以后,我每次上线一张新的拉链表之前,都会强制把这两条自检跑一遍,并且写进调度里每天跑,告警直接发到负责人。倒不是不信任自己写的 SQL,而是历史快照这种东西,出错的时候是静默的:报表不会报错,只会安静地给出一个错误的数字,然后被拿去做决策。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 13:59:44

Unity建模新姿势:ProBuilder高效3D创作指南

1. Unity也能“建模”&#xff1f;揭秘3D创作新姿势上周在项目里遇到个需求&#xff1a;需要快速生成一批风格化的3D建筑模型。当我正纠结要不要开Blender时&#xff0c;同事突然来了句"为啥不用Unity直接做&#xff1f;"当时我就愣住了——Unity不是游戏引擎吗&…

作者头像 李华
网站建设 2026/10/28 13:59:40

SATA协议中ATA与ATAPI设备识别机制详解

1. SATA协议中的设备识别机制解析在存储设备通信领域&#xff0c;SATA协议作为并行ATA技术的继任者&#xff0c;其设备识别机制直接影响着主机与存储设备的互操作性。每次当我们连接硬盘或光驱时&#xff0c;系统后台都在执行一套精密的识别流程&#xff0c;而ATA_CMD_ID_ATA和…

作者头像 李华
网站建设 2026/10/28 13:58:58

园区碳排闭环管理:从能耗统计到精准核算

1. 从能耗统计到碳排闭环&#xff1a;园区管理的范式转变我清楚地记得三年前第一次接触园区碳管理项目时的场景。当时客户拿着厚厚一沓Excel表格自豪地展示&#xff1a;"我们园区所有设备的能耗数据都统计得清清楚楚&#xff01;"但当问及"这些能耗对应多少碳排…

作者头像 李华
网站建设 2026/10/28 13:57:10

ECC深度解析:从椭圆曲线密码学到SAP LSMW操作实战

1. 从“ECC”这三个字母说起&#xff1a;它到底是什么&#xff0c;为什么总被反复提起第一次看到“ECC”这个标题&#xff0c;很多人脑子里会蹦出完全不同的东西。做密码学的会想到椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;&#xff0c;做企业信息化…

作者头像 李华
网站建设 2026/10/28 13:55:49

Servlet生命周期与线程安全深度解析

1. Servlet核心概念与生命周期解析Servlet作为Java EE体系中最基础的Web组件&#xff0c;其生命周期管理是每个Java Web开发者必须掌握的硬核知识。我在实际企业级项目开发中发现&#xff0c;90%的初级开发者对Servlet生命周期的理解仅停留在"init-service-destroy"的…

作者头像 李华
网站建设 2026/10/28 13:55:36

技能系统拆解:从底层素养到场景技巧的工程化实践

1. 从“skills”这个词说起&#xff1a;为什么我决定认真拆解它“skills”这个词&#xff0c;乍一看太普通了。它不像某个框架名、某个工具链、某个报错代码那样自带技术光环&#xff0c;搜索引擎里一搜&#xff0c;出来的结果也大多是泛泛而谈的职场文章。但恰恰是这种“普通”…

作者头像 李华