本文来源于数据从业者全栈知识库,更多体系化内容请访问知识库。
数据开发工程师 L3:架构演进
写在前面如果你正在读这篇文档,说明你已经在数据开发领域摸爬滚打了几年。你对数仓建模、Hive/Spark 已经相当熟练,日常工作得心应手。但你开始感到某种瓶颈:业务方要实时数据,现有的 T+1 架构满足不了;数据量越来越大,以前的优化手段不够用了;新技术层出不穷,Flink、数据湖、流批一体…你不确定该往哪个方向发力。
L3 阶段是一个分水岭。从这里开始,你不再只是”写代码的”,而是要开始思考”为什么这么做”、“有没有更好的架构”。这篇文档会帮助你理清这个阶段的学习重点,以及如何从”熟练工”进化为”架构师”。
这个阶段的你,可能是这样的
画像一:业务要实时数据,但你只会离线
老板说:“竞对的数据大屏是实时的,我们也要。“产品说:“用户下单后,5秒内就要在 APP 里看到状态更新。“你慌了——你的技能树全点在离线数仓上,Flink 只听过没用过,Kafka 只知道是个消息队列。
给你的建议:实时计算是 L3 阶段最重要的技能跃迁。好消息是,实时和离线的思维方式有很多相通之处。你在 Spark SQL 上的经验,可以快速迁移到 Flink SQL。建议从 Flink SQL 入手,先跑通一个简单的实时 ETL,再慢慢深入 DataStream API 和状态管理。
画像二:Spark 任务越来越慢,调参调不动了
你负责的 Spark 任务,数据量翻了一倍,运行时间从 2 小时变成了 8 小时。你试了各种参数调优——增加 executor 数量、调整内存配比、调整 shuffle 分区数——但效果有限。你意识到,可能不是参数的问题,而是架构的问题。
给你的建议:到了 L3 阶段,“调参”已经不是主要手段了。你需要深入理解 Spark 的执行原理——Stage 是怎么划分的?Shuffle 数据是怎么落盘的?内存是怎么管理的?搞清楚这些,你才能从根本上解决问题,而不是在参数上碰运气。
画像三:想往架构师方向发展,但不知道从哪开始
你听说高级别的岗位叫”数据架构师”,薪资很高,也很有技术含量。但你不知道架构师具体做什么,也不确定自己是否具备那些能力。你想往这个方向发展,但没有明确的路径。
给你的建议:架构师不是突然”升级”的,而是在日常工作中逐渐培养出来的。你可以从以下几个方面开始:
- 每次接需求时,多想想”有没有更好的架构方案”
- 主动参与系统设计评审,学习别人的设计思路
- 尝试写技术方案文档,把你的设计思考落到纸面上
- 关注业界的架构演进,了解为什么别人要这么设计
画像四:对数据治理没什么概念,感觉是”虚的”
你听过数据质量、元数据管理、数据血缘这些词,但觉得这些是”管理层的事”,和写代码没什么关系。你的关注点一直在技术实现上,对治理体系不太上心。
给你的建议:数据治理绝对不是”虚的”。当你半夜被叫起来排查”数据怎么又错了”,当你花了三天才搞清楚一个字段的口径,当你的任务因为上游变更突然挂掉——这些都是缺乏治理的后果。L3 阶段,你需要开始建立治理思维:写代码的同时,思考如何让这套系统更可控、更可追溯、更少出问题。
L3 阶段的核心目标
用一句话概括:
能够设计和落地复杂的数据架构,解决性能、时效、质量方面的核心挑战。
具体来说:
- 掌握实时计算技术,能构建秒级延迟的数据链路
- 深入理解计算引擎原理,能进行深度性能优化
- 能进行架构选型和设计,权衡各种方案的利弊
- 具备数据治理意识,能建立质量保障体系
L2 阶段你学会了”构建系统”,L3 阶段你要学会”设计架构”。构建是执行,架构是决策。
必须掌握的核心技能
1. 实时计算 —— 从 T+1 到 T+0
这是 L3 阶段最重要的能力跃迁。离线计算和实时计算是两种完全不同的思维方式。
离线 vs 实时的本质区别:
| 维度 | 离线计算 | 实时计算 |
|---|---|---|
| 数据特点 | 有界数据集 | 无界数据流 |
| 计算模式 | 批处理(一次处理所有) | 流处理(逐条/微批处理) |
| 时效性 | T+1 或更长 | 秒级/分钟级 |
| 容错方式 | 任务失败重跑 | Checkpoint + 状态恢复 |
| 核心挑战 | 数据量、计算效率 | 延迟、乱序、状态管理 |
为什么实时计算这么难?
离线计算处理的是”已经发生完”的数据,可以反复计算、校验。实时计算处理的是”正在发生”的数据,你不知道后面还有什么,而且必须快速响应。
几个核心挑战:
- 乱序问题:用户 10:00 的行为,可能 10:05 才到达系统。你该按发生时间算还是到达时间算?
- 状态管理:要算用户的累计消费额,必须存储历史状态。状态存在哪?多大?崩溃了怎么恢复?
- Exactly-Once:消息来了处理一半系统挂了,重启后怎么保证不丢不重?
Flink 核心概念:
- 时间语义:
// Event Time:事件发生时间(最常用,但需要处理乱序)// Processing Time:处理时间(最简单,但结果不可复现)// Ingestion Time:进入 Flink 的时间(折中方案)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);- Watermark(水位线):
Watermark 是处理乱序数据的核心机制。它告诉系统:“我认为时间戳小于这个值的数据都已经到齐了。”
// 假设数据最多乱序 5 秒WatermarkStrategy .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, timestamp) -> event.getTimestamp());- 窗口(Window):
// 滚动窗口:每 5 分钟一个窗口,窗口不重叠stream.keyBy(e -> e.userId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .sum("amount");
// 滑动窗口:窗口大小 10 分钟,每 5 分钟滑动一次stream.keyBy(e -> e.userId) .window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(5))) .sum("amount");
// 会话窗口:不活跃超过 30 分钟,窗口关闭stream.keyBy(e -> e.userId) .window(EventTimeSessionWindows.withGap(Time.minutes(30))) .sum("amount");- 状态(State):
// Keyed State:每个 Key 独立的状态public class CountFunction extends KeyedProcessFunction<String, Event, Result> { // 值状态:存储一个值 private ValueState<Long> countState;
// 列表状态:存储一个列表 private ListState<Event> historyState;
// Map状态:存储一个Map private MapState<String, Long> detailState;
@Override public void open(Configuration parameters) { countState = getRuntimeContext().getState( new ValueStateDescriptor<>("count", Long.class)); }
@Override public void processElement(Event event, Context ctx, Collector<Result> out) { Long count = countState.value(); if (count == null) count = 0L; count++; countState.update(count); // ... }}- Checkpoint:
Flink 通过定期做快照(Checkpoint)来保证容错。任务崩溃后可以从最近的 Checkpoint 恢复。
// 启用 Checkpoint,每 60 秒一次env.enableCheckpointing(60000);
// Exactly-Once 语义(更安全,但更慢)env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// At-Least-Once 语义(更快,但可能重复)env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE);Flink SQL —— 快速入门实时计算:
如果你已经熟悉 SQL,Flink SQL 是最快的入门方式。
-- 创建 Kafka 源表CREATE TABLE order_source ( order_id STRING, user_id STRING, amount DECIMAL(10,2), order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND) WITH ( 'connector' = 'kafka', 'topic' = 'orders', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json');
-- 实时聚合:每分钟的订单统计SELECT TUMBLE_START(order_time, INTERVAL '1' MINUTE) as window_start, COUNT(*) as order_cnt, SUM(amount) as total_amountFROM order_sourceGROUP BY TUMBLE(order_time, INTERVAL '1' MINUTE);推荐学习:实时数据架构
实时计算的坑实时任务一旦上线,就是 7x24 小时运行的。和离线任务不同,你没法说”今晚重跑一下就好了”。所以:
- 一定要做好监控和报警
- 状态不能无限增长,要设置 TTL
- 要考虑好 Schema 变更怎么处理
- 要有回溯方案(从某个时间点重新消费 Kafka)
2. 数据湖与湖仓一体 —— 架构的下一站
传统数据仓库有一些固有的问题:
- 不支持 ACID 事务,数据更新只能全量覆盖
- 只能存储结构化数据,非结构化数据没法处理
- Schema 强绑定,修改表结构很痛苦
数据湖技术(Hudi、Iceberg、Delta Lake)就是为了解决这些问题。
核心能力对比:
| 特性 | 传统 Hive | 数据湖(Hudi/Iceberg) |
|---|---|---|
| ACID 事务 | 不支持 | 支持 |
| 增量更新 | INSERT OVERWRITE | UPSERT/DELETE |
| Schema 演进 | 困难 | 支持 |
| 时间旅行 | 不支持 | 支持(查历史快照) |
| 存储格式 | Parquet/ORC | Parquet + 元数据 |
Hudi 核心概念:
Copy-on-Write (COW):- 写入时复制整个文件- 读取性能好(直接读 Parquet)- 写入性能差(要重写文件)- 适合读多写少的场景
Merge-on-Read (MOR):- 写入时只追加 Delta 文件- 写入性能好- 读取时需要合并(读性能略差)- 适合写多读少的场景实际应用场景:
-- Hudi 表创建示例CREATE TABLE hudi_order ( order_id STRING, user_id STRING, amount DECIMAL(10,2), status STRING, update_time TIMESTAMP) USING hudiOPTIONS ( 'primaryKey' = 'order_id', 'type' = 'cow', 'preCombineField' = 'update_time');
-- 支持 UPSERT(有则更新,无则插入)MERGE INTO hudi_order targetUSING source_data sourceON target.order_id = source.order_idWHEN MATCHED THEN UPDATE SET *WHEN NOT MATCHED THEN INSERT *;
-- 时间旅行:查询昨天的数据快照SELECT * FROM hudi_order TIMESTAMP AS OF '2024-06-14';湖仓一体架构:
传统架构:数据源 → 数据湖(原始存储) → 数据仓库(分析) ↑ 两套系统,数据要搬来搬去
湖仓一体:数据源 → 数据湖 + 仓库能力(一套系统搞定) ↑ 存储和计算分离,同一份数据支持批/流/交互式分析推荐学习:数据仓库与数据湖建模 → 云原生数据架构
3. 深度性能优化 —— 从调参到调架构
L2 阶段的优化主要是”调参”,L3 阶段要深入到原理层面。
Spark 执行原理深度解析:
一个 Spark SQL 的执行过程:
SQL 语句 ↓ 解析逻辑计划(Logical Plan) ↓ 优化器(Catalyst)优化后的逻辑计划 ↓ 物理计划生成物理计划(Physical Plan) ↓ 代码生成(Codegen)RDD 执行图 ↓ DAGSchedulerStage 划分(以 Shuffle 为边界) ↓ TaskSchedulerTask 分发到 Executor 执行几个关键优化点:
- 减少 Shuffle:
Shuffle 是分布式计算中最昂贵的操作。数据要写磁盘、通过网络传输、再读出来合并。
-- 不好的写法:两次 ShuffleSELECT a.user_id, b.order_cnt, c.pay_amountFROM users aJOIN ( SELECT user_id, COUNT(*) as order_cnt FROM orders GROUP BY user_id) b ON a.user_id = b.user_idJOIN ( SELECT user_id, SUM(amount) as pay_amount FROM payments GROUP BY user_id) c ON a.user_id = c.user_id;
-- 优化后:合并子查询,减少 ShuffleSELECT a.user_id, COUNT(DISTINCT o.order_id) as order_cnt, SUM(p.amount) as pay_amountFROM users aLEFT JOIN orders o ON a.user_id = o.user_idLEFT JOIN payments p ON a.user_id = p.user_idGROUP BY a.user_id;- 利用分区裁剪:
-- 不好的写法:全表扫描SELECT * FROM orders WHERE order_date >= '2024-06-01';
-- 好的写法:如果 dt 是分区字段,只扫描需要的分区SELECT * FROM orders WHERE dt >= '2024-06-01';- 避免数据膨胀:
-- 危险的写法:笛卡尔积SELECT a.*, b.*FROM table_a aJOIN table_b bON a.key = b.key AND a.key IS NULL;-- 如果 a.key 有很多 NULL,会产生笛卡尔积
-- 更危险的写法:CROSS JOINSELECT * FROM table_a CROSS JOIN table_b;-- 1万行 x 1万行 = 1亿行- AQE(Adaptive Query Execution):
Spark 3.0 引入的自适应查询执行,可以在运行时动态调整执行计划。
-- 启用 AQESET spark.sql.adaptive.enabled = true;
-- 自动合并小分区(避免大量小文件)SET spark.sql.adaptive.coalescePartitions.enabled = true;
-- 自动处理数据倾斜SET spark.sql.adaptive.skewJoin.enabled = true;JVM 层面的优化:
# Executor 内存配置--executor-memory 8g--conf spark.executor.memoryOverhead=2g
# 内存管理--conf spark.memory.fraction=0.6 # 执行+存储内存占比--conf spark.memory.storageFraction=0.5 # 存储内存占比
# GC 优化--conf spark.executor.extraJavaOptions="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"推荐学习:性能优化
性能优化的正确姿势不要盲目优化。正确的流程是:
- 定位瓶颈:看 Spark UI,找出最慢的 Stage
- 分析原因:是数据倾斜?是 Shuffle 太多?是内存不够?
- 针对性优化:根据原因选择合适的优化手段
- 验证效果:对比优化前后的执行时间和资源消耗
4. 数据治理 —— 从混乱到有序
L3 阶段,你要开始建立治理思维。这不是管理层的事,而是架构设计的一部分。
数据质量管理:
数据质量问题的代价是巨大的。我见过因为一个字段口径错误,导致财务报表偏差几百万;见过因为数据延迟,导致运营活动失败。
质量检查的几个维度:
| 维度 | 含义 | 检查方法 |
|---|---|---|
| 完整性 | 数据是否缺失 | NULL 值比例、行数波动 |
| 准确性 | 数据是否正确 | 业务规则校验、交叉验证 |
| 一致性 | 不同数据源是否一致 | 核对关键指标 |
| 时效性 | 数据是否及时 | 监控任务延迟 |
| 唯一性 | 是否有重复数据 | 主键去重检查 |
-- 数据质量检查示例
-- 完整性检查:关键字段 NULL 比例SELECT COUNT(*) as total, SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) as null_cnt, SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) / COUNT(*) as null_ratioFROM dwd_order_detailWHERE dt = '${bizdate}';
-- 一致性检查:订单金额和支付金额是否匹配SELECT SUM(order_amount) as order_sum, SUM(pay_amount) as pay_sum, ABS(SUM(order_amount) - SUM(pay_amount)) / SUM(order_amount) as diff_ratioFROM ads_daily_summaryWHERE dt = '${bizdate}';
-- 唯一性检查:主键是否重复SELECT order_id, COUNT(*) as cntFROM dwd_order_detailWHERE dt = '${bizdate}'GROUP BY order_idHAVING cnt > 1;元数据管理与数据血缘:
当你有几千张表时,“这个字段是从哪里来的”就成了一个大问题。
数据血缘的价值:1. 影响分析:修改一张表前,知道会影响哪些下游2. 问题追溯:数据错了,能快速定位是哪个环节出问题3. 口径统一:知道每个指标是怎么算出来的成本治理:
大数据计算资源很贵。L3 工程师要有成本意识。
成本优化的几个方向:1. 资源利用率:任务申请 100G 内存,实际只用 20G2. 存储优化:历史数据压缩、冷热分层3. 计算优化:避免重复计算,合理设置任务周期4. 淘汰无用数据:很多表几个月没人用了,占着资源推荐学习:数据质量管理体系与实践 → 数据开发文档管理
5. 云原生与容器化 —— 需要学吗?
你可能听说”现在都上 K8s 了”、“不会云原生找不到工作”。这里帮你理清。
什么情况下需要学 Kubernetes?
| 你的情况 | K8s 是否必要 | 建议 |
|---|---|---|
| 公司数据平台部署在 K8s 上 | 需要 | 至少能看懂 YAML、会用 kubectl |
| 公司还是传统 YARN 集群 | 暂不必要 | 先把当前技术栈学精 |
| 想做数据平台架构师 | 必须学 | 云原生是未来趋势 |
| 只做 ETL 开发 | 不必要 | 平台运维有专人负责 |
L3 阶段需要了解的程度:
基本概念(必须知道):- Pod:K8s 最小调度单位- Deployment:管理 Pod 副本- Service:服务发现和负载均衡- ConfigMap/Secret:配置管理
实操技能(按需学习):- 能看懂 Spark/Flink on K8s 的 YAML 配置- 能用 kubectl 查看日志、排查问题- 理解 Spark on K8s 和 Spark on YARN 的区别云原生 vs 传统方案对比:
| 组件 | 传统方案 | 云原生方案 |
|---|---|---|
| 计算引擎 | Spark on YARN | Spark on K8s |
| 实时引擎 | Flink on YARN | Flink Kubernetes Operator |
| 消息队列 | 自建 Kafka 集群 | Kafka on K8s / 云托管 |
| 存储 | HDFS | S3 / OSS / MinIO |
务实建议不要为了学 K8s 而学 K8s。如果你当前工作用不到,先把实时计算、架构设计这些核心技能学好。当公司开始做云原生转型时,再深入也不迟。
6. AI 时代对 L3 工程师的影响
L3 阶段,你需要思考 AI 对数据工程的影响——不是焦虑”会不会被取代”,而是思考”如何利用”。
AI 能帮 L3 工程师做什么?
| 场景 | AI 能做 | 你必须做 |
|---|---|---|
| 架构设计 | 列出方案选项、分析优缺点 | 结合公司情况做最终决策 |
| 技术选型 | 比较 Flink vs Spark 特点 | 考虑团队能力、运维成本 |
| 性能调优 | 分析执行计划、建议方向 | 验证效果、处理边界情况 |
| 代码编写 | 生成 Flink/Spark 代码框架 | Review 逻辑、处理异常 |
AI 替代不了什么?
- 架构决策:需要结合公司实际情况权衡
- 深度调优:复杂问题需要深入理解原理
- 业务理解:数据模型设计需要理解业务
- 故障处理:线上问题需要快速判断和决策
关于 MLOps / 特征工程:
L3 阶段你可能开始接触 ML 相关需求(特征计算、数据集准备)。了解基本概念有帮助,但不是必须——除非你的工作方向明确是 ML 平台开发。
核心观点AI 时代,L3 工程师的价值在于:架构决策能力 + 深度问题解决能力 + 业务理解能力。这些恰恰是 AI 做不好的。把 AI 当高效工具用,同时深耕这些核心能力。
架构选型的思考框架
L3 阶段,你经常要做架构选型。这里提供一个思考框架:
Lambda 架构 vs Kappa 架构
Lambda 架构: 数据源 ↓ ┌────┴────┐批处理层 实时处理层 └────┬────┘ ↓ 服务层
优点:批处理保证准确性,实时满足时效性缺点:两套代码,维护成本高
Kappa 架构:数据源 → 消息队列 → 实时处理 → 服务层 ↑ 重放(回溯)
优点:一套代码,架构简单缺点:对实时引擎要求高,历史重算成本高如何选择?
- 如果团队实时能力强,数据量不是特别大,Kappa 更简单
- 如果需要复杂的批处理逻辑,或者需要经常回算历史,Lambda 更稳妥
- 很多公司采用”伪 Lambda”:实时链路用 Flink,每天跑批任务修正数据
选型决策清单
每次做技术选型时,问自己这些问题:
- 业务需求:时效性要求多高?数据量有多大?准确性要求多高?
- 团队能力:团队熟悉什么技术栈?能否支撑新技术的运维?
- 运维成本:这个技术生态是否成熟?出了问题能否快速定位?
- 可扩展性:未来数据量增长 10 倍,这个架构还能撑住吗?
- 成本:计算资源、存储资源、人力成本各是多少?
技术选型的陷阱不要为了用新技术而用新技术。我见过很多团队,业务场景明明用 Hive 就够了,非要上 Flink;数据量明明不大,非要搞分布式。结果运维成本大增,效率反而下降。选型要基于问题,而不是基于技术流行度。
你可能会遇到的困难
”Flink 学了很多,但工作中用不上”
你的公司可能还是以离线为主,没有实时业务场景。
解决方案:
- 主动找实时场景——实时监控大屏、实时推荐、实时风控,很多业务其实有需求,只是没人做
- 如果公司确实没有,可以考虑换一个有实时业务的平台历练
- 至少保持学习,技术储备在,机会来了才能抓住
”感觉自己只会 CRUD,没有架构能力”
架构能力不是天生的,是在实践中培养出来的。
培养方法:
- 每次设计前,先画架构图,和团队讨论
- 多看别人的系统是怎么设计的(开源项目、技术博客、架构书籍)
- 主动参与系统重构,这是最好的架构训练
- 复盘出过的问题,思考”如果重新设计,怎么避免这个问题"
"数据治理不知道从哪开始”
数据治理是一个体系工程,不要指望一步到位。
建议的起步方式:
- 从数据质量开始——先把关键表的质量检查做起来
- 建立基本的监控告警——任务失败、数据异常要能及时发现
- 梳理核心链路的血缘——至少知道核心报表是从哪些表算出来的
- 逐步完善,不要追求完美
”不确定要不要深入源码”
源码阅读是一个争议话题。有人觉得必须读,有人觉得没必要。
我的建议:
- 不需要通读全部源码,那是不可能的任务
- 但关键模块要理解——比如 Spark 的 Shuffle 实现、Flink 的 Checkpoint 机制
- 遇到诡异问题时,源码是最终的答案
- 如果想往架构师方向发展,源码阅读能力是必备的
L3 阶段可以胜任的岗位
完成 L3 阶段的学习后,你可以胜任:
高级数据开发工程师
- 主要工作:核心数据系统开发、性能优化、架构设计
- 薪资参考:一线城市 35-55K,二线城市 25-40K
- 面试重点:实时计算、性能调优、架构设计能力
实时计算工程师
- 主要工作:实时数据链路建设、Flink/Kafka 集群运维
- 特点:专注实时领域,技术深度要求高
数据架构师(初级)
- 主要工作:数据平台架构设计、技术选型、标准制定
- 特点:从执行转向规划,需要更广的技术视野
L3 的瓶颈L3 是一个比较难突破的阶段。很多人会在这个阶段停留很长时间。突破的关键是:
- 不要只做自己熟悉的事,要主动接触新领域
- 培养系统性思维,从全局看问题
- 提升表达和沟通能力,好的架构需要”卖出去”
给 L3 学习者的真诚建议
1. 深度和广度要平衡
L3 阶段容易走两个极端:要么只钻一个方向,要么什么都想学。正确的做法是:在某一个领域(比如实时计算)建立深度,同时保持对其他领域的了解。
2. 从”解决问题”到”预防问题”
L2 阶段你学会了解决问题,L3 阶段要学会预防问题。设计架构时,要思考:这个系统可能出什么问题?如何提前规避?
3. 开始建立影响力
L3 阶段,你应该开始在团队内建立技术影响力:
- 做技术分享,把你的经验传播出去
- 写技术文档,让后来者少走弯路
- 参与招聘,帮助团队识别人才
- 指导新人,在教的过程中深化理解
4. 保持对业务的敏感度
技术最终是为业务服务的。不要只顾着研究技术,要理解业务目标是什么、数据是如何产生价值的。能用技术解决业务问题的人,永远比只会技术的人更有价值。
接下来
当你能够独立设计复杂的数据架构,有这样的困惑时:
- “我应该如何规划整个公司的数据平台?”
- “团队该怎么组建?流程该怎么设计?”
- “数据平台的 ROI 应该怎么衡量?”
- “新技术那么多,应该投入多少资源跟进?”
恭喜你,你已经准备好进入下一个阶段了。
➡️ L4:技术战略 —— 技术管理、平台规划、组织建设
相关资源:
- 实时数据架构 —— 实时计算架构设计
- 数据仓库与数据湖建模 —— 数据湖技术详解
- 性能优化 —— Spark/Flink 性能调优
- 数据质量管理体系与实践 —— 数据治理方法
- L2:核心构建 —— 如果数仓基础不够扎实,可以回顾