
简介这份PDF资料面向数据仓库初学者与有一定经验但缺乏体系化认知的从业者围绕数仓建模、工具链与平台建设展开帮助读者从写SQL的层面进阶到数据平台应用视角。内容覆盖数仓初识、架构发展史、建模方法论、分层建设、指标体系、事实表与维度表设计规范、开发规范等建模主线并系统梳理Hive、Spark-SQL、Impala、Presto等查询引擎Sqoop、Flume、NiFi等同步工具以及调度、元数据、监控、报表、数据湖、实时数仓与数据资产等模块末尾以三家公司的数仓建设与实时数仓案例收口。资源包共1个PDF文件约254KB轻量便于随时查阅。目前已有405人学习下载适合希望系统补齐数仓知识体系、对照实战案例查漏补缺的读者。1. 数据仓库到底在解决什么问题从两张报表对不上说起月初对账业务方甩过来两张报表一张来自订单系统显示上月成交 1200 万另一张来自财务系统显示 980 万。两边都觉得自己是对的吵到老板那里最后发现是口径不一致——订单系统算的是下单金额财务系统算的是实际收款金额中间还有退款、优惠券分摊、跨月结算没对齐。这种场景在稍微有点规模的公司里几乎每个月都要上演一次。数据仓库要解决的核心问题就是这个把散落在各个业务系统里的数据按统一的口径、统一的粒度、统一的时间基准重新组织一遍让所有人查同一份数据时得到同一个答案。这套「数据仓库理论与实战」的资源面向的就是正在或准备从事数仓方向的工程师。它不讲虚的从数仓的分层架构、维度建模、缓慢变化维、事实表设计一路讲到 ETL 调度、数据质量校验、指标口径管理。适合两类人一类是刚转行做数仓、被分层和建模绕晕的新手另一类是在业务里摸爬滚打了一阵、想回头把理论体系补扎实的熟手。下面我按自己拆这套资料时的理解把关键环节和容易翻车的地方捋一遍。2. 分层架构与维度建模把业务过程翻译成表结构2.1 为什么非要分 ODS、DWD、DWS、ADS 四层很多人第一次看到数仓分层图会觉得多此一举——数据直接从业务库抽过来写报表不就行了我一开始也这么想直到有一次上游订单表加了个字段下游十几张报表全炸了。分层的本质是解耦ODS 层保持和源系统同构只做抽取不做清洗DWD 层做清洗、去重、脱敏、维度退化形成明细事实DWS 层按主题做轻度聚合比如「用户日粒度消费汇总」ADS 层才是面向具体报表的宽表或指标表。这样上游变字段只需要改 DWD 到 ODS 的映射DWS 和 ADS 基本不动。常见做法是 ODS 按天分区全量或增量落地DWD 用拉链表或快照表保存历史变化DWS 按维度组合预聚合ADS 直接对接 BI 工具。参数上分区字段一般用dt日期或hour小时存储格式选 Parquet 或 ORC压缩用 Snappy 兼顾速度和体积。2.2 维度建模星型模型和雪花模型怎么选维度建模是数仓理论里最核心也最容易学歪的部分。星型模型就是一张事实表周围挂一圈维度表维度表不再往下拆雪花模型是把维度表再规范化比如商品维度拆成商品表、类目表、品牌表。理论课上通常说雪花模型节省存储、星型模型查询快但实际项目里我几乎没见过纯雪花模型——因为 BI 工具做关联查询时多一层 join 就多一层性能损耗和口径歧义。选型判断标准很简单如果维度属性经常需要单独维护比如类目树由运营单独管理那就拆出去如果只是商品的一些固定属性颜色、尺码直接冗余在商品维度表里。下面是一个典型的订单事实表建表语句用 Hive 风格写CREATE TABLE dwd_order_detail ( order_id STRING COMMENT 订单ID, user_id STRING COMMENT 用户ID, sku_id STRING COMMENT 商品SKU, order_amount DECIMAL(18,2) COMMENT 下单金额, pay_amount DECIMAL(18,2) COMMENT 实付金额, discount_amount DECIMAL(18,2) COMMENT 优惠金额, order_status INT COMMENT 订单状态, create_time STRING COMMENT 创建时间, pay_time STRING COMMENT 支付时间, dt STRING COMMENT 分区日期 ) PARTITIONED BY (dt STRING) STORED AS PARQUET;这段建表里几个关键点金额字段用DECIMAL而不是FLOAT避免浮点精度丢失dt作为分区字段放在最后Hive 会按日期切分目录order_status用整型编码而不是字符串节省空间也方便枚举。事实表设计时还要注意粒度——是订单粒度还是订单明细粒度如果一张表里既有订单级别的优惠金额又有明细级别的商品数量那就粒度混了聚合时必然翻车。2.3 缓慢变化维拉链表不是唯一解缓慢变化维SCD是数仓面试必问、实战必踩的坑。Type 1 直接覆盖不保留历史Type 2 加start_date和end_date保留版本Type 3 加一列「上一版本值」。很多人一上来就无脑上 Type 2 拉链表结果发现用户维度每天新增几百万条存储扛不住查询还要过滤end_date 9999-12-31。我的经验是核心维度用户、商品、门店用 Type 2因为要追溯历史归属普通维度比如页面来源、渠道分类用 Type 1 覆盖就行没人关心三个月前这个渠道叫什么名字。拉链表的更新逻辑一般用「全量对比 增量合并」先取当天最新快照和昨天拉链表里end_date 9999-12-31的记录做对比有变化的关掉旧链、开新链没变化的保持原样。这个逻辑写起来不复杂但边界条件多——比如同一天内维度变了两次怎么办一般约定取当天最后一次为准。3. ETL 调度与数据质量让数据按时、按质到达3.1 调度依赖怎么配才不出环ETL 调度是数仓的血液循环系统。常见工具是 Airflow、DolphinScheduler、DataWorks 这类核心概念是 DAG有向无环图。配置依赖时最容易犯的错是「隐式依赖」——A 任务读了 B 任务产出的表但调度上没配依赖结果 A 跑的时候 B 还没跑完读到空数据。这种问题在测试环境很难发现因为测试时数据量小、跑得快上了生产就随机翻车。正确做法是每个任务显式声明上游任务和上游表调度系统根据表血缘自动推导依赖。如果调度工具不支持表级血缘那就手动配任务依赖并且加一个「数据就绪检查」节点——检查上游分区是否存在、行数是否大于 0。下面是一个 Airflow 的依赖配置片段from airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime, timedelta default_args { owner: data_team, retries: 2, retry_delay: timedelta(minutes5), depends_on_past: False, } with DAG( dwd_order_detail, default_argsdefault_args, schedule_interval0 2 * * *, # 每天凌晨2点跑 start_datedatetime(2024, 1, 1), catchupFalse, ) as dag: check_ods BashOperator( task_idcheck_ods_partition, bash_commandhive -e SELECT count(1) FROM ods_order WHERE dt\{{ ds }}\, ) build_dwd BashOperator( task_idbuild_dwd_order_detail, bash_commandhive -f /scripts/dwd_order_detail.sql --dt {{ ds }}, ) check_ods build_dwd这里schedule_interval用 cron 表达式控制每天凌晨 2 点执行catchupFalse表示不回补历史任务retries2给两次重试机会。check_ods先确认上游分区有数据再触发build_dwd。参数上{{ ds }}是 Airflow 内置的模板变量代表执行日期这样脚本不用硬编码日期。3.2 数据质量校验别等业务方来投诉数据质量校验要覆盖几个维度完整性分区是否存在、行数是否骤降、唯一性主键是否重复、一致性金额合计是否对得上、及时性任务是否按时完成。我一般会在 DWD 层和 DWS 层之间加一个质量检查任务跑几条 SQL 断言不通过就阻断下游并告警。-- 检查主键重复 SELECT order_id, count(1) AS cnt FROM dwd_order_detail WHERE dt ${dt} GROUP BY order_id HAVING cnt 1; -- 检查金额异常实付大于下单金额 SELECT count(1) FROM dwd_order_detail WHERE dt ${dt} AND pay_amount order_amount; -- 检查行数波动与昨天相比下降超过30% SELECT today.cnt AS today_cnt, yesterday.cnt AS yesterday_cnt, (today.cnt - yesterday.cnt) / yesterday.cnt AS diff_rate FROM (SELECT count(1) AS cnt FROM dwd_order_detail WHERE dt ${dt}) today, (SELECT count(1) AS cnt FROM dwd_order_detail WHERE dt date_sub(${dt}, 1)) yesterday WHERE abs((today.cnt - yesterday.cnt) / yesterday.cnt) 0.3;这三条断言分别对应唯一性、一致性和完整性。实际部署时会把它们包在一个脚本里任何一条返回非空就触发告警。注意date_sub的用法依赖具体 SQL 引擎Hive 用date_subSpark SQL 也支持但 Presto 要用date_add(day, -1, ...)跨引擎迁移时要改。3.3 增量抽取的三种姿势从业务库抽数据到 ODS常见三种方式全量抽取每天拉整张表、增量抽取按update_time拉变化数据、CDC变更数据捕获读 binlog。全量简单但压力大小表可以大表别碰增量抽取要处理「更新丢失」——如果一条记录在两次抽取之间被更新了两次按update_time 上次抽取时间只能拿到最后一次中间状态丢了CDC 最准但架构复杂需要维护 binlog 消费位点。我一般这样选配置表、维度表用全量订单、支付这类流水表用增量 幂等写入按主键 merge用户资料这种频繁更新的表用 CDC。增量抽取的 SQL 模板SELECT * FROM source_order WHERE update_time ${last_extract_time} AND update_time ${current_extract_time};last_extract_time和current_extract_time由调度系统传入一般取上一个调度周期的起止时间。注意时区问题——如果业务库用的是 UTC 时间而数仓用东八区那update_time比较时会差 8 小时这个坑我见过不止一次。4. 避坑与排查那些年数仓翻过的车4.1 现象报表数字对不上但两边 SQL 看起来一样原因最常见的是「空值处理不一致」。比如SUM(amount)遇到NULL会跳过但SUM(COALESCE(amount, 0))会把NULL当 0 算。如果两张报表一张用了COALESCE一张没用结果就差在那些NULL记录上。另一个原因是 join 类型不同——LEFT JOIN和INNER JOIN在维度缺失时行数不一样。解决统一在 DWD 层做空值填充金额类字段默认 0字符串类字段默认空串或UNKNOWN。所有报表都从同一张 DWS 表出不允许直接查 DWD。4.2 现象任务昨天还好好的今天突然 OOM原因数据倾斜。某个热门商品的订单量是普通商品的几千倍按sku_id做 group by 时一个 reduce 任务扛了绝大部分数据。或者分区字段选得不好某一天的数据量是平时的十倍。解决先看执行计划里哪个 stage 耗时最长确认倾斜键。常用手段是「加盐」——给倾斜键拼一个随机后缀打散到多个 reduce再二次聚合。或者单独把倾斜键拎出来处理剩下的走正常逻辑。4.3 现象拉链表查询越来越慢原因拉链表只增不删历史版本越积越多查询时WHERE end_date 9999-12-31过滤不掉旧数据全表扫描。解决按start_date做分区查询时带上分区条件或者定期归档超过一年的历史版本到冷存储。另外拉链表的主键要建索引或 Z-Order 优化否则每次关联都是全表扫。4.4 现象调度任务显示成功但下游读到空数据原因任务写了数据但没提交事务或者写到了临时目录没 move 到正式分区。Hive 的INSERT OVERWRITE如果中途失败可能留下一个空分区。解决在任务最后加一个「分区修复」步骤检查目标分区行数是否大于 0不满足就抛异常。另外写入时用INSERT OVERWRITE而不是INSERT INTO避免重复数据累积。4.5 现象指标口径改了但历史数据没重跑原因业务方说「从今天起退款金额要从成交额里扣掉」你改了 SQL但只跑了今天的分区历史分区还是旧口径。结果做同比时发现口径不一致。解决建立「指标版本」概念每次口径变更记录变更日期和影响范围历史数据要么重跑要么在报表层标注口径切换点。重跑成本高的话至少在 ADS 层加一个metric_version字段让业务方知道数据是怎么算出来的。5. 指标口径管理与血缘追溯一个具体技巧数仓做到后面最头疼的不是技术是「口径管理」。同一个「活跃用户」运营说打开 App 就算产品说要有浏览行为才算财务说要有消费才算。如果没有一套机制来管理这些定义数仓工程师就会变成「取数机器」每天被各种临时需求淹没。我自己的做法是建一张「指标字典表」把每个指标的口径、计算逻辑、责任人、变更记录都落进去。这张表本身也是数仓的一部分用 Hive 表存每天同步到 BI 工具供业务方查询。结构大概这样字段类型说明metric_codeSTRING指标唯一编码如active_user_daumetric_nameSTRING指标中文名definitionSTRING口径描述如「当日有浏览行为的去重用户数」sql_logicSTRING核心计算逻辑可粘贴 SQL 片段ownerSTRING责任人versionINT版本号每次口径变更递增effective_dateSTRING生效日期update_timeSTRING最后更新时间有了这张表业务方来问「活跃用户怎么算的」直接甩链接口径要改先改字典表再改 SQL变更记录自动留痕。配合调度系统的表血缘功能还能做到「改了 DWD 的某个字段自动通知所有下游指标责任人」。血缘追溯这块常见做法是用 SQL 解析工具比如 Apache Calcite 或开源的 SQLLineage从 ETL 脚本里提取表级和字段级依赖写入图数据库。查询时输入一张表名就能看到它上游依赖哪些表、下游影响哪些报表。我一般会在调度系统里集成这个能力每次发布新任务时自动更新血缘图。最后说一个我自己的习惯每次上线新的 ETL 任务前强制走一遍「空跑 对账」。空跑是把dt设成一个历史日期看任务能不能正常产出对账是拿新任务的产出和旧口径的结果做 diff差异超过阈值就人工确认。这个习惯帮我拦住了至少三次重大口径事故。希望帮到你。本文还有配套的精品资源点击获取