☰
银行数据仓库建设实战:分层建模、ETL调度与数据质量保障
2026/10/15 16:12:36 网站建设 项目流程

简介:上海银行数据仓库建设情况是一份面向银行IT、数据仓库架构师及金融科技学习者的参考文档,系统呈现上海银行一、二期数据仓库平台的建设全貌。文档从银行业务报表统计与监管报送的痛点切入,重点介绍通过ETL将T24对公对私、储蓄、国际业务、信用卡、个贷、信贷等系统数据整合进基础数据平台,并基于FS-LDM逻辑模型建立单一业务视图,支撑反洗钱分析、1104非现场监管报表、CEO-DASHBOARD、智能取数等应用。同时梳理了源数据提供层、ETL处理层、应用层三层物理架构,对报表口径统一、数据质量管控、监管合规分析等难题给出可落地思路。资源打包为1个docx文档,压缩包大小仅289KB,便于二次编辑与打印保存。目前已有619人学习下载,适合作为银行数据仓库项目立项设计、技术选型或方案汇报的参考资料。

1. 银行数据仓库建设情况:先搞清楚它在解决什么问题

谈到银行的数据仓库建设,很多人的第一反应是先选一套数据库产品,或者先把源系统接口都接进来。但我实际接手某城商行的数据仓库建设时,听到最多的需求其实是两句大白话:月底的报表能不能早点出,报送口径的数据能不能别再手工拼。这两句话背后是一整条链路:核心账务、信贷、渠道、支付、卡系统,几十套系统每天产生几亿条流水,白天支撑交易,凌晨留出三四个小时窗口,要把前一天的数搬进数仓、清洗、建模、汇总,再喂给几百张报表和下游系统。所以数据仓库建设在银行里从来不是单纯的技术选型,它解决的是口径一致、时效可控、历史可追溯三个问题。这篇内容适合正在搭数仓或者刚接手银行数仓的开发、建模、运维同学,我会从架构分层、ETL调度、数据质量到常见故障,按一线实操顺序一次讲透。

2. 数仓分层设计与模型规范:贴源、明细、汇总到底怎么划分

2.1 分层为什么是银行数仓的第一件事

银行源系统数量多、上线时间早、接口风格各不相同。核心系统还是十几年前的老业务,字段命名看不太懂;渠道系统又是另外一套标准,更别说信贷、理财这类后来加的系统。如果不做分层,直接让报表去拉源系统,会出现一个典型场面:同一个客户号,在A报表里取的是核心系统的表,在B报表里取的是渠道侧的表,两边字段名叫法不一样,数据对不上,业务找科技吵架。

分层的本质是把“贴近源系统”和“贴近业务”分开。银行数仓现在的主流做法是五层结构:STG临时落地层、ODS操作数据存储层、DWD明细层、DWS汇总层、ADS应用层。STG只负责把源数据搬进来,不承担任何加工;ODS负责保留历史,让源系统和数仓解耦;DWD做清洗、标准化、维度退化,是数仓建模的主体;DWS按客户、机构、产品等主题汇总;ADS直接对接报表和外部系统。每层职责一旦乱了,后面全是补丁。

我一般会把ODS和DWD的边界卡得很死:ODS里字段和源系统保持一致,DWD里才允许改字段名、补默认值、统一编码。这样源系统变更时,DWD以下的模型可以不动,影响面被控制在ODS到DWD这一段。这个设计对银行特别重要,因为银行源系统变更流程很重,不可能为了数仓去改源,只有让数仓的模型层足够稳定,才能扛住上游的反复变化。

如果分层没定好,最常见的后果就是“贴源表被一百张报表直接引用”。源系统一个字段类型调整,报表第二天就错位,数仓团队被迫进入全天候救火。分层看上去多花了存储和加工时间,实际上是在给后续所有需求买保险。

2.2 各层命名规范:一份可以直接抄的清单

分层定完之后,第一件事就是定命名规范。没有规范之前,某张表叫“客户信息表(最终版)”,第二版叫“客户信息表(最终版2)”,第三版直接叫“客户信息表最终版改”。这种名字在银行数仓里是灾难,因为下游报表一多,根本不知道引的是哪一份。

我习惯用“前缀+主题+粒度+频率后缀”的命名方式。前缀代表层,主题代表业务域,粒度代表主维度,后缀代表更新方式。拿客户信息举例:

层前缀命名示例职责
临时落地层stg_stg_cust_info_di数据抽取落地,与源结构一致,按批清空
操作数据存储层ods_ods_cust_info_da保留源系统全量历史,按日分区
明细层dwd_dwd_cust_info_da清洗去重、统一编码,面向主题组织
汇总层dws_dws_cust_asset_sum_da按客户、机构等粒度轻度汇总
应用层ads_ads_rpt_cust_asset_da面向报表和下游推送的最终结果

后缀我也固定下来:_di表示按批增量、_da表示按日分区写入、_df表示每日全量快照、_mi表示月度增量。命名上区分增量和全量,是为了排查问题时一眼看出这张表的历史保留策略;同时要禁止任何“test”“bak”“new”“old”这类词出现在正式表名里。规范不用多,五条以内,能卡住主要问题就够了,写多了反而没人执行。

命名规范定了以后,还要同步定分区字段和主键规范。我习惯统一用dt表示数据日期分区,用biz_date表示业务日期,用etl_time表示跑批完成时间。这三个字段在各层表里都必须有,否则做数据质量校验和追数的时候,根本不知道这条记录是哪一天、哪个批次进来的。

2.3 维度建模落地:从客户维度表到拉链表

选型阶段最常见的争论是:用三范式建模还是维度建模。银行传统数仓很多是范式建模,ER图画得很漂亮,但出报表要join十几张表,性能扛不住。我现在的做法是混合式:ODS保持贴源,DWD里把核心维度做成维度表,事实表按需要关联,少量高热度维度直接退化进事实表。

客户维度是银行数仓里最典型也最难做的一张表。一天之内客户改名、换证件、改手机号都可能发生,下游对账和审计要看某个历史时点的客户信息,直接覆盖更新就丢历史了。所以银行客户维度我一般做成拉链表,用生效日期和失效日期保存历史版本:

-- 客户维度拉链表,Hive风格语法,Oracle环境请自行替换字段类型与分区写法 CREATE TABLE dwd_dim_customer_da ( cust_id STRING COMMENT '客户号', cust_name STRING COMMENT '客户名称', cust_type STRING COMMENT '客户类型', cert_no STRING COMMENT '证件号码', eff_dt STRING COMMENT '生效日期', end_dt STRING COMMENT '失效日期', is_current TINYINT COMMENT '是否当前版本,1当前,0历史' ) PARTITIONED BY (dt STRING COMMENT '数据日期') STORED AS ORC;

拉链表的逻辑说明:每天做一次关旧开新,把当天发生变化的客户旧记录置为失效,插入一条新记录。查询历史时点,直接按“业务日期在eff_dt和end_dt之间”过滤。这么做代价是存储翻了几倍,但对银行来说,客户关系历史是硬要求,审计和风险场景都得有,存储成本是可以接受的。

如果业务上确实只看当前状态,比如账户状态、产品信息这一类变化快但不需要追溯的,我建议不要硬上拉链,直接用每日全量快照,一天一个分区,既简单又抗变更。拉链表不是万能答案,只有真正有历史追溯需求的表才值得用。

2.4 模型评审时我会重点盯的三个点

分层和模型定了,代码也写了,最后一道关是评审。我在评审时一般只盯三个点。第一个是时间口径字段有没有混用。一张表里同时出现“业务日期”“会计日期”“跑批日期”“源系统更新时间”,如果程序里自己选一个来过滤,结果一定乱。我要求事实表必须能明确找到业务日期字段,并且全链路由调度统一传参,不依赖表里的隐性日期。

第二个是维度表是不是全局统一。客户维度在A项目一套、B项目又一套,C项目甚至连客户主键都不一样,这是银行数仓最常见的内耗。统一维度表不一定要做得完美,但必须是全行唯一,下游再有特殊场景,也只能基于这唯一一套去扩展。

第三个是宽表是不是真的需要。看到有人为了省事,把客户、账户、产品、机构全拼进一张大宽表,字段一百多个,问项目组谁来更新、谁负责,答不上来。宽表不是不能做,但一定要有明确的使用方和更新批次,否则后期字段冲突、数据不一致全来了,等于埋了个黑匣子。

3. 日终批量与增量抽取:一套能对抗凌晨三点的落地方案

3.1 批量窗口怎么排:从源系统到应用层的链路

银行数仓的主体是T+1日批,也就是当天处理前一天的数据。日批窗口排得合不合理,直接决定第二天早上报表能不能按时出。常见做法是把一天拆成几个时间段:源系统在晚间错峰卸数,数仓在凌晨集中加工,早上上班前把结果送出去。

我一般会按这个节奏排批量窗口:源系统卸数放在16点到22点,错开交易高峰;STG落地和ODS装载放在22点到次日1点;DWD清洗建模放在1点到3点;DWS汇总放在3点到5点;ADS报表和推送放在5点到7点。每个环节之间留30分钟缓冲,不做无缝衔接,因为银行晚上经常有源系统延迟,缓冲不够,一个环节延迟会击穿整个链路。

窗口排好之后,还要做容量估算。我通常用“高峰期数据量+上线一年后的增长系数”来验证每个环节,经验值是预留30%到50%的缓冲。如果某个环节实测要跑3小时,只给它排2小时,翻车只是时间问题。监控上要能区分“跑完了但延迟”和“还没跑完”,前者补时间,后者是性能问题,处理方式完全不同。

3.2 增量抽取的两种可靠方式与SQL模板

抽数方式决定了批量每晚要搬多少数据。银行有些源表一天几千万流水,全量抽取不现实,所以增量抽取是必须的。最可靠的方式是源系统的更新时间戳增量抽取,前提是源表里有一个由应用维护的update_time字段,并且每次变更都会更新。

-- 增量抽取:取业务日期T-1当天实际发生变更的记录 -- 调度传入两个参数:PRE_DATE=前一天,BIZ_DATE=当前业务日期 SELECT cust_id, cust_name, cust_type, update_time FROM source_system.cust_info WHERE update_time >= TO_DATE('${PRE_DATE}', 'yyyyMMdd') AND update_time < TO_DATE('${BIZ_DATE}', 'yyyyMMdd');

这段SQL的逻辑是:用update_time做半开区间过滤,[前一天零点,当天零点),保证一条记录不会被两个批次重复抽取。参数说明:PRE_DATE和BIZ_DATE由调度平台统一传入,严禁在代码里用SYSDATE自己算日期;update_time必须表示“业务变更时间”,而不是数据入库时间,否则源系统补数时会漏数据。

如果源系统没有可用的时间戳,或者时间戳精度不够,我一般改用全量抽取加MERGE的方式。思路是每天把源表全量拉进ODS,再用MERGE语句和DWD目标表做对比,有变化就更新,没变化就保留:

-- 全量抽取后通过MERGE完成增量更新,适用于无可靠时间戳且数据量可控的表 MERGE INTO dwd_customer t USING ( SELECT cust_id, cust_name, cust_type, update_time FROM ods_customer WHERE dt = '${BIZ_DATE}' ) s ON t.cust_id = s.cust_id WHEN MATCHED THEN UPDATE SET t.cust_name = s.cust_name, t.cust_type = s.cust_type, t.update_time = s.update_time WHEN NOT MATCHED THEN INSERT (cust_id, cust_name, cust_type, update_time) VALUES (s.cust_id, s.cust_name, s.cust_type, s.update_time);

MERGE的适用范围是千万行以内的维表和小流水表。超大流水表做MERGE性能很差,这种只能靠时间戳增量。

提示:银行源表里的update_time经常存在“系统批量更新时间”和“业务变更时间”两种含义,抽取前一定要和源系统负责人确认清楚,搞错一个,增量就漏一批。

3.3 调度重跑与幂等设计:失败后如何不翻车

批量调度最怕的是任务失败后盲目重跑。如果整个批次从头跑,前面已经成功的任务会重复消费数据,可能把目标表搞出重复;如果单独重跑失败的任务,又可能因为数据已经写到一半,产生半截数据。所以每个作业必须做幂等设计,我习惯用“先删后插”的固定模式:

-- 幂等重跑:先删除当前业务日期的目标分区,再执行插入 -- 注意删除条件是业务日期,不是跑批日期 DELETE FROM dwd_loan_acct_da WHERE dt = '${BIZ_DATE}'; INSERT INTO dwd_loan_acct_da ( cust_id, acct_id, loan_amt, dt ) SELECT cust_id, acct_id, loan_amt, dt FROM ods_loan_acct WHERE dt = '${BIZ_DATE}';

先删后插的逻辑说明:因为目标表按业务日期分区,重跑时先删除当天数据,再插入新数据,即使任务失败再重跑多次,最后结果也是一致的。删除条件必须用业务日期dt,不能用跑批日期,否则会把正式数据和补数数据混在一起。参数说明:${BIZ_DATE}来自调度平台,dt字段在DWD层统一等于业务日期。

调度层面,我会维护一张作业状态表,记录每个作业的名字、业务日期、开始时间、结束时间、状态。失败后重跑时,只从失败作业开始往下游重投,已经成功的上游作业不碰。状态表本身要在重跑前更新状态,避免同一个作业被两个实例同时跑。这张表不大,但它是整个批量的“日志”和“后悔药”,排查问题时第一眼就看它。

4. 数据质量与监管报送:数仓里两座必须填平的坑

4.1 数据血缘:为什么它是银行数仓的“底账”

数据血缘在银行数仓里不是可选项,而是硬需求。原因很简单:一张DWS汇总表可能被几十张报表引用,改动一个字段,影响面靠人肉根本记不住。我见过最典型的场景是,某开发改了一张底表的字段精度,自测通过,结果第二天报表对不上,查了一上午才发现是底下三层里的间接引用被影响。

所以从项目第一天就要建血缘登记机制。血缘的核心就四个信息:来源表、来源字段、目标表、目标字段。任何ETL作业上线前,必须登记一张映射表,否则不让发版。

-- 字段级血缘登记表,ETL作业上线前必须写入 CREATE TABLE meta_data_lineage ( source_table STRING COMMENT '来源表', source_field STRING COMMENT '来源字段', target_table STRING COMMENT '目标表', target_field STRING COMMENT '目标字段', etl_job STRING COMMENT '加工脚本名称', eff_date STRING COMMENT '生效日期', owner_dept STRING COMMENT '负责部门' );

血缘表做到字段级,是排查问题的关键。表级血缘只能告诉你哪个表被依赖,字段级血缘能告诉你这次改的字段会波动到哪几张报表。维护上不要指望开发自觉,要和发版流程绑定:没有血缘登记,代码不合并。我见过讲得很漂亮的血缘工具最后没人登记,反而是这种和流程绑在一起的最有用。

4.2 完整性校验SQL:每天开账前先跑一遍

数据质量校验如果等报表出来再人工对,等于没做。在银行数仓里,我习惯把校验脚本挂在日终批量末尾,开账前把该检查的都跑完,有差异直接拦截,不让问题数据流到第二天早上。

最基础也最不能缺的是条数校验:ODS抽进来了多少,DWD加工后剩多少,如果中间有丢失,第一时间能看到。下面是每日条数核对的一个简单写法:

-- 日终完整性校验:同一业务日期下ODS与DWD的客户表条数差异 -- 期望输出diff_count=0,非0即为加工丢失或重复 SELECT COALESCE(a.cnt, 0) - COALESCE(b.cnt, 0) AS diff_count FROM ( SELECT COUNT(*) AS cnt FROM ods_customer WHERE dt = '${BIZ_DATE}' ) a LEFT JOIN ( SELECT COUNT(*) AS cnt FROM dwd_customer WHERE dt = '${BIZ_DATE}' ) b ON 1 = 1;

条数校验只是门槛,还要加字段级校验。客户号不能为空、金额不能为负、证件号格式要对,这些规则可以抽象成一套通用脚本,用参数传表名、字段名和校验规则。例如非空率校验的思路是:

-- 关键字段非空校验:找出客户表中客户号为空或空串的记录占比 SELECT COUNT(*) AS total_cnt, SUM(CASE WHEN cust_id IS NULL OR cust_id = '' THEN 1 ELSE 0 END) AS null_cnt FROM dwd_customer WHERE dt = '${BIZ_DATE}';

参数说明:total_cnt和null_cnt的比值就是非空率,我一般设定99.9%作为阈值,低于阈值直接告警。金额类字段还要加“小于0”的规则,日期类字段加“不在业务日期范围内”的规则。校验结果落一张质量日报表,每天开账前第一件事就是看这张表,全绿再放行。

4.3 报表报送的应用链路:为什么容易前后不一致

银行数仓有很大一部分工作是为了满足对外报送,这类数据最烦的地方是同一个指标被不同部门重复取数,出来的结果却不一样。原因基本都在链路:有人从DWS取,有人直接查ODS,有人临时写SQL再手工加工,时间口径还不统一。

我把报送类需求一般归成三类,用不同链路处理:

报送场景推荐取数位置加工方式时效要求
常规报表DWS或ADS走成熟模型,直接出数T+1上午
专项统计DWD二次加工进报送集市,统一口径月末集中
临时数据核查ODS/明细层临时SQL,一次性取数按通知要求

为什么专项统计和临时核查最容易出问题?因为每次临时写SQL,都会把口径重新“解释”一遍,A同事理解的“客户数”和B同事理解的“客户数”可能差了一个过滤条件。解决方式只有一个:把共性指标收敛到ADS层,做成指标字典,指标编码、指标名称、计算公式、取数表全部固化。之后所有报表和报送需求只允许从指标字典里取,不允许自己再造一个口径。这个习惯需要业务和数据团队一起坚持,但一旦积累起来,对账的时间会大幅减少。

5. 银行数仓建设常见问题排查:四个让我通宵的典型故障

5.1 故障一:日终批量中断后全链路重跑,第二天早上报表依然空着

现象:某天凌晨2点日终批量里一个作业失败,当班运维把整个批次从头重跑,结果到早上8点还没跑完,核心日报一张没出。

原因:批量链路里几十个作业串行依赖,从头重跑把两个小时浪费在已经成功的作业上,真正失败的作业反而一直没跑到。

解决:靠作业状态表做“只重跑失败及其下游”的定点恢复,同时把每层作业改成幂等设计,重跑不会污染目标表。我的习惯是重跑前先看状态表里失败点在哪个作业、什么时间开始、日志最后一行写什么,而不是看到“失败”就一键重启,这个习惯能少熬很多夜。

5.2 故障二:增量抽取出现重复数据,明细表主键冲突

现象:某张客户关系表连续两天在日终抽取时报唯一键冲突,抽数任务失败,上游报表全部延迟。

原因:源系统的update_time精度只到秒,同一秒内发生多次变更时,增量查询会抽到重复记录;更常见的是前一次重跑没有清理已写入的数据,又插了一遍。

解决:目标表写入前先删除当日分区,再在抽取SQL里按业务主键和更新时间做去重。如果是源表更新非常频繁且没有可靠时间戳,我建议改成全量快照,不要硬啃增量,快照的存储成本远小于半夜被数据冲突叫起来。

5.3 故障三:源系统字段悄悄变更,汇总层数据全部错位

现象:某天早上发现客户表里“性别”字段的值变成了手机号,往下查,几张明细表同一列全是错位值。

原因:源系统在表中间插入了一个新字段,而ETL脚本用的是SELECT *,按位置取数导致后续字段整体右移。银行源系统变更流程重,但表结构变更不一定第一时间通知数仓,这是最常见的隐患来源。

解决:所有源表映射必须显式写字段名,禁止SELECT *,并且源系统侧建立接口变更通知机制,表结构变更前必须先通知数仓评估影响。我还会在ETL开发规范里强制加一条:新脚本上线前必须比对源表结构的字段顺序和类型,少了这一步,隐患就会留到夜里爆发。

5.4 故障四:月初对账永远差一分钱,查了两天查不出来

现象:每月1日对账,总资产总是差几分钱,有时多有时少,反复查账也查不出原因。

原因:时间口径不统一。有的作业用系统当前日期作为业务日期,有的用核心系统的日切日期,月末最后一天跨日交易被记到了不同日期。这种问题靠肉眼根本定位不到,因为两边数据都“对”,只是口径不同。

解决:ODS入口统一约定“数仓业务日期=核心系统日切日期”,调度平台统一传递BIZ_DATE参数,代码里禁止使用SYSDATE、NOW()这类函数取时间。我经历过一次这类问题之后,把“全代码不使用当前时间函数”写进了开发规范,从那以后类似的月初问题再没出现过。

6. 一个值得养成的验收习惯:先核对数据血缘再发版

发版前只看SQL跑得通不够,真正的风险在改动影响面。我现在的习惯是,任何涉及表结构、字段类型、口径逻辑的改动,上线前先跑一遍血缘核对,把下游依赖全部列出来,再决定要不要动。

-- 发版前血缘核对:列出所有依赖该字段的下游表、字段和加工脚本 SELECT target_table, target_field, etl_job, update_time FROM meta_data_lineage WHERE source_table = 'dwd_customer' AND source_field = 'cust_type' ORDER BY target_table, target_field;

查询结果会直接列出这个字段被下游哪些表、哪些脚本引用。如果工具成熟,直接导出影响面清单;如果工具不成熟,就按这张元数据表手工拉Excel。影响面里有重要报表或对外推送的,逐个确认兼容性再发版,不能只改完自测通过就完事。

这个习惯是我交过一次学费换来的。那次改一张客户维度表的字段精度,自测全过,结果下游三十多张报表里有两张间接引用的表错位,第二天对外口径少了几万条记录,定位花了整个上午。从那时起,我给自己定了四步发版清单:开发自测、字段映射复核、血缘影响面核对、上游变更确认,少一步都不发。希望这个习惯也能帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询