未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合已有Oracle SQL、PL/SQL及数据仓库基础的读者,用于在隔离测试环境中学习ETL分层、控制表、水位线、清洗转换及报送任务拆解,不是可独立跑通或直接部署的银行项目。水位线初始化、数据库连接与授权、业务表DDL、序列及多项函数尚未提供,前后流程的字段衔接也不完整;调度分派、交易分页与成功计数、异常日志SQL、WARNING流转、空批次统计、质量评分及报送编号映射和日期边界仍有实现问题,不能据示例成功日志认定数据已完整处理。时间戳抽取不保证捕获延迟提交或删除,已经分段提交的数据也不能由后续回滚整批撤销。EAST章节的字段、编码、阈值、周期及客户风险分类仅作教学假设,不可作为真实客户评估或监管合规依据,实际要求须以对应机构采用的正式规范为准。安全包缺少解密实现,加密代码存在密钥与安全设计问题,不得用于真实客户敏感数据;总结技能树的Mermaid定义也仍需修正。本次仅作全文结构和代码文本审阅,未进行外部事实核验、编译执行或图表渲染测试。
Oracle银行ETL开发 - 零基础入门教程
🎯 什么是ETL(用最简单的话说)
NOTE 💡 什么是ETL?
ETL就像是数据的"搬运工+翻译官+整理师":
- Extract(抽取)= 搬运工:从各个系统把数据"搬"出来
- Transform(转换)= 翻译官:把不同格式的数据"翻译"成统一格式
- Load(加载)= 整理师:把处理好的数据"整理"到数据仓库里
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[🏪 ATM系统]
B[💻 网银系统]
C[🏛️ 柜台系统]
D[📦 临时存储区]
E[🔄 数据清洗]
F[🎨 业务规则应用]
G[🏢 数据仓库]
A -->|抽取| D
B -->|抽取| D
C -->|抽取| D
D -->|转换| E
E -->|转换| F
F -->|加载| G
style A fill:#ff9999
style B fill:#ff9999
style C fill:#ff9999
style D fill:#ffcc99
style E fill:#99ccff
style F fill:#99ccff
style G fill:#99ff99
🏦 银行ETL的假设示例场景
想象一下银行每天的数据流:
早上6点:ATM机产生了1万笔取款记录
上午9点:网银系统产生了5万笔转账记录
下午3点:柜台系统产生了3万笔存款记录
晚上8点:信用卡系统产生了8万笔消费记录
这些数据来自不同的系统,格式不同,但银行需要把它们汇总起来做分析。这就是ETL的工作!
银行IT部门新入职的小陈接到第一个任务:"把各个系统的交易数据整合到数据仓库,明天行长要看昨天的经营报表。"小陈一头雾水:ATM系统用的是XML格式,网银是JSON,柜台系统是CSV,信用卡系统又是另一种格式。"这些不同的数据怎么统一处理?"这就是银行ETL开发要解决的核心问题。
在实际工作环境中,这样的挑战每天都在发生。理解相关知识不仅能解决当前问题,更能提升整体的专业能力。
📚 第一步:理解ETL的整体流程
🗺️ ETL的完整旅程(就像一条流水线)
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[🏦 原始数据]
B[📥 抽取Extract]
C[📦 临时存储]
D[🔄 转换Transform]
E[✨ 清洗后数据]
F[📤 加载Load]
G[🏢 数据仓库]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
subgraph "抽取阶段"
B1[增量抽取]
B2[全量抽取]
B3[实时抽取]
end
subgraph "转换阶段"
D1[数据清洗]
D2[格式转换]
D3[业务规则]
D4[质量检查]
end
subgraph "加载阶段"
F1[插入模式]
F2[更新模式]
F3[合并模式]
end
B --> B1
B --> B2
B --> B3
D --> D1
D --> D2
D --> D3
D --> D4
F --> F1
F --> F2
F --> F3
style A fill:#ff9999
style G fill:#99ff99
style C fill:#ffcc99
style E fill:#ccffcc
TIP 🌟 数据转换示例
让我们用银行转账数据来举例:
原始数据(来自核心银行系统):
- 转账ID: TXN20240101001
- 从账户: 6228123456789
- 到账户: 6228987654321
- 金额: 1000.00
- 时间: 2024-01-01 14:30:25
- 状态: S
经过ETL处理后(进入数据仓库):
- 交易序号: 1001
- 客户代码: C10001 (通过账户查找到客户)
- 交易类型: 转账
- 交易金额: 1000.00
- 交易日期: 2024-01-01
- 交易时间: 14:30:25
- 交易状态: 成功
- 风险等级: 低风险 (根据金额自动判断)
- 所属分行: 北京分行 (根据账户归属自动填充)
🎯 ETL的三个核心步骤详解
📥 Extract(抽取)- 数据搬运工
NOTE 🚚 什么是数据抽取?
这一步在做什么? 就像从不同的仓库里搬货物,ETL要从各个业务系统中把数据"搬"出来。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
subgraph "银行业务系统"
A[🏦 核心银行系统]
B[🏧 ATM系统]
C[💻 网银系统]
D[💳 信用卡系统]
E[🏪 柜台系统]
end
subgraph "数据抽取层"
F[📥 数据抽取器]
end
subgraph "临时存储区"
G[📦 Stage区]
end
A -->|账户、客户信息| F
B -->|取款、存款记录| F
C -->|在线交易记录| F
D -->|刷卡消费记录| F
E -->|柜台业务记录| F --> G
style A fill:#ff9999
style B fill:#ff9999
style C fill:#ff9999
style D fill:#ff9999
style E fill:#ff9999
style F fill:#99ccff
style G fill:#ffcc99
IMPORTANT ⚡ 抽取方式对比
全量抽取 vs 增量抽取
| 对比项目 | 全量抽取 | 增量抽取 |
|---|---|---|
| 速度 | 慢 ⏱️ | 快 🚀 |
| 资源占用 | 高 📊 | 低 📈 |
| 实现复杂度 | 简单 😊 | 复杂 🤔 |
| 数据完整性 | 取决于源数据、抽取范围和一致性控制 | 还需维护变更捕获与水位线策略 |
| 适用场景 | 初始化、小表 | 日常ETL、大表 |
增量抽取的实现方法:
-- 🎯 方法1:基于时间戳(最常用)
-- 假设我们要抽取昨天新增的客户信息
SELECT customer_id, customer_name, phone, email, create_time
FROM core_banking.customer_info -- 核心银行系统的客户表
WHERE create_time >= TRUNC(SYSDATE) - 1 -- 昨天00:00:00开始
AND create_time < TRUNC(SYSDATE); -- 今天00:00:00之前
-- 💡 解释:
-- TRUNC(SYSDATE) - 1 = 昨天的00:00:00
-- TRUNC(SYSDATE) = 今天的00:00:00
-- 这样就能抓到昨天一整天新增的客户
-- 🎯 方法2:基于版本号或序列号
-- 假设交易表有一个自增的交易序号
SELECT trans_id, account_id, amount, trans_time
FROM core_banking.transaction_log
WHERE trans_id > 1000000 -- 上次抽取的最大序号
AND trans_id <= 1050000; -- 本次要抽取到的序号
-- 💡 解释:
-- 每次记录抽取到哪个序号了,下次从这个序号后面开始抓
🔄 Transform(转换)- 数据翻译官
NOTE 🔄 什么是数据转换?
这一步在做什么? 就像翻译官,把来自不同系统的"方言"翻译成统一的"普通话"。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[📦 原始数据]
B{数据转换中心}
C[🧹 数据清洗]
D[📊 格式统一]
E[🎯 业务规则]
F[✅ 质量检查]
G[去重复]
H[去空值]
I[修正错误]
J[日期格式]
A --> B
B --> C
B --> D
B --> E
B --> F
C --> G
C --> H
C --> I
D --> E
D --> J
J --> K[数字格式]
J --> L[文本格式]
J --> K
D --> L
L --> M[计算字段]
L --> N[分类归档]
L --> M
E --> N
N --> O[业务映射]
N --> P[完整性]
N --> O
F --> P
P --> Q[准确性]
P --> R[一致性]
P --> S[清洗完成数据]
P --> Q
F --> R
G --> S
H --> S
I --> S
J --> S
K --> S
L --> S
M --> S
N --> S
O --> S
P --> S
Q --> S
R --> S
style A fill:#ff9999
style B fill:#99ccff
style S fill:#99ff99
IMPORTANT 🎨 银行数据转换的常见场景
场景1:数据格式统一
-- 📱 不同系统的手机号格式不同,需要统一
-- 原始数据可能是:
-- 系统A:13812345678
-- 系统B:138-1234-5678
-- 系统C:+86-138-1234-5678
-- 🛠️ 删除非数字字符(保留国家码,不保证结果为11位)
SELECT
customer_id,
customer_name,
-- 清理手机号:去掉所有非数字字符,只保留数字
REGEXP_REPLACE(phone, '[^0-9]', '') as clean_phone
FROM stage_customer_info;
-- 💡 解释:
-- REGEXP_REPLACE(phone, '[^0-9]', '') 的意思是:
-- phone: 要处理的字段
-- '[^0-9]': 正则表达式,表示"非数字字符"
-- '': 替换成空字符,即删除
-- 结果:+86-138-1234-5678 变成 8613812345678;该表达式仅删除非数字字符,不会删除国家码
场景2:业务规则转换
-- 🏆 根据客户资产计算客户等级
SELECT
customer_id,
customer_name,
total_balance,
-- 根据余额计算客户等级
CASE
WHEN total_balance >= 1000000 THEN '钻石客户'
WHEN total_balance >= 500000 THEN '白金客户'
WHEN total_balance >= 100000 THEN '金卡客户'
WHEN total_balance >= 10000 THEN '银卡客户'
ELSE '普通客户'
END as customer_level
FROM (
-- 先计算每个客户的总资产
SELECT
c.customer_id,
c.customer_name,
NVL(SUM(a.balance), 0) as total_balance
FROM customer_info c
LEFT JOIN account_info a ON c.customer_id = a.customer_id
GROUP BY c.customer_id, c.customer_name
);
-- 💡 解释:
-- 1. 内层查询:计算每个客户在所有账户中的总余额
-- 2. CASE语句:根据余额范围给客户分等级
-- 3. NVL(SUM(a.balance), 0):如果客户没有账户,总余额显示为0
场景3:数据质量检查和清洗
-- 🔍 清洗客户信息数据
SELECT
customer_id,
-- 客户姓名:去掉前后空格,转成大写
TRIM(UPPER(customer_name)) as customer_name,
-- 身份证号:只保留数字和字母X
UPPER(REGEXP_REPLACE(id_card, '[^0-9Xx]', '')) as clean_id_card,
-- 邮箱:转成小写
LOWER(TRIM(email)) as email,
-- 年龄:根据生日计算
FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) as age,
-- 性别:统一编码
CASE
WHEN UPPER(gender) IN ('男', 'M', 'MALE') THEN 'M'
WHEN UPPER(gender) IN ('女', 'F', 'FEMALE') THEN 'F'
ELSE 'U' -- 未知
END as gender_code
FROM stage_customer_info
WHERE
-- 数据质量检查:过滤掉明显错误的数据
customer_name IS NOT NULL -- 姓名不能为空
AND LENGTH(customer_name) >= 2 -- 姓名至少2个字符
AND birth_date IS NOT NULL -- 生日不能为空
AND birth_date <= SYSDATE -- 生日不能是未来日期
AND MONTHS_BETWEEN(SYSDATE, birth_date) >= 0; -- 年龄不能是负数
-- 💡 解释:
-- 1. TRIM():去掉字符串前后的空格
-- 2. UPPER()/LOWER():大小写转换
-- 3. REGEXP_REPLACE():正则表达式替换
-- 4. MONTHS_BETWEEN():计算两个日期之间的月数
-- 5. WHERE条件:数据质量检查,过滤异常数据
📤 Load(加载)- 数据整理师
NOTE 📤 什么是数据加载?
这一步在做什么? 就像整理师,把清洗干净的数据按照规则整齐地放到数据仓库的各个"柜子"里。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
subgraph "数据仓库架构"
A[🏢 数据仓库]
subgraph "事实表层 - 记录业务事件"
B[💳 交易事实表]
C[🏠 贷款事实表]
D[💰 理财事实表]
E[📊 其他事实表]
end
subgraph "维度表层 - 描述业务对象"
F[👤 客户维度表]
G[🏪 产品维度表]
H[📅 时间维度表]
I[🌍 地区维度表]
end
A --> B
A --> C
A --> D
A --> E
A --> F
A --> G
A --> H
A --> I
B -.->|关联| F
B -.->|关联| G
B -.->|关联| H
B -.->|关联| I
end
style A fill:#99ff99
style B fill:#ffcc99
style C fill:#ffcc99
style D fill:#ffcc99
style E fill:#ffcc99
style F fill:#ccffff
style G fill:#ccffff
style H fill:#ccffff
style I fill:#ccffff
TIP 🏗️ 数据仓库结构解释
- 事实表:存储可度量的业务事件(如交易金额、贷款数量)
- 维度表:存储描述性信息(如客户姓名、产品名称)
- 星型模式:事实表居中,维度表围绕,便于分析查询
加载策略的两种方式:
1. 插入模式(INSERT):每次都添加新记录
-- 🆕 插入今天的交易记录到事实表
INSERT INTO fact_transaction (
trans_id, -- 交易ID
customer_key, -- 客户代码(关联客户维度表)
account_key, -- 账户代码(关联账户维度表)
product_key, -- 产品代码(关联产品维度表)
trans_date, -- 交易日期
trans_amount, -- 交易金额
trans_type, -- 交易类型
create_time -- 记录创建时间
)
SELECT
t.trans_id,
c.customer_key, -- 通过客户ID关联到维度表获取key
a.account_key, -- 通过账户ID关联到维度表获取key
p.product_key, -- 通过产品ID关联到维度表获取key
TRUNC(t.trans_time), -- 交易日期(去掉时分秒)
t.amount,
t.trans_type,
SYSDATE
FROM ods_transaction t -- 已清洗的交易数据
JOIN dim_customer c ON t.customer_id = c.customer_id
JOIN dim_account a ON t.account_id = a.account_id
JOIN dim_product p ON a.product_id = p.product_id
WHERE TRUNC(t.trans_time) = TRUNC(SYSDATE) - 1 -- 只加载昨天的数据
AND NOT EXISTS (
-- 防止重复插入:检查这笔交易是否已经存在
SELECT 1 FROM fact_transaction
WHERE trans_id = t.trans_id
);
-- 💡 解释:
-- 1. JOIN操作:把业务ID转换成维度表的代理键
-- 2. NOT EXISTS:防止重复插入相同的交易
-- 3. TRUNC():截取日期,去掉时分秒部分
2. 合并模式(MERGE):存在就更新,不存在就插入
-- 🔄 更新客户维度表(处理客户信息变更)
MERGE INTO dim_customer target
USING (
SELECT
customer_id,
customer_name,
phone,
email,
address,
customer_level -- 新计算的客户等级
FROM ods_customer_info
WHERE batch_date = TRUNC(SYSDATE) -- 今天处理的数据
) source
ON (target.customer_id = source.customer_id) -- 匹配条件
WHEN MATCHED THEN -- 如果客户已存在,就更新信息
UPDATE SET
target.customer_name = source.customer_name,
target.phone = source.phone,
target.email = source.email,
target.address = source.address,
target.customer_level = source.customer_level,
target.update_time = SYSDATE,
target.version = target.version + 1 -- 版本号加1
WHEN NOT MATCHED THEN -- 如果是新客户,就插入新记录
INSERT (
customer_key, customer_id, customer_name, phone, email,
address, customer_level, create_time, update_time, version
) VALUES (
dim_customer_seq.NEXTVAL, -- 自动生成主键
source.customer_id,
source.customer_name,
source.phone,
source.email,
source.address,
source.customer_level,
SYSDATE, -- 创建时间
SYSDATE, -- 更新时间
1 -- 初始版本号
);
-- 💡 解释:
-- 1. MERGE:Oracle的"智能插入更新"语句
-- 2. ON条件:定义如何判断记录是否存在
-- 3. WHEN MATCHED:存在就执行UPDATE
-- 4. WHEN NOT MATCHED:不存在就执行INSERT
-- 5. 版本号:记录更新次数;此处覆盖旧值,不保留历史版本数据
🏗️ 第二步:搭建ETL控制框架
IMPORTANT 🎯 为什么需要ETL控制框架?
在银行这种对数据准确性要求极高的环境中,我们需要一个"管家"来统一管理所有的ETL作业。
📋 ETL控制表设计(就像作业管理系统)
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[🎓 学校课程表类比]
B[📋 ETL控制框架]
A --> B
subgraph "学校课程管理"
C[📚 每门课程有固定时间]
D[📖 有些课程有先修要求]
E[📝 需要记录出勤情况]
end
subgraph "ETL作业管理"
F[⏰ 每个作业有执行时间]
G[🔗 有些作业有依赖关系]
H[📊 需要记录执行情况]
end
A --> C
A --> D
A --> E
B --> F
B --> G
B --> H
style A fill:#ffffcc
style B fill:#ccffcc
NOTE 💡 控制框架核心思想
ETL控制表就像课程表一样,管理着所有ETL作业的执行:
- 📅 调度管理:什么时候执行哪个作业
- 🔗 依赖管理:哪些作业必须先完成
- 📊 监控管理:作业执行状态跟踪
- ❌ 异常管理:出错时如何处理
-- 🎯 ETL作业控制表(就像课程表)
CREATE TABLE etl_job_control (
job_id VARCHAR2(20), -- 作业编号(如:JOB001)
job_name VARCHAR2(100), -- 作业名称(如:客户信息抽取)
job_type VARCHAR2(30), -- 作业类型(抽取/转换/加载)
source_system VARCHAR2(50), -- 来源系统(如:核心银行系统)
target_table VARCHAR2(100), -- 目标表名
schedule_time VARCHAR2(20), -- 执行时间(如:每天凌晨2点)
is_active CHAR(1) DEFAULT 'Y', -- 是否启用(Y/N)
dependency_jobs VARCHAR2(500), -- 依赖的作业(必须先完成的作业)
max_runtime_minutes NUMBER(5), -- 最大运行时间(分钟)
notification_email VARCHAR2(200) -- 出错时通知邮箱
);
-- 💡 解释:
-- 这个表就像是ETL作业的"档案册",记录了每个作业的基本信息
-- dependency_jobs字段很重要:比如"客户等级计算"必须在"客户信息抽取"完成后才能执行
-- 🎯 ETL执行日志表(就像考勤记录)
CREATE TABLE etl_execution_log (
execution_id VARCHAR2(30), -- 执行ID(每次运行都不同)
job_id VARCHAR2(20), -- 作业ID(关联控制表)
batch_date DATE, -- 批次日期(处理哪天的数据)
start_time TIMESTAMP, -- 开始时间
end_time TIMESTAMP, -- 结束时间
status VARCHAR2(20), -- 状态(运行中/成功/失败)
records_processed NUMBER(15), -- 处理了多少条记录
records_inserted NUMBER(15), -- 插入了多少条记录
records_updated NUMBER(15), -- 更新了多少条记录
records_rejected NUMBER(15), -- 拒绝了多少条记录(数据质量问题)
error_message VARCHAR2(4000) -- 错误信息(如果失败的话)
);
-- 💡 解释:
-- 这个表记录每次ETL作业的执行情况,就像学生的考勤记录
-- 通过这个表可以知道:哪个作业什么时候执行的,成功了吗,处理了多少数据
🎮 ETL作业调度器(就像自动排课系统)
TIP 🤖 智能调度器的工作原理
现在我们来创建一个"智能管家",它能按照预定规则自动执行ETL作业:
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
sequenceDiagram
participant 调度器 as 🤖 ETL调度器
participant 控制表 as 📋 作业控制表
participant 日志表 as 📊 执行日志表
participant 作业1 as 🏦 客户数据抽取
participant 作业2 as 💳 交易数据抽取
participant 作业3 as 🧹 数据清洗
调度器->>控制表: 1. 查询待执行作业
控制表-->>调度器: 返回作业列表
调度器->>调度器: 2. 检查依赖关系
调度器->>日志表: 3. 记录开始执行
调度器->>作业1: 4. 执行客户数据抽取
作业1-->>调度器: 执行完成
调度器->>日志表: 记录作业1完成
调度器->>作业2: 5. 执行交易数据抽取
作业2-->>调度器: 执行完成
调度器->>日志表: 记录作业2完成
调度器->>作业3: 6. 执行数据清洗
作业3-->>调度器: 执行完成
调度器->>日志表: 记录作业3完成
调度器->>调度器: 7. 生成执行报告
-- 🤖 ETL调度器存储过程
CREATE OR REPLACE PROCEDURE run_etl_jobs(
p_batch_date IN DATE -- 要处理哪天的数据
)
AS
-- 📝 定义变量
v_job_name VARCHAR2(100);
v_execution_id VARCHAR2(30);
v_can_run BOOLEAN;
v_job_status VARCHAR2(20);
BEGIN
DBMS_OUTPUT.PUT_LINE('🚀 开始执行ETL作业调度');
DBMS_OUTPUT.PUT_LINE('📅 批次日期:' || TO_CHAR(p_batch_date, 'YYYY-MM-DD'));
-- 🔍 第1步:查找所有需要执行的作业
FOR job_rec IN (
SELECT job_id, job_name, dependency_jobs
FROM etl_job_control
WHERE is_active = 'Y' -- 只执行启用的作业
ORDER BY
-- 💡 聪明的排序:没有依赖的作业优先执行
CASE WHEN dependency_jobs IS NULL THEN 1 ELSE 2 END,
job_id
) LOOP
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('📋 准备执行作业:' || job_rec.job_name);
-- 🔍 第2步:检查依赖作业是否完成
v_can_run := check_dependencies(job_rec.job_id, p_batch_date);
IF v_can_run THEN
DBMS_OUTPUT.PUT_LINE('✅ 依赖检查通过,开始执行...');
-- 🚀 第3步:执行作业
BEGIN
-- 生成唯一的执行ID
v_execution_id := 'EXEC_' || job_rec.job_id || '_' ||
TO_CHAR(SYSDATE, 'YYYYMMDDHH24MISS');
-- 记录开始执行
INSERT INTO etl_execution_log (
execution_id, job_id, batch_date, start_time, status
) VALUES (
v_execution_id, job_rec.job_id, p_batch_date, SYSTIMESTAMP, 'RUNNING'
);
COMMIT;
-- 根据作业类型执行不同的处理逻辑
IF job_rec.job_name LIKE '%客户信息%' THEN
extract_customer_data(p_batch_date, v_execution_id);
ELSIF job_rec.job_name LIKE '%交易数据%' THEN
extract_transaction_data(p_batch_date, v_execution_id);
ELSIF job_rec.job_name LIKE '%数据清洗%' THEN
clean_data(p_batch_date, v_execution_id);
-- 可以根据需要添加更多作业类型
END IF;
DBMS_OUTPUT.PUT_LINE('🎉 作业执行成功:' || job_rec.job_name);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 作业执行失败:' || job_rec.job_name);
DBMS_OUTPUT.PUT_LINE('错误信息:' || SQLERRM);
-- 记录失败信息
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'FAILED',
error_message = SQLERRM
WHERE execution_id = v_execution_id;
COMMIT;
END;
ELSE
DBMS_OUTPUT.PUT_LINE('⏳ 依赖作业未完成,跳过执行:' || job_rec.job_name);
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('🏁 ETL作业调度完成');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ ETL调度器异常:' || SQLERRM);
RAISE;
END run_etl_jobs;
/
-- 💡 解释:
-- 1. 这个存储过程就像一个"智能管家",会自动安排ETL作业的执行顺序
-- 2. 它会检查每个作业的依赖关系,确保按正确顺序执行
-- 3. 如果某个作业失败了,会记录详细的错误信息
-- 4. 每个作业都有唯一的执行ID,方便跟踪和排查问题
-- 🔍 依赖检查函数(检查作业是否可以执行)
CREATE OR REPLACE FUNCTION check_dependencies(
p_job_id IN VARCHAR2, -- 要检查的作业ID
p_batch_date IN DATE -- 批次日期
) RETURN BOOLEAN
AS
v_dependency_jobs VARCHAR2(500); -- 依赖的作业列表
v_dep_job VARCHAR2(50); -- 单个依赖作业
v_dep_status VARCHAR2(20); -- 依赖作业的状态
v_pos NUMBER := 1; -- 字符串解析位置
v_next_pos NUMBER; -- 下一个逗号位置
BEGIN
-- 📝 第1步:获取依赖作业列表
SELECT dependency_jobs INTO v_dependency_jobs
FROM etl_job_control
WHERE job_id = p_job_id;
-- ✅ 如果没有依赖,直接返回可以执行
IF v_dependency_jobs IS NULL THEN
RETURN TRUE;
END IF;
-- 🔍 第2步:逐个检查依赖作业的状态
-- 依赖作业列表格式:JOB001,JOB002,JOB003
WHILE v_pos <= LENGTH(v_dependency_jobs) LOOP
-- 找到下一个逗号的位置
v_next_pos := INSTR(v_dependency_jobs, ',', v_pos);
IF v_next_pos = 0 THEN
-- 最后一个作业ID
v_dep_job := TRIM(SUBSTR(v_dependency_jobs, v_pos));
v_pos := LENGTH(v_dependency_jobs) + 1;
ELSE
-- 中间的作业ID
v_dep_job := TRIM(SUBSTR(v_dependency_jobs, v_pos, v_next_pos - v_pos));
v_pos := v_next_pos + 1;
END IF;
-- 检查这个依赖作业是否成功完成
BEGIN
SELECT status INTO v_dep_status
FROM etl_execution_log
WHERE job_id = v_dep_job
AND batch_date = p_batch_date
AND execution_id = (
-- 获取最新的执行记录
SELECT MAX(execution_id)
FROM etl_execution_log
WHERE job_id = v_dep_job AND batch_date = p_batch_date
);
-- 如果依赖作业没有成功完成,返回FALSE
IF v_dep_status != 'SUCCESS' THEN
DBMS_OUTPUT.PUT_LINE('⚠️ 依赖作业未完成:' || v_dep_job || '(状态:' || v_dep_status || ')');
RETURN FALSE;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- 依赖作业还没有执行记录
DBMS_OUTPUT.PUT_LINE('⚠️ 依赖作业尚未执行:' || v_dep_job);
RETURN FALSE;
END;
END LOOP;
-- 所有依赖作业都成功完成
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 依赖检查异常:' || SQLERRM);
RETURN FALSE;
END check_dependencies;
/
-- 💡 解释:
-- 1. 这个函数负责检查一个作业的所有依赖是否都已完成
-- 2. 它会解析依赖作业列表(逗号分隔的字符串)
-- 3. 逐个检查每个依赖作业的最新执行状态
-- 4. 只有所有依赖作业都成功完成,才返回TRUE
🔄 第三步:数据抽取实战
NOTE 💼 实战环节开始
现在让我们来看看如何从银行的各个业务系统中抽取数据。
📊 增量抽取实战案例
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[🏦 核心银行系统]
B[📊 数据变化监控]
C[🆕 新客户注册]
D[✏️ 客户信息更新]
E[❌ 客户状态变更]
F[📥 增量抽取器]
G[⏰ 时间戳比较]
H[🔍 水位线机制]
I[📦 临时存储区]
A --> B
B --> C
B --> D
B --> E
C --> F
D --> F
E --> F
F --> G
G --> H
G --> I
H --> J[🔄 数据仓库同步]
I --> J
style A fill:#ff9999
style F fill:#99ccff
style I fill:#ffcc99
style J fill:#99ff99
IMPORTANT 🎯 实战场景说明
场景:从核心银行系统抽取客户信息
银行的核心系统每天都会有新客户注册,老客户信息更新。我们需要把这些变化同步到数据仓库中。
核心挑战:
- 🕐 如何知道哪些数据是新的或更新的?
- 📊 如何确保不漏数据、不重复数据?
- ⚡ 如何提高抽取效率?
-- 🎯 客户信息增量抽取存储过程
CREATE OR REPLACE PROCEDURE extract_customer_data(
p_batch_date IN DATE, -- 批次日期
p_execution_id IN VARCHAR2 -- 执行ID
)
AS
v_last_extract_time TIMESTAMP; -- 上次抽取时间
v_current_time TIMESTAMP; -- 本次抽取时间
v_extracted_count NUMBER := 0; -- 抽取记录数
BEGIN
DBMS_OUTPUT.PUT_LINE('📥 开始抽取客户信息数据');
-- 🕰️ 第1步:确定抽取时间范围
v_current_time := SYSTIMESTAMP;
-- 从水位线表获取上次抽取时间
SELECT last_extract_time INTO v_last_extract_time
FROM etl_watermark -- 水位线表,记录各表的抽取进度
WHERE table_name = 'CUSTOMER_INFO'
AND source_system = 'CORE_BANKING';
DBMS_OUTPUT.PUT_LINE('⏰ 抽取时间范围:');
DBMS_OUTPUT.PUT_LINE(' 从:' || TO_CHAR(v_last_extract_time, 'YYYY-MM-DD HH24:MI:SS'));
DBMS_OUTPUT.PUT_LINE(' 到:' || TO_CHAR(v_current_time, 'YYYY-MM-DD HH24:MI:SS'));
-- 📋 第2步:抽取增量数据到临时表
INSERT INTO stage_customer_info (
customer_id, -- 客户ID
customer_name, -- 客户姓名
id_card, -- 身份证号
phone, -- 手机号
email, -- 邮箱
birth_date, -- 生日
gender, -- 性别
address, -- 地址
annual_income, -- 年收入
create_time, -- 创建时间
update_time, -- 更新时间
extract_time, -- 抽取时间(我们加的)
batch_date, -- 批次日期(我们加的)
processing_status -- 处理状态(我们加的)
)
SELECT
customer_id,
customer_name,
id_card,
phone,
email,
birth_date,
gender,
address,
annual_income,
create_time,
update_time,
v_current_time, -- 记录抽取时间
p_batch_date, -- 记录批次日期
'PENDING' -- 初始状态为待处理
FROM core_banking.customer_info@core_bank_link -- 通过数据库链接访问核心系统
WHERE update_time > v_last_extract_time -- 只抓取有更新的记录
AND update_time <= v_current_time; -- 确保时间范围明确
-- 📊 记录抽取数量
v_extracted_count := SQL%ROWCOUNT;
DBMS_OUTPUT.PUT_LINE('📈 本次抽取记录数:' || v_extracted_count);
-- 💾 第3步:更新水位线(记录这次抽取到哪里了)
UPDATE etl_watermark
SET last_extract_time = v_current_time,
last_extract_count = v_extracted_count,
update_time = SYSDATE
WHERE table_name = 'CUSTOMER_INFO'
AND source_system = 'CORE_BANKING';
-- ✅ 第4步:更新执行日志
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'SUCCESS',
records_processed = v_extracted_count,
records_inserted = v_extracted_count
WHERE execution_id = p_execution_id;
COMMIT;
DBMS_OUTPUT.PUT_LINE('✅ 客户信息抽取完成');
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- 可能是第一次抽取,没有水位线记录
DBMS_OUTPUT.PUT_LINE('⚠️ 未找到水位线记录,可能是首次抽取');
-- 可以选择全量抽取或者抛出异常
RAISE;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 客户信息抽取失败:' || SQLERRM);
-- 回滚事务
ROLLBACK;
-- 更新执行日志
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'FAILED',
error_message = SQLERRM
WHERE execution_id = p_execution_id;
COMMIT;
RAISE;
END extract_customer_data;
/
-- 💡 解释:
-- 1. 水位线机制:记录每次抽取的进度,下次从这个点继续
-- 2. 时间范围控制:限定update_time抽取区间;是否漏抓或重复还取决于源端更新时间、提交时序、时间一致性及重跑策略
-- 3. 异常处理:出错时回滚事务,记录错误信息
-- 4. 执行日志:详细记录每次执行的情况
-- 🎯 交易数据抽取(处理大数据量的技巧)
CREATE OR REPLACE PROCEDURE extract_transaction_data(
p_batch_date IN DATE,
p_execution_id IN VARCHAR2
)
AS
v_start_time TIMESTAMP;
v_end_time TIMESTAMP;
v_batch_size CONSTANT NUMBER := 50000; -- 每批处理5万条
v_total_count NUMBER := 0;
v_current_batch NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE('💳 开始抽取交易数据');
-- 📅 确定抽取日期范围(通常抽取昨天的数据)
v_start_time := TRUNC(p_batch_date); -- 昨天00:00:00
v_end_time := TRUNC(p_batch_date) + 1; -- 今天00:00:00
DBMS_OUTPUT.PUT_LINE('📊 抽取范围:' || TO_CHAR(v_start_time, 'YYYY-MM-DD HH24:MI:SS') ||
' 到 ' || TO_CHAR(v_end_time, 'YYYY-MM-DD HH24:MI:SS'));
-- 🔍 先统计总记录数
SELECT COUNT(*) INTO v_total_count
FROM core_banking.transaction_log@core_bank_link
WHERE trans_time >= v_start_time
AND trans_time < v_end_time;
DBMS_OUTPUT.PUT_LINE('📈 预计抽取记录数:' || TO_CHAR(v_total_count, '999,999,999'));
-- 💡 分批处理大数据量(避免内存溢出)
FOR batch_rec IN (
SELECT * FROM (
SELECT
trans_id, account_id, trans_type, amount, trans_time,
channel_code, branch_code, status, description,
ROW_NUMBER() OVER (ORDER BY trans_time) as rn
FROM core_banking.transaction_log@core_bank_link
WHERE trans_time >= v_start_time
AND trans_time < v_end_time
)
-- 分批查询:第1批1-50000,第2批50001-100000...
WHERE rn BETWEEN (v_current_batch * v_batch_size + 1)
AND ((v_current_batch + 1) * v_batch_size)
) LOOP
-- 插入到临时表
INSERT INTO stage_transaction (
trans_id, account_id, trans_type, amount, trans_time,
channel_code, branch_code, status, description,
extract_time, batch_date, processing_status
) VALUES (
batch_rec.trans_id, batch_rec.account_id, batch_rec.trans_type,
batch_rec.amount, batch_rec.trans_time, batch_rec.channel_code,
batch_rec.branch_code, batch_rec.status, batch_rec.description,
SYSTIMESTAMP, p_batch_date, 'PENDING'
);
-- 每处理1万条记录提交一次(避免事务太大)
IF MOD(SQL%ROWCOUNT, 10000) = 0 THEN
COMMIT;
DBMS_OUTPUT.PUT_LINE('📊 已处理:' || TO_CHAR(SQL%ROWCOUNT, '999,999,999') || ' 条记录');
END IF;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('✅ 交易数据抽取完成,共 ' || TO_CHAR(v_total_count, '999,999,999') || ' 条记录');
-- 更新执行日志
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'SUCCESS',
records_processed = v_total_count,
records_inserted = v_total_count
WHERE execution_id = p_execution_id;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 交易数据抽取失败:' || SQLERRM);
ROLLBACK;
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'FAILED',
error_message = SQLERRM
WHERE execution_id = p_execution_id;
COMMIT;
RAISE;
END extract_transaction_data;
/
-- 💡 解释:
-- 1. 分批处理:大数据量分成小批次处理,避免内存问题
-- 2. 定期提交:避免事务过大,提高处理效率
-- 3. 进度显示:让用户了解处理进度
-- 4. ROW_NUMBER():Oracle的行号函数,用于分页
🛠️ 第四步:数据转换实战
NOTE 🛠️ 转换阶段目标
数据抽取到临时区域后,需要进行清洗和转换,确保数据质量符合数据仓库的要求。
🧹 数据清洗实战
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[📦 原始数据]
B[🔍 数据质量检查]
A --> B --> C{数据质量评估}
C -->|✅ 合格| D[🧹 数据清洗]
C -->|⚠️ 警告| E[🔧 数据修复]
C -->|❌ 拒绝| F[🗑️ 拒绝处理]
C --> G[📊 格式标准化]
F --> G
E --> G --> H[✨ 清洗完成]
H --> I[📋 异常报告]
subgraph "清洗规则"
J[去除空格]
K[统一大小写]
L[格式验证]
M[重复数据处理]
end
D --> J
D --> K
D --> L
D --> M
style A fill:#ff9999
style H fill:#99ff99
style I fill:#ffcccc
IMPORTANT 🎯 数据清洗实战场景
场景:清洗客户信息数据,确保数据质量
常见数据质量问题:
- 📱 手机号格式不统一:138-1234-5678、13812345678
- 📧 邮箱大小写混乱:User@GMAIL.COM、user@gmail.com
- 👤 姓名包含多余空格:" 张 三 "
- 🆔 身份证号x大小写不一致:12345x、12345X
-- 🔍 数据质量检查函数
CREATE OR REPLACE FUNCTION validate_customer_info(
p_customer_id IN VARCHAR2, -- 客户ID
p_customer_name IN VARCHAR2, -- 客户姓名
p_id_card IN VARCHAR2, -- 身份证号
p_phone IN VARCHAR2, -- 手机号
p_email IN VARCHAR2, -- 邮箱
p_birth_date IN DATE -- 生日
) RETURN VARCHAR2 -- 返回验证结果
AS
v_age NUMBER;
BEGIN
-- ✅ 第1项检查:必填字段
IF p_customer_id IS NULL THEN
RETURN 'ERROR:客户ID不能为空';
END IF;
IF TRIM(p_customer_name) IS NULL THEN
RETURN 'ERROR:客户姓名不能为空';
END IF;
-- ✅ 第2项检查:身份证号格式
IF p_id_card IS NOT NULL THEN
-- 身份证号必须是18位,前17位是数字,最后一位是数字或X
IF NOT REGEXP_LIKE(p_id_card, '^\d{17}[\dXx]$') THEN
RETURN 'ERROR:身份证号格式错误,应为18位数字+数字或X';
END IF;
-- 简单的身份证校验:检查生日部分是否合理
DECLARE
v_id_birth_year NUMBER;
BEGIN
v_id_birth_year := TO_NUMBER(SUBSTR(p_id_card, 7, 4));
IF v_id_birth_year < 1900 OR v_id_birth_year > EXTRACT(YEAR FROM SYSDATE) THEN
RETURN 'ERROR:身份证号中的出生年份不合理';
END IF;
EXCEPTION
WHEN OTHERS THEN
RETURN 'ERROR:身份证号中的日期部分格式错误';
END;
END IF;
-- ✅ 第3项检查:手机号格式
IF p_phone IS NOT NULL THEN
-- 手机号必须是11位,以1开头,第二位是3-9
IF NOT REGEXP_LIKE(p_phone, '^1[3-9]\d{9}$') THEN
RETURN 'ERROR:手机号格式错误,应为11位数字,以1开头';
END IF;
END IF;
-- ✅ 第4项检查:邮箱格式
IF p_email IS NOT NULL THEN
-- 简单的邮箱格式检查
IF NOT REGEXP_LIKE(p_email, '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') THEN
RETURN 'ERROR:邮箱格式错误';
END IF;
END IF;
-- ✅ 第5项检查:年龄合理性
IF p_birth_date IS NOT NULL THEN
v_age := FLOOR(MONTHS_BETWEEN(SYSDATE, p_birth_date) / 12);
IF v_age < 0 THEN
RETURN 'ERROR:出生日期不能是未来日期';
ELSIF v_age > 150 THEN
RETURN 'ERROR:年龄超过150岁,请检查出生日期';
ELSIF v_age < 16 THEN
RETURN 'WARNING:客户年龄小于16岁,请确认';
END IF;
END IF;
-- 🎉 所有检查都通过
RETURN 'PASS';
EXCEPTION
WHEN OTHERS THEN
RETURN 'ERROR:数据验证过程出错-' || SQLERRM;
END validate_customer_info;
/
-- 💡 解释:
-- 1. REGEXP_LIKE():Oracle的正则表达式匹配函数
-- 2. ^\d{17}[\dXx]$:正则表达式,表示17位数字+1位数字或X
-- 3. MONTHS_BETWEEN():计算两个日期之间的月数
-- 4. 分级验证:ERROR表示严重错误,WARNING表示需要关注
-- 🧹 客户信息数据清洗存储过程
CREATE OR REPLACE PROCEDURE clean_customer_data(
p_batch_date IN DATE,
p_execution_id IN VARCHAR2
)
AS
v_total_count NUMBER := 0; -- 总记录数
v_valid_count NUMBER := 0; -- 有效记录数
v_invalid_count NUMBER := 0; -- 无效记录数
v_validation_result VARCHAR2(200);
-- 🎯 游标:逐条处理临时表中的数据
CURSOR c_stage_data IS
SELECT customer_id, customer_name, id_card, phone, email,
birth_date, gender, address, annual_income
FROM stage_customer_info
WHERE batch_date = p_batch_date
AND processing_status = 'PENDING'; -- 只处理待处理的数据
BEGIN
DBMS_OUTPUT.PUT_LINE('🧹 开始清洗客户信息数据');
-- 🔄 逐条处理数据
FOR customer_rec IN c_stage_data LOOP
v_total_count := v_total_count + 1;
-- 📋 第1步:数据质量检查
v_validation_result := validate_customer_info(
customer_rec.customer_id,
customer_rec.customer_name,
customer_rec.id_card,
customer_rec.phone,
customer_rec.email,
customer_rec.birth_date
);
-- 📊 第2步:根据验证结果处理数据
IF v_validation_result = 'PASS' OR v_validation_result LIKE 'WARNING:%' THEN
-- ✅ 数据质量检查通过,进行数据清洗
UPDATE stage_customer_info SET
-- 清洗客户姓名:去空格,统一大小写
customer_name = TRIM(INITCAP(customer_name)), -- 首字母大写
-- 清洗手机号:去掉所有非数字字符
phone = CASE
WHEN phone IS NOT NULL THEN REGEXP_REPLACE(phone, '[^0-9]', '')
ELSE phone
END,
-- 清洗邮箱:转小写,去空格
email = CASE
WHEN email IS NOT NULL THEN LOWER(TRIM(email))
ELSE email
END,
-- 清洗身份证号:转大写(X要大写)
id_card = CASE
WHEN id_card IS NOT NULL THEN UPPER(TRIM(id_card))
ELSE id_card
END,
-- 清洗地址:去掉多余空格
address = CASE
WHEN address IS NOT NULL THEN REGEXP_REPLACE(TRIM(address), '\s+', ' ')
ELSE address
END,
-- 更新处理状态
processing_status = CASE
WHEN v_validation_result = 'PASS' THEN 'VALIDATED'
ELSE 'WARNING'
END,
error_message = CASE
WHEN v_validation_result LIKE 'WARNING:%' THEN v_validation_result
ELSE NULL
END,
validation_time = SYSDATE
WHERE customer_id = customer_rec.customer_id
AND batch_date = p_batch_date;
v_valid_count := v_valid_count + 1;
ELSE
-- ❌ 数据质量检查未通过,标记为拒绝
UPDATE stage_customer_info SET
processing_status = 'REJECTED',
error_message = v_validation_result,
validation_time = SYSDATE
WHERE customer_id = customer_rec.customer_id
AND batch_date = p_batch_date;
v_invalid_count := v_invalid_count + 1;
DBMS_OUTPUT.PUT_LINE('❌ 数据质量问题 - 客户ID:' || customer_rec.customer_id ||
',原因:' || v_validation_result);
END IF;
-- 📊 每处理1000条记录显示一次进度
IF MOD(v_total_count, 1000) = 0 THEN
DBMS_OUTPUT.PUT_LINE('📈 已处理:' || v_total_count || ' 条记录');
COMMIT; -- 定期提交
END IF;
END LOOP;
COMMIT;
-- 📊 汇总统计
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('🎯 客户信息数据清洗完成');
DBMS_OUTPUT.PUT_LINE('📊 处理统计:');
DBMS_OUTPUT.PUT_LINE(' 总记录数:' || TO_CHAR(v_total_count, '999,999,999'));
DBMS_OUTPUT.PUT_LINE(' 有效记录:' || TO_CHAR(v_valid_count, '999,999,999') ||
' (' || ROUND(v_valid_count/v_total_count*100, 2) || '%)');
DBMS_OUTPUT.PUT_LINE(' 无效记录:' || TO_CHAR(v_invalid_count, '999,999,999') ||
' (' || ROUND(v_invalid_count/v_total_count*100, 2) || '%)');
-- ✅ 更新执行日志
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'SUCCESS',
records_processed = v_total_count,
records_inserted = v_valid_count,
records_rejected = v_invalid_count
WHERE execution_id = p_execution_id;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 数据清洗失败:' || SQLERRM);
ROLLBACK;
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'FAILED',
error_message = SQLERRM
WHERE execution_id = p_execution_id;
COMMIT;
RAISE;
END clean_customer_data;
/
-- 💡 解释:
-- 1. INITCAP():首字母大写函数
-- 2. REGEXP_REPLACE():正则表达式替换,'\s+'表示一个或多个空格
-- 3. 游标处理:逐条处理数据,可以精确控制每条记录的处理逻辑
-- 4. 统计报告:提供详细的处理统计,便于监控数据质量
🎨 业务规则转换实战
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false}}%%
flowchart TD
A[👤 客户基础信息]
A --> B[🎯 业务规则引擎]
subgraph "转换规则"
C[📊 客户等级计算]
D[🎯 风险偏好评估]
E[🌍 地区信息提取]
F[💰 收入等级划分]
G[👥 年龄段分组]
end
B --> C
B --> D
B --> E
B --> F
B --> G
C --> H[💎 钻石客户]
C --> I[⭐ 白金客户]
C --> J[🥇 金卡客户]
C --> K[🥈 银卡客户]
C --> L[🥉 普通客户]
D --> M[💪 激进型]
D --> N[🎯 积极型]
D --> O[⚖️ 平衡型]
D --> P[🛡️ 保守型]
H & I & J & K & L & M & N & O & P --> Q[✨ 业务化数据]
style A fill:#ff9999
style B fill:#99ccff
style Q fill:#99ff99
TIP 🎨 业务转换实战场景
场景:根据银行业务规则转换客户数据
转换目标:
- 🏆 自动计算客户等级(钻石、白金、金卡等)
- 🎯 评估风险承受能力(激进、保守等)
- 🌍 提取地理信息(省市区)
- 💰 划分收入等级
-- 🎯 客户数据业务转换存储过程
CREATE OR REPLACE PROCEDURE transform_customer_data(
p_batch_date IN DATE,
p_execution_id IN VARCHAR2
)
AS
v_processed_count NUMBER := 0;
-- 📊 客户等级计算函数
FUNCTION calculate_customer_level(
p_total_balance IN NUMBER, -- 总资产
p_annual_income IN NUMBER, -- 年收入
p_account_count IN NUMBER, -- 账户数量
p_age IN NUMBER -- 年龄
) RETURN VARCHAR2 AS
BEGIN
-- 🏆 钻石客户:资产500万以上 或 年收入100万以上
IF p_total_balance >= 5000000 OR p_annual_income >= 1000000 THEN
RETURN 'DIAMOND';
-- 💎 白金客户:资产100万以上 或 年收入50万以上 或 多账户高净值客户
ELSIF p_total_balance >= 1000000 OR p_annual_income >= 500000 OR
(p_account_count >= 5 AND p_total_balance >= 500000) THEN
RETURN 'PLATINUM';
-- 🥇 金卡客户:资产10万以上 或 年收入20万以上 或 年轻高收入
ELSIF p_total_balance >= 100000 OR p_annual_income >= 200000 OR
(p_age BETWEEN 25 AND 35 AND p_annual_income >= 100000) THEN
RETURN 'GOLD';
-- 🥈 银卡客户:有一定资产或收入
ELSIF p_total_balance >= 10000 OR p_annual_income >= 50000 THEN
RETURN 'SILVER';
-- 🥉 普通客户
ELSE
RETURN 'BRONZE';
END IF;
END calculate_customer_level;
-- 🎯 风险偏好评估函数
FUNCTION assess_risk_profile(
p_age IN NUMBER,
p_annual_income IN NUMBER,
p_customer_level IN VARCHAR2
) RETURN VARCHAR2 AS
BEGIN
-- 💪 激进型:年轻且高收入
IF p_age BETWEEN 25 AND 40 AND p_annual_income >= 300000 THEN
RETURN 'AGGRESSIVE';
-- 🎯 积极型:中年中高收入,或高等级客户
ELSIF (p_age BETWEEN 30 AND 50 AND p_annual_income >= 100000) OR
p_customer_level IN ('DIAMOND', 'PLATINUM') THEN
RETURN 'MODERATE_AGGRESSIVE';
-- ⚖️ 平衡型:中等收入中年客户
ELSIF p_age BETWEEN 35 AND 55 AND p_annual_income >= 50000 THEN
RETURN 'MODERATE';
-- 🛡️ 保守型:年长客户或低收入客户
ELSIF p_age >= 50 OR p_annual_income < 50000 THEN
RETURN 'CONSERVATIVE';
-- 🤔 稳健型:其他情况
ELSE
RETURN 'BALANCED';
END IF;
END assess_risk_profile;
BEGIN
DBMS_OUTPUT.PUT_LINE('🎨 开始客户数据业务转换');
-- 🔄 批量转换并插入到ODS层
INSERT INTO ods_customer_info (
customer_id,
customer_name,
id_card,
phone,
email,
birth_date,
age, -- 计算出的年龄
age_group, -- 年龄段
gender,
gender_desc, -- 性别描述
address,
city, -- 从地址中提取的城市
province, -- 从地址中提取的省份
annual_income,
income_level, -- 收入等级
total_balance, -- 总资产
account_count, -- 账户数量
customer_level, -- 客户等级
risk_profile, -- 风险偏好
is_vip, -- 是否VIP
create_time,
etl_batch_id,
etl_time
)
SELECT
sci.customer_id,
sci.customer_name,
sci.id_card,
sci.phone,
sci.email,
sci.birth_date,
-- 📊 计算年龄
FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) as age,
-- 📊 年龄段分组
CASE
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 18 THEN '未成年'
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 25 THEN '青年'
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 35 THEN '青年白领'
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 45 THEN '中年精英'
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 55 THEN '中年稳健'
WHEN FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12) < 65 THEN '准退休'
ELSE '退休'
END as age_group,
sci.gender,
-- 📊 性别描述转换
CASE
WHEN UPPER(sci.gender) IN ('M', '男', 'MALE') THEN '男'
WHEN UPPER(sci.gender) IN ('F', '女', 'FEMALE') THEN '女'
ELSE '未知'
END as gender_desc,
sci.address,
-- 🌍 从地址中提取城市信息
CASE
WHEN sci.address LIKE '%北京%' THEN '北京'
WHEN sci.address LIKE '%上海%' THEN '上海'
WHEN sci.address LIKE '%广州%' THEN '广州'
WHEN sci.address LIKE '%深圳%' THEN '深圳'
WHEN sci.address LIKE '%杭州%' THEN '杭州'
WHEN sci.address LIKE '%南京%' THEN '南京'
WHEN sci.address LIKE '%成都%' THEN '成都'
WHEN sci.address LIKE '%武汉%' THEN '武汉'
-- 可以根据需要添加更多城市
ELSE '其他'
END as city,
-- 🌍 从地址中提取省份信息
CASE
WHEN sci.address LIKE '%北京%' THEN '北京市'
WHEN sci.address LIKE '%上海%' THEN '上海市'
WHEN sci.address LIKE '%广东%' OR sci.address LIKE '%广州%' OR sci.address LIKE '%深圳%' THEN '广东省'
WHEN sci.address LIKE '%浙江%' OR sci.address LIKE '%杭州%' THEN '浙江省'
WHEN sci.address LIKE '%江苏%' OR sci.address LIKE '%南京%' THEN '江苏省'
WHEN sci.address LIKE '%四川%' OR sci.address LIKE '%成都%' THEN '四川省'
WHEN sci.address LIKE '%湖北%' OR sci.address LIKE '%武汉%' THEN '湖北省'
-- 可以根据需要添加更多省份
ELSE '其他'
END as province,
sci.annual_income,
-- 💰 收入等级分类
CASE
WHEN sci.annual_income >= 1000000 THEN '高收入'
WHEN sci.annual_income >= 500000 THEN '中高收入'
WHEN sci.annual_income >= 200000 THEN '中等收入'
WHEN sci.annual_income >= 100000 THEN '中低收入'
WHEN sci.annual_income >= 50000 THEN '低收入'
ELSE '收入不详'
END as income_level,
-- 💎 客户资产信息(通过关联账户表获取)
NVL(acc_info.total_balance, 0) as total_balance,
NVL(acc_info.account_count, 0) as account_count,
-- 🏆 计算客户等级
calculate_customer_level(
NVL(acc_info.total_balance, 0),
NVL(sci.annual_income, 0),
NVL(acc_info.account_count, 0),
FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12)
) as customer_level,
-- 🎯 评估风险偏好
assess_risk_profile(
FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12),
NVL(sci.annual_income, 0),
calculate_customer_level(
NVL(acc_info.total_balance, 0),
NVL(sci.annual_income, 0),
NVL(acc_info.account_count, 0),
FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12)
)
) as risk_profile,
-- ⭐ VIP标识
CASE
WHEN calculate_customer_level(
NVL(acc_info.total_balance, 0),
NVL(sci.annual_income, 0),
NVL(acc_info.account_count, 0),
FLOOR(MONTHS_BETWEEN(SYSDATE, sci.birth_date) / 12)
) IN ('DIAMOND', 'PLATINUM') THEN 'Y'
ELSE 'N'
END as is_vip,
sci.create_time,
p_execution_id,
SYSDATE
FROM stage_customer_info sci
LEFT JOIN (
-- 📊 计算客户的账户汇总信息
SELECT
customer_id,
SUM(balance) as total_balance,
COUNT(*) as account_count
FROM account_info
WHERE status = 'ACTIVE' -- 只统计活跃账户
GROUP BY customer_id
) acc_info ON sci.customer_id = acc_info.customer_id
WHERE sci.batch_date = p_batch_date
AND sci.processing_status = 'VALIDATED'; -- 只转换已验证的数据
v_processed_count := SQL%ROWCOUNT;
COMMIT;
DBMS_OUTPUT.PUT_LINE('✅ 客户数据业务转换完成');
DBMS_OUTPUT.PUT_LINE('📊 转换记录数:' || TO_CHAR(v_processed_count, '999,999,999'));
-- 📊 生成转换统计报告
FOR stat_rec IN (
SELECT
customer_level,
COUNT(*) as customer_count,
ROUND(COUNT(*) * 100.0 / v_processed_count, 2) as percentage
FROM ods_customer_info
WHERE etl_batch_id = p_execution_id
GROUP BY customer_level
ORDER BY COUNT(*) DESC
) LOOP
DBMS_OUTPUT.PUT_LINE(' ' || stat_rec.customer_level || ' 客户:' ||
stat_rec.customer_count || ' 人 (' || stat_rec.percentage || '%)');
END LOOP;
-- ✅ 更新执行日志
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'SUCCESS',
records_processed = v_processed_count,
records_inserted = v_processed_count
WHERE execution_id = p_execution_id;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 客户数据转换失败:' || SQLERRM);
ROLLBACK;
UPDATE etl_execution_log
SET end_time = SYSTIMESTAMP,
status = 'FAILED',
error_message = SQLERRM
WHERE execution_id = p_execution_id;
COMMIT;
RAISE;
END transform_customer_data;
/
-- 💡 解释:
-- 1. 嵌套函数:在存储过程内部定义业务逻辑函数
-- 2. 复杂的CASE语句:实现多层次的业务规则判断
-- 3. 子查询关联:通过LEFT JOIN获取客户的账户汇总信息
-- 4. 统计报告:生成转换后的数据分布统计
🏛️ 第五步:EAST5.0标准实践项目
IMPORTANT 🏛️ EAST5.0与ETL开发
本节以EAST5.0监管数据报送场景组织ETL设计示例;下述字段、编码、阈值和报送流程不构成正式规范,实际要求及适用范围应以对应机构采用的监管文件为准。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
subgraph "EAST5.0数据治理框架"
A[📊 数据标准化]
B[🔍 数据质量管控]
C[📋 监管报送]
D[🔒 数据安全合规]
end
subgraph "ETL实现层"
E[📥 数据抽取]
F[🔄 数据转换]
G[📤 数据加载]
H[✅ 质量检查]
end
A --> E
B --> F
B --> H
C --> G
D --> E
D --> F
D --> G
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#99ccff
style D fill:#ccffcc
📊 EAST5.0数据标准化实践
NOTE 📊 EAST5.0数据标准要求
核心要求:
- 🏷️ 统一数据编码标准
- 📏 标准化数据格式
- 🔗 建立数据血缘关系
- 📋 完整的元数据管理
实践项目:构建EAST5.0标准化数据转换
-- 🏛️ EAST5.0标准化客户信息转换
CREATE OR REPLACE PROCEDURE transform_east50_customer_data(
p_batch_date IN DATE,
p_execution_id IN VARCHAR2
)
AS
v_processed_count NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE('🏛️ 开始EAST5.0标准化客户数据转换');
-- 🔄 按照EAST5.0标准转换客户数据
INSERT INTO east50_customer_standard (
east_customer_id, -- EAST5.0标准客户编号
customer_type_code, -- 客户类型编码(个人P/企业C)
id_type_code, -- 证件类型编码
id_number, -- 证件号码
customer_name_cn, -- 中文名称
customer_name_en, -- 英文名称
gender_code, -- 性别编码(M/F/U)
birth_date, -- 出生日期
nationality_code, -- 国籍编码(ISO标准)
residence_country, -- 居住国家编码
occupation_code, -- 职业编码(EAST5.0标准)
industry_code, -- 行业编码(CSRC标准)
risk_rating, -- 风险评级(1-10级)
kyc_level, -- KYC等级(1-3级)
aml_risk_score, -- 反洗钱风险评分
data_source, -- 数据来源系统
create_timestamp, -- 创建时间戳
update_timestamp, -- 更新时间戳
data_quality_score, -- 数据质量评分
east_version -- EAST版本号
)
SELECT
-- 🔢 生成EAST5.0标准客户编号(bank_code + customer_seq)
'0001' || LPAD(customer_seq.NEXTVAL, 12, '0') as east_customer_id,
-- 👤 客户类型标准化
CASE
WHEN customer_type = '个人' THEN 'P'
WHEN customer_type = '企业' THEN 'C'
ELSE 'U' -- 未知
END as customer_type_code,
-- 🆔 证件类型标准化(按EAST5.0编码)
CASE
WHEN id_type = '身份证' THEN '01'
WHEN id_type = '护照' THEN '02'
WHEN id_type = '军官证' THEN '03'
WHEN id_type = '营业执照' THEN '11'
WHEN id_type = '组织机构代码证' THEN '12'
ELSE '99' -- 其他
END as id_type_code,
-- 📄 证件号码标准化处理
TRIM(REGEXP_REPLACE(UPPER(id_card), '[^A-Z0-9]', '')) as id_number,
-- 📝 姓名标准化
TRIM(customer_name) as customer_name_cn,
NVL(TRIM(customer_name_en), TRIM(customer_name)) as customer_name_en,
-- ⚥ 性别编码标准化
CASE
WHEN UPPER(gender) IN ('M', '男', 'MALE', '1') THEN 'M'
WHEN UPPER(gender) IN ('F', '女', 'FEMALE', '2') THEN 'F'
ELSE 'U'
END as gender_code,
birth_date,
-- 🌍 国籍编码(ISO 3166-1 alpha-2)
CASE
WHEN nationality = '中国' THEN 'CN'
WHEN nationality = '美国' THEN 'US'
WHEN nationality = '英国' THEN 'GB'
WHEN nationality = '日本' THEN 'JP'
ELSE 'CN' -- 默认中国
END as nationality_code,
-- 🏠 居住国家(默认与国籍相同)
CASE
WHEN nationality = '中国' THEN 'CN'
WHEN nationality = '美国' THEN 'US'
WHEN nationality = '英国' THEN 'GB'
WHEN nationality = '日本' THEN 'JP'
ELSE 'CN'
END as residence_country,
-- 💼 职业编码(按国家标准GB/T 6565-2015)
get_occupation_code(occupation) as occupation_code,
-- 🏭 行业编码(证监会行业分类)
get_industry_code(industry) as industry_code,
-- 📊 风险评级计算
calculate_risk_rating(
total_assets,
annual_income,
transaction_frequency,
geographic_risk
) as risk_rating,
-- 🔍 KYC等级评定
calculate_kyc_level(
id_verification_status,
address_verification_status,
income_verification_status
) as kyc_level,
-- 🚨 反洗钱风险评分
calculate_aml_risk_score(
transaction_pattern,
geographic_risk,
occupation_risk,
customer_behavior
) as aml_risk_score,
'CORE_BANKING' as data_source,
SYSTIMESTAMP as create_timestamp,
SYSTIMESTAMP as update_timestamp,
-- 📈 数据质量评分
calculate_data_quality_score(
customer_name,
id_card,
phone,
address,
birth_date
) as data_quality_score,
'5.0' as east_version
FROM ods_customer_info
WHERE batch_date = p_batch_date
AND processing_status = 'VALIDATED';
v_processed_count := SQL%ROWCOUNT;
COMMIT;
DBMS_OUTPUT.PUT_LINE('✅ EAST5.0标准化转换完成');
DBMS_OUTPUT.PUT_LINE('📊 转换记录数:' || TO_CHAR(v_processed_count, '999,999,999'));
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ EAST5.0标准化转换失败:' || SQLERRM);
ROLLBACK;
RAISE;
END transform_east50_customer_data;
/
🔍 EAST5.0数据质量管控实践
WARNING ⚠️ EAST5.0数据质量要求
教学示例的假设目标,不代表EAST5.0正式要求:
- 📊 假设数据完整性目标不低于95%
- 🎯 假设数据准确性目标不低于99%
- ⏱️ 假设数据及时性目标为T+1
- 🔗 假设数据一致性目标为100%
-- 🔍 EAST5.0数据质量检查框架
CREATE OR REPLACE PROCEDURE east50_data_quality_check(
p_table_name IN VARCHAR2,
p_batch_date IN DATE,
p_execution_id IN VARCHAR2
)
AS
v_total_records NUMBER := 0;
v_quality_issues NUMBER := 0;
v_completeness_score NUMBER;
v_accuracy_score NUMBER;
v_consistency_score NUMBER;
v_overall_score NUMBER;
BEGIN
DBMS_OUTPUT.PUT_LINE('🔍 开始EAST5.0数据质量检查:' || p_table_name);
-- 📊 1. 完整性检查
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM ' || p_table_name ||
' WHERE batch_date = :1'
INTO v_total_records
USING p_batch_date;
-- 检查必填字段完整性
FOR field_rec IN (
SELECT column_name, nullable
FROM user_tab_columns
WHERE table_name = UPPER(p_table_name)
AND column_name IN (
'EAST_CUSTOMER_ID', 'CUSTOMER_TYPE_CODE',
'ID_TYPE_CODE', 'ID_NUMBER', 'CUSTOMER_NAME_CN'
)
) LOOP
DECLARE
v_null_count NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM ' || p_table_name ||
' WHERE ' || field_rec.column_name || ' IS NULL' ||
' AND batch_date = :1'
INTO v_null_count
USING p_batch_date;
IF v_null_count > 0 THEN
v_quality_issues := v_quality_issues + v_null_count;
-- 记录质量问题
INSERT INTO east50_quality_issues (
issue_id, table_name, column_name, issue_type,
issue_count, batch_date, create_time
) VALUES (
quality_issue_seq.NEXTVAL, p_table_name,
field_rec.column_name, 'COMPLETENESS',
v_null_count, p_batch_date, SYSDATE
);
DBMS_OUTPUT.PUT_LINE('⚠️ 完整性问题:' || field_rec.column_name ||
' 缺失 ' || v_null_count || ' 条记录');
END IF;
END;
END LOOP;
-- 📏 2. 准确性检查
-- 检查证件号码格式
DECLARE
v_invalid_id_count NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM ' || p_table_name ||
' WHERE NOT REGEXP_LIKE(id_number, ''^[A-Z0-9]{15,20}$'')' ||
' AND batch_date = :1'
INTO v_invalid_id_count
USING p_batch_date;
IF v_invalid_id_count > 0 THEN
v_quality_issues := v_quality_issues + v_invalid_id_count;
INSERT INTO east50_quality_issues (
issue_id, table_name, column_name, issue_type,
issue_count, batch_date, create_time
) VALUES (
quality_issue_seq.NEXTVAL, p_table_name,
'ID_NUMBER', 'ACCURACY',
v_invalid_id_count, p_batch_date, SYSDATE
);
DBMS_OUTPUT.PUT_LINE('⚠️ 准确性问题:证件号码格式错误 ' ||
v_invalid_id_count || ' 条记录');
END IF;
END;
-- 🔗 3. 一致性检查
-- 检查客户编号唯一性
DECLARE
v_duplicate_count NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) - COUNT(DISTINCT east_customer_id) FROM ' ||
p_table_name || ' WHERE batch_date = :1'
INTO v_duplicate_count
USING p_batch_date;
IF v_duplicate_count > 0 THEN
v_quality_issues := v_quality_issues + v_duplicate_count;
INSERT INTO east50_quality_issues (
issue_id, table_name, column_name, issue_type,
issue_count, batch_date, create_time
) VALUES (
quality_issue_seq.NEXTVAL, p_table_name,
'EAST_CUSTOMER_ID', 'CONSISTENCY',
v_duplicate_count, p_batch_date, SYSDATE
);
DBMS_OUTPUT.PUT_LINE('⚠️ 一致性问题:客户编号重复 ' ||
v_duplicate_count || ' 条记录');
END IF;
END;
-- 📈 计算质量评分
v_completeness_score := ROUND((v_total_records - v_quality_issues) * 100.0 / v_total_records, 2);
v_accuracy_score := ROUND((v_total_records - v_quality_issues) * 100.0 / v_total_records, 2);
v_consistency_score := ROUND((v_total_records - v_quality_issues) * 100.0 / v_total_records, 2);
v_overall_score := ROUND((v_completeness_score + v_accuracy_score + v_consistency_score) / 3, 2);
-- 💾 记录质量评分
INSERT INTO east50_quality_summary (
summary_id, table_name, batch_date,
total_records, quality_issues,
completeness_score, accuracy_score, consistency_score, overall_score,
meets_east50_std, create_time
) VALUES (
quality_summary_seq.NEXTVAL, p_table_name, p_batch_date,
v_total_records, v_quality_issues,
v_completeness_score, v_accuracy_score, v_consistency_score, v_overall_score,
CASE WHEN v_overall_score >= 95 THEN 'Y' ELSE 'N' END,
SYSDATE
);
COMMIT;
-- 📊 输出质量报告
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('📊 EAST5.0数据质量报告');
DBMS_OUTPUT.PUT_LINE('====================');
DBMS_OUTPUT.PUT_LINE('表名:' || p_table_name);
DBMS_OUTPUT.PUT_LINE('总记录数:' || TO_CHAR(v_total_records, '999,999,999'));
DBMS_OUTPUT.PUT_LINE('质量问题:' || TO_CHAR(v_quality_issues, '999,999'));
DBMS_OUTPUT.PUT_LINE('完整性评分:' || v_completeness_score || '%');
DBMS_OUTPUT.PUT_LINE('准确性评分:' || v_accuracy_score || '%');
DBMS_OUTPUT.PUT_LINE('一致性评分:' || v_consistency_score || '%');
DBMS_OUTPUT.PUT_LINE('综合评分:' || v_overall_score || '%');
IF v_overall_score >= 95 THEN
DBMS_OUTPUT.PUT_LINE('✅ 达到本示例综合评分阈值,不代表符合EAST5.0正式要求');
ELSE
DBMS_OUTPUT.PUT_LINE('❌ 未达到本示例综合评分阈值');
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ EAST5.0质量检查失败:' || SQLERRM);
ROLLBACK;
RAISE;
END east50_data_quality_check;
/
📋 EAST5.0监管报送实践
NOTE 📋 EAST5.0报送要求
报送标准:
- 📅 定期报送(月度/季度/年度)
- 📊 标准化报表格式
- 🔒 数据加密传输
- ✅ 报送确认机制
-- 📋 EAST5.0监管报送数据生成
CREATE OR REPLACE PROCEDURE generate_east50_report(
p_report_type IN VARCHAR2, -- 报告类型:MONTHLY/QUARTERLY/YEARLY
p_report_period IN VARCHAR2, -- 报告期间:YYYYMM/YYYYQQ/YYYY
p_execution_id IN VARCHAR2
)
AS
v_report_id VARCHAR2(100);
v_report_count NUMBER := 0;
v_report_date DATE;
BEGIN
DBMS_OUTPUT.PUT_LINE('📋 开始生成EAST5.0监管报送数据');
DBMS_OUTPUT.PUT_LINE('报告类型:' || p_report_type);
DBMS_OUTPUT.PUT_LINE('报告期间:' || p_report_period);
-- 🆔 生成报告ID
v_report_id := 'RPT_' || p_report_type || '_' || p_report_period || '_' ||
TO_CHAR(SYSDATE, 'YYYYMMDDHH24MISS');
-- 📅 确定报告日期
CASE p_report_type
WHEN 'MONTHLY' THEN
v_report_date := LAST_DAY(TO_DATE(p_report_period, 'YYYYMM'));
WHEN 'QUARTERLY' THEN
v_report_date := LAST_DAY(ADD_MONTHS(TO_DATE(SUBSTR(p_report_period, 1, 4) || '01', 'YYYYMM'),
TO_NUMBER(SUBSTR(p_report_period, 6, 1)) * 3 - 1));
WHEN 'YEARLY' THEN
v_report_date := TO_DATE(p_report_period || '1231', 'YYYYMMDD');
END CASE;
-- 📊 1. 客户信息报送表
INSERT INTO east50_customer_report (
report_id,
report_period,
bank_code,
customer_id,
customer_type,
id_type,
id_number,
customer_name,
gender,
birth_date,
nationality,
occupation,
industry,
risk_rating,
account_count,
total_balance,
create_timestamp
)
SELECT
v_report_id,
p_report_period,
'0001' as bank_code,
east_customer_id,
customer_type_code,
id_type_code,
id_number,
customer_name_cn,
gender_code,
birth_date,
nationality_code,
occupation_code,
industry_code,
risk_rating,
(SELECT COUNT(*) FROM account_info a WHERE a.customer_id = c.east_customer_id) as account_count,
(SELECT NVL(SUM(balance), 0) FROM account_info a WHERE a.customer_id = c.east_customer_id) as total_balance,
SYSTIMESTAMP
FROM east50_customer_standard c
WHERE c.create_timestamp <= v_report_date
AND c.data_quality_score >= 95; -- 只报送高质量数据
v_report_count := v_report_count + SQL%ROWCOUNT;
-- 📊 2. 交易信息报送表
INSERT INTO east50_transaction_report (
report_id,
report_period,
bank_code,
transaction_id,
customer_id,
account_id,
transaction_type,
transaction_amount,
transaction_date,
counterparty_info,
channel_code,
risk_flag,
create_timestamp
)
SELECT
v_report_id,
p_report_period,
'0001' as bank_code,
trans_id,
customer_key,
account_key,
trans_type,
trans_amount,
trans_date,
counterparty_info,
channel_code,
CASE WHEN trans_amount >= 50000 THEN 'HIGH_RISK' ELSE 'NORMAL' END as risk_flag,
SYSTIMESTAMP
FROM fact_transaction
WHERE trans_date BETWEEN
CASE p_report_type
WHEN 'MONTHLY' THEN TRUNC(TO_DATE(p_report_period, 'YYYYMM'), 'MM')
WHEN 'QUARTERLY' THEN TRUNC(ADD_MONTHS(TO_DATE(SUBSTR(p_report_period, 1, 4) || '01', 'YYYYMM'),
(TO_NUMBER(SUBSTR(p_report_period, 6, 1)) - 1) * 3), 'MM')
WHEN 'YEARLY' THEN TRUNC(TO_DATE(p_report_period, 'YYYY'), 'YYYY')
END
AND v_report_date;
v_report_count := v_report_count + SQL%ROWCOUNT;
-- 📊 3. 风险信息报送表
INSERT INTO east50_risk_report (
report_id,
report_period,
bank_code,
customer_id,
risk_type,
risk_level,
risk_score,
risk_description,
mitigation_measures,
create_timestamp
)
SELECT
v_report_id,
p_report_period,
'0001' as bank_code,
customer_id,
risk_type,
risk_level,
risk_score,
risk_description,
mitigation_measures,
SYSTIMESTAMP
FROM customer_risk_assessment
WHERE assessment_date <= v_report_date
AND risk_level IN ('HIGH', 'MEDIUM'); -- 只报送中高风险客户
v_report_count := v_report_count + SQL%ROWCOUNT;
-- 📝 4. 生成报送汇总记录
INSERT INTO east50_report_summary (
report_id,
report_type,
report_period,
bank_code,
total_records,
report_status,
generate_time,
execution_id
) VALUES (
v_report_id,
p_report_type,
p_report_period,
'0001',
v_report_count,
'GENERATED',
SYSDATE,
p_execution_id
);
COMMIT;
DBMS_OUTPUT.PUT_LINE('✅ EAST5.0监管报送数据生成完成');
DBMS_OUTPUT.PUT_LINE('📊 报告ID:' || v_report_id);
DBMS_OUTPUT.PUT_LINE('📊 报送记录数:' || TO_CHAR(v_report_count, '999,999,999'));
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ EAST5.0报送数据生成失败:' || SQLERRM);
ROLLBACK;
RAISE;
END generate_east50_report;
/
🔒 EAST5.0数据安全合规实践
WARNING 🔒 EAST5.0安全合规要求
安全标准:
- 🔐 敏感数据加密存储
- 👥 细粒度权限控制
- 📋 完整审计日志
- 🛡️ 数据脱敏处理
-- 🔒 EAST5.0数据安全合规框架
CREATE OR REPLACE PACKAGE east50_security_pkg AS
-- 🔐 敏感数据加密
FUNCTION encrypt_sensitive_data(
p_data IN VARCHAR2,
p_data_type IN VARCHAR2
) RETURN VARCHAR2;
-- 🔓 敏感数据解密
FUNCTION decrypt_sensitive_data(
p_encrypted_data IN VARCHAR2,
p_data_type IN VARCHAR2
) RETURN VARCHAR2;
-- 🎭 数据脱敏处理
FUNCTION mask_sensitive_data(
p_data IN VARCHAR2,
p_mask_type IN VARCHAR2
) RETURN VARCHAR2;
-- 📋 记录数据访问日志
PROCEDURE log_data_access(
p_user_id IN VARCHAR2,
p_table_name IN VARCHAR2,
p_operation IN VARCHAR2,
p_record_count IN NUMBER
);
END east50_security_pkg;
/
CREATE OR REPLACE PACKAGE BODY east50_security_pkg AS
-- 🔐 敏感数据加密实现
FUNCTION encrypt_sensitive_data(
p_data IN VARCHAR2,
p_data_type IN VARCHAR2
) RETURN VARCHAR2 IS
v_encrypted_data VARCHAR2(4000);
v_encryption_key VARCHAR2(100) := 'EAST50_ENCRYPTION_KEY_2024';
BEGIN
-- 根据数据类型选择加密策略
CASE p_data_type
WHEN 'ID_NUMBER' THEN
-- 身份证号强加密
v_encrypted_data := DBMS_CRYPTO.ENCRYPT(
UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
DBMS_CRYPTO.ENCRYPT_AES256 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5,
UTL_I18N.STRING_TO_RAW(v_encryption_key, 'AL32UTF8')
);
WHEN 'PHONE' THEN
-- 手机号加密
v_encrypted_data := DBMS_CRYPTO.ENCRYPT(
UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5,
UTL_I18N.STRING_TO_RAW(v_encryption_key, 'AL32UTF8')
);
ELSE
-- 默认加密
v_encrypted_data := DBMS_CRYPTO.ENCRYPT(
UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5,
UTL_I18N.STRING_TO_RAW(v_encryption_key, 'AL32UTF8')
);
END CASE;
RETURN RAWTOHEX(v_encrypted_data);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 数据加密失败:' || SQLERRM);
RETURN NULL;
END encrypt_sensitive_data;
-- 🎭 数据脱敏处理实现
FUNCTION mask_sensitive_data(
p_data IN VARCHAR2,
p_mask_type IN VARCHAR2
) RETURN VARCHAR2 IS
v_masked_data VARCHAR2(4000);
BEGIN
CASE p_mask_type
WHEN 'ID_NUMBER' THEN
-- 身份证号脱敏:保留前3位和后2位
IF LENGTH(p_data) >= 6 THEN
v_masked_data := SUBSTR(p_data, 1, 3) ||
RPAD('*', LENGTH(p_data) - 5, '*') ||
SUBSTR(p_data, -2);
ELSE
v_masked_data := RPAD('*', LENGTH(p_data), '*');
END IF;
WHEN 'PHONE' THEN
-- 手机号脱敏:保留前3位和后2位
IF LENGTH(p_data) >= 6 THEN
v_masked_data := SUBSTR(p_data, 1, 3) ||
RPAD('*', LENGTH(p_data) - 5, '*') ||
SUBSTR(p_data, -2);
ELSE
v_masked_data := RPAD('*', LENGTH(p_data), '*');
END IF;
WHEN 'NAME' THEN
-- 姓名脱敏:保留姓氏
IF LENGTH(p_data) > 1 THEN
v_masked_data := SUBSTR(p_data, 1, 1) || RPAD('*', LENGTH(p_data) - 1, '*');
ELSE
v_masked_data := '*';
END IF;
ELSE
-- 默认脱敏:全部替换为*
v_masked_data := RPAD('*', LENGTH(p_data), '*');
END CASE;
RETURN v_masked_data;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('❌ 数据脱敏失败:' || SQLERRM);
RETURN RPAD('*', LENGTH(p_data), '*');
END mask_sensitive_data;
-- 📋 数据访问日志记录
PROCEDURE log_data_access(
p_user_id IN VARCHAR2,
p_table_name IN VARCHAR2,
p_operation IN VARCHAR2,
p_record_count IN NUMBER
) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO east50_access_log (
log_id,
user_id,
table_name,
operation_type,
record_count,
access_time,
session_id,
ip_address,
client_info
) VALUES (
access_log_seq.NEXTVAL,
p_user_id,
p_table_name,
p_operation,
p_record_count,
SYSTIMESTAMP,
SYS_CONTEXT('USERENV', 'SESSIONID'),
SYS_CONTEXT('USERENV', 'IP_ADDRESS'),
SYS_CONTEXT('USERENV', 'CLIENT_INFO')
);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('❌ 访问日志记录失败:' || SQLERRM);
END log_data_access;
END east50_security_pkg;
/
📊 EAST5.0项目实施计划
TIP 📅 EAST5.0实施路线图
分阶段实施计划:
第一阶段(1-2个月):基础数据标准化
- 建立EAST5.0数据标准
- 完成核心客户数据转换
- 实施基础质量检查
第二阶段(3-4个月):全面质量管控
- 完善数据质量检查框架
- 建立质量监控体系
- 实现自动化质量报告
第三阶段(5-6个月):监管报送对接
- 开发报送数据生成功能
- 完成报送接口对接
- 实现定期自动报送
第四阶段(持续优化):安全合规强化
- 完善数据安全体系
- 优化访问控制机制
- 持续合规性监控
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
gantt
title EAST5.0 ETL项目实施甘特图
dateFormat YYYY-MM-DD
section 基础建设
数据标准制定 :done, std, 2024-01-01, 2024-01-15
ETL框架搭建 :done, etl, 2024-01-16, 2024-02-15
section 数据转换
客户数据标准化 :active, cust, 2024-02-01, 2024-02-29
交易数据标准化 :trans, 2024-02-15, 2024-03-15
风险数据标准化 :risk, 2024-03-01, 2024-03-31
section 质量管控
质量检查开发 :quality, 2024-03-15, 2024-04-15
监控体系建设 :monitor, 2024-04-01, 2024-04-30
section 报送对接
报送功能开发 :report, 2024-05-01, 2024-05-31
接口联调测试 :test, 2024-05-15, 2024-06-15
section 上线运行
生产环境部署 :deploy, 2024-06-01, 2024-06-30
持续优化维护 :maintain, 2024-07-01, 2024-12-31
🎓 总结:银行ETL开发完整指南
SUCCESS 🎉 本文展示了银行ETL开发的核心思路
完成依赖补充、代码修正和测试练习后,可进一步学习与验证以下能力:
- 📚 ETL的基本概念和银行应用场景
- 🏗️ 搭建ETL控制框架
- 🔄 数据抽取的实战技巧
- 🛠️ 数据转换和清洗方法
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
mindmap
root((银行ETL开发技能树))
基础概念
ETL三步骤
银行业务场景
数据仓库架构
技术实现
控制框架设计
调度器开发
异常处理机制
抽取技术
增量抽取
水位线机制
大数据量处理
转换技术
数据清洗
格式统一
业务规则应用
质量检查
最佳实践
性能优化
监控体系
错误恢复
文档规范
class root accent
TIP 💡 学习建议
对于初学者:
- 🎯 先理解ETL的基本概念,不要急于编写复杂代码
- 🏗️ 重点掌握控制框架的设计思想
- 💻 多练习SQL语句,特别是数据转换的逻辑
- 📊 关注数据质量检查,这在银行业务中极其重要
下一步学习方向:
- 🚀 学习更高级的ETL工具(如Kettle、DataStage)
- 📈 掌握实时ETL和流处理技术
- 🔒 深入了解银行数据安全和合规要求
- 🎯 学习数据仓库建模和OLAP分析
WARNING ⚠️ 银行ETL开发注意事项
数据安全:
- 🔐 敏感数据必须加密存储和传输
- 👥 严格控制数据访问权限
- 📋 完整的操作审计日志
数据准确性:
- ✅ 每个环节都要有数据校验
- 🔄 建立数据对账机制
- 📊 定期进行数据质量检查
性能考量:
- ⏰ 合理安排ETL作业时间窗口
- 📈 监控系统资源使用情况
- 🔧 定期优化SQL语句和索引
NOTE 📚 进一步学习资源
推荐阅读:
- 《数据仓库工具箱》- 维度建模经典教材
- 《Oracle数据库管理》- 深入理解Oracle性能优化
- 银行业数据治理相关规范文档
实践建议:
- 🏗️ 搭建个人的ETL测试环境
- 💼 参与实际的银行数据项目
- 🤝 加入数据工程师技术社区
- 📈 关注最新的大数据技术发展
🎯 最后的话:ETL开发是数据工程的核心技能,在银行这样的金融机构中,ETL不仅仅是技术工作,更是保障业务连续性和数据准确性的重要环节。希望这个教程能帮助你在数据工程师的道路上走得更远!