小肥柴的Hadoop之旅 快速实验篇(B02)DWD 生产级升级
电商用户行为分析实战 · B 组 · 第二阶段
上游:B01 产出
dwd_user_behavior_orc(180570 行,非分区)技术栈:Hive on MR(纯 SQL,无新 MR 程序)
目录
- [前言:B02 到底在做什么](#前言:B02 到底在做什么)
- [0. 实验全景:从 B01 冻结表到 B02 分区资产](#0. 实验全景:从 B01 冻结表到 B02 分区资产)
- [1. B01 留下的三处"将就"](#1. B01 留下的三处"将就")
- [1.1 将就一:主表无分区](#1.1 将就一:主表无分区)
- [1.2 将就二:时间字段是字符串](#1.2 将就二:时间字段是字符串)
- [1.3 将就三:没有会话内序号](#1.3 将就三:没有会话内序号)
- [2. B02 的使命:从"能用"到"好用"](#2. B02 的使命:从"能用"到"好用")
- [2.1 任务定调](#2.1 任务定调)
- [2.2 B02 不做什么(划清边界)](#2.2 B02 不做什么(划清边界))
- [2.3 B02 与 B01 的关系](#2.3 B02 与 B01 的关系)
- [3. 四个关键设计决策](#3. 四个关键设计决策)
- [3.1 决策一:dt 分区](#3.1 决策一:dt 分区)
- [3.2 决策二:ts timestamp](#3.2 决策二:ts timestamp)
- [3.3 决策三:seq_in_session](#3.3 决策三:seq_in_session)
- [3.4 决策四:子表补 ts 和 dt](#3.4 决策四:子表补 ts 和 dt)
- [4. 脚本逐个讲](#4. 脚本逐个讲)
- [4.0 五个脚本的依赖链](#4.0 五个脚本的依赖链)
- [4.1 脚本 00:备份(可回退)](#4.1 脚本 00:备份(可回退))
- [4.2 脚本 01:建表(DDL 与 DML 分离)](#4.2 脚本 01:建表(DDL 与 DML 分离))
- [4.3 脚本 02:灌数据(★ 最重一步)](#4.3 脚本 02:灌数据(★ 最重一步))
- [4.4 脚本 03:重建四张子表](#4.4 脚本 03:重建四张子表)
- [4.5 脚本 04:统一验证](#4.5 脚本 04:统一验证)
- [4.6 脚本 05:质量报告](#4.6 脚本 05:质量报告)
- [5. 卡顿排查与备用方案(专题)](#5. 卡顿排查与备用方案(专题))
- [5.1 第一步永远是先看进程](#5.1 第一步永远是先看进程)
- [5.2 脚本 02 卡 / OOM:本地模式陷阱](#5.2 脚本 02 卡 / OOM:本地模式陷阱)
- [5.3 脚本 03 慢:4 个 MR 串行](#5.3 脚本 03 慢:4 个 MR 串行)
- [5.4 脚本 04 卡:24 个 UNION ALL 编译慢](#5.4 脚本 04 卡:24 个 UNION ALL 编译慢)
- [5.5 脚本 05 卡:
{ }组合命令](#5.5 脚本 05 卡:{ } 组合命令) - [5.6 为什么不改 hive-env.sh 的堆](#5.6 为什么不改 hive-env.sh 的堆)
- [6. 问题与修正](#6. 问题与修正)
- [6.1 本地模式 OOM](#6.1 本地模式 OOM)
- [6.2 交互式终端 PS2 卡死](#6.2 交互式终端 PS2 卡死)
- [6.3 Hive 日志显示 dt=null](#6.3 Hive 日志显示 dt=null)
- [6.4 交叉核对 Ambiguous column](#6.4 交叉核对 Ambiguous column)
- [6.5 04 脚本编译慢](#6.5 04 脚本编译慢)
- [7. 验证与守恒](#7. 验证与守恒)
- [7.1 主表守恒](#7.1 主表守恒)
- [7.2 seq 不变量](#7.2 seq 不变量)
- [7.3 ts 不变量](#7.3 ts 不变量)
- [7.4 子表守恒](#7.4 子表守恒)
- [7.5 与 B01 对照](#7.5 与 B01 对照)
- [8. B02 对下游的贡献(核心章节)](#8. B02 对下游的贡献(核心章节))
- [9. 工程收获](#9. 工程收获)
- [10. 方法收获](#10. 方法收获)
- [11. 遗留问题](#11. 遗留问题)
- [12. 结语](#12. 结语)
- [附录 A:B02 交付清单](#附录 A:B02 交付清单)
- [附录 B:脚本 04 完整验证结果](#附录 B:脚本 04 完整验证结果)
- [附录 C:执行环境](#附录 C:执行环境)
前言:B02 到底在做什么
B01 完成时,B 组实验有了一张 180570 行的主 DWD 表 dwd_user_behavior_orc,四张多值子表,以及一份已被验证的数据认知。
但 B01 的主表和子表都是"能用"的表,不是"好用"的表。
- "能用"是指:查数、聚合、算结果,都没问题。
- "好用"是指:下游 10 个项目(B03--B12)用起来,不用重复劳动、不用各自处理公共问题、不会口径分叉。
B02 要做的,就是把"能用"升级成"好用"。
这份报告和一般"施工说明"不同,它不只写"建了哪些表、跑了什么 SQL",还写每个脚本是为了解决谁的痛点、为什么这样设计、不这样做会怎样 。因为真实的数据工程里,最贵的不是敲代码,是重复劳动和口径分叉。
全文围绕四个关键设计决策展开,每个决策都从一个具体的下游痛点出发。执行过程中踩了 5 个坑,每个坑都对应一次修正。这些修正,才是 B02 对后续 B03--B15 的真正贡献。
0. 实验全景:从 B01 冻结表到 B02 分区资产
先给一张全景图。
text
B01 产出(冻结,回退用)
/b_group/dwd/user_behavior_orc 非分区 ORC,180570 行,25 列
├── dwd_order_category 非分区 ORC,35188 行
├── dwd_order_product 非分区 ORC,23597 行
├── dwd_pay_category 非分区 ORC,24147 行
└── dwd_pay_product 非分区 ORC,16096 行
│
│ B02 五个脚本
│ ① 00 备份 B01 子表 → *_b01
│ ② 01 建主分区表 dwd_user_behavior
│ ③ 02 动态分区写入 + 算 ts + 算 seq_in_session
│ ④ 03 重建四张分区子表
│ ⑤ 04/05 统一验证 + 质量报告
▼
B02 产出(正式消费层)
主表
└── b_group.dwd_user_behavior 分区 ORC,180570 行,11 分区
含 ts timestamp + seq_in_session bigint
子表(同名重建,均分区)
├── b_group.dwd_order_category 分区 ORC,35188 行,含 ts
├── b_group.dwd_order_product 分区 ORC,23597 行,含 ts
├── b_group.dwd_pay_category 分区 ORC,24147 行,含 ts
└── b_group.dwd_pay_product 分区 ORC,16096 行,含 ts
备份
└── b_group.dwd_*_b01 四张 B01 子表备份(回退用)
- 小结 :B01 给了 10 个下游项目"一份能用的数据",B02 把它们各自要处理的公共问题,一次性前置做掉。
1. B01 留下的三处"将就"
B01 的任务是"摸清数据",不是"建消费级资产"。B01 选择先出可用 DWD,把结构优化留到 B02,是正确的分阶段。但 B02 开始时,必须先把这三处"将就"看清楚,才能设计对症的升级方案。
1.1 将就一:主表无分区
现象 :dwd_user_behavior_orc 是一张非分区表,180570 行全部在一个 HDFS 目录里。
证据 :B01 表结构没有 PARTITIONED BY 子句。
后果:
任何按天查询,例如 B03 漏斗想按天看:
sql
SELECT dt, behavior_type, COUNT(*)
FROM dwd_user_behavior_orc
WHERE dt = '2019-07-17'
GROUP BY dt, behavior_type;
即使 WHERE dt = '2019-07-17' 只命中 16627 行,Hive 也必须扫全表 180570 行 。因为 Hive 不知道 dt 是分区键,只当普通列处理。
代价:
- 扫描量 × 11(11 天,任何一天都要扫全表)
- B03、B04、B06、B09、B10 全都按天或跨天分析,每个项目都要吃这个亏
这不是 B01 的错:B01 优先把数据落地,分区优化留到 B02,避免一次做太多。
1.2 将就二:时间字段是字符串
现象 :action_time 是字符串,格式 2019-07-17 10:00:00。
证据 :B01 表结构里 action_time string。
后果:
任何时间差分析,例如 B07 想算 session 时长:
sql
SELECT session_id,
UNIX_TIMESTAMP(MAX(action_time)) - UNIX_TIMESTAMP(MIN(action_time)) AS duration
FROM dwd_user_behavior_orc
GROUP BY session_id;
每次都要显式写 UNIX_TIMESTAMP() 。这不是"多打几个字"的问题,是口径一致性问题:
- B06 可能写
unix_timestamp - B07 可能写
to_unix_timestamp - B12 可能写
unix_timestamp(action_time, 'yyyy-MM-dd HH:mm:ss')
三种写法,理论上等价,但任何一处写错格式,结果就错,且不易发现。
B02 的解法 :在 DWD 层一次性算好 ts timestamp,下游直接用。
1.3 将就三:没有会话内序号
现象 :dwd_user_behavior_orc 里,session 内的动作顺序没有任何现成的排序字段。
证据 :B01 §3.5 已实测,raw_line_id 不能当顺序------3908 行(2.16%)在 session 内时间倒流,文件顺序 ≠ 时间顺序。
后果:
任何序列分析(B06 页面单跳、B07 会话序列、B12 异常检测),都要自己写窗口函数:
sql
ROW_NUMBER() OVER (
PARTITION BY session_id
ORDER BY action_time, raw_line_id
) AS seq
而每个项目的写法会分叉:
- B06 可能写
ORDER BY action_time - B07 可能写
ORDER BY action_time, raw_line_id - B12 可能写
ORDER BY action_time DESC
排序键不同 = 序列结果不同 = 分析结论不同 。而 B01 已经明确告诉我们:必须用 action_time + raw_line_id 双键,否则同秒内多个动作的相对顺序不稳定。
B02 的解法 :在 DWD 层一次性算好 seq_in_session,下游全部用它,一个来源,一个口径。
2. B02 的使命:从"能用"到"好用"
2.1 任务定调
B02 不是再造一张 DWD,而是把 B01 的 DWD 升级成下游愿意每天用的资产。
"愿意每天用"意味着四件事:
| 维度 | B01 状态 | B02 目标 |
|---|---|---|
| 查得快 | 无分区,全表扫 | dt 分区,按天过滤 |
| 算得准 | 字符串时间,易写错 | ts timestamp,统一口径 |
| 写得少 | 无 seq,每次自己算 | seq_in_session,直接可用 |
| 口径统一 | 子表无 dt/ts | 子表补 dt、ts,与主表对齐 |
2.2 B02 不做什么(划清边界)
明确边界,避免范围膨胀:
- 不重建
dwd_user_behavior_dirty:B01 实测真缺失 = 0,脏表为空,无重建价值。 - 不引入城市/品类维表:维表是 B09 的任务,B02 只做结构升级,不做语义补全。
- 不改子表字段语义 :保持
category_id/product_id独立,不重新尝试配对(B01 已推翻配对假设)。 - 不动 07-27 数据:疑截断只是标注,不剔除、不修正。
- 不改
hive-env.sh的堆:2GB 小集群上给 Hive CLI 加堆会挤压 master 上其他进程,选择通过拆脚本而不是加堆来解决编译慢的问题(详见 §5.6)。
2.3 B02 与 B01 的关系
B02 不是 B01 的重复,而是B01 数据认知的固化:
| B01 的实测认知 | B02 的固化动作 |
|---|---|
| 数据覆盖 11 天,不是 1 天 | 按 dt 分 11 个分区 |
| 2.16% 行在 session 内乱序 | 用 action_time, raw_line_id 算 seq_in_session |
| 品类与产品不配对 | 保持四张独立子表,不合并 |
| 脏数据极少 | 不做额外清洗,只做结构升级 |
| 07-27 疑截断 | 分区保留,标注异常 |
B02 的每一个动作,都能追到 B01 的一条实测结论。
3. 四个关键设计决策
3.1 决策一:dt 分区
做了什么选择?
主表按 dt 分区,11 个分区。
如果不做,后续哪些项目会难受?
- B03 漏斗:想按天看转化率,每次全表扫。
- B04 品类 Top10:想按天看品类排名,每次全表扫。
- B06 页面单跳:想按天看跳转,每次全表扫。
- B09 城市偏好:想按天看区域热度,每次全表扫。
一句话 :11 天数据,每次按天查都扫全表 = 11 倍浪费。
为什么不选另一个?
- 不选分桶:数据量小,分桶收益不明显,且分桶后查询语法更复杂。
- 不选多重分区 :如
PARTITION BY (dt, behavior_type),分区数膨胀到 44 个,小集群管理成本高,且行为类型过滤用WHERE就够。
代价:11 个分区目录,HDFS 小文件稍多,但 ORC + Snappy 后每分区几百 KB,可接受。
3.2 决策二:ts timestamp
做了什么选择?
主表和子表都新增 ts timestamp 字段,与 action_time 字符串并存。
如果不做,后续哪些项目会难受?
- B06 页面单跳:算页面对时间间隔,每次
unix_timestamp。 - B07 会话时长:同上。
- B12 异常检测:判断"秒级高频",每次
unix_timestamp。
一句话 :时间差分析是 B06/B07/B12 的公共需求,每次各自算一遍 = 三份口径。
为什么保留字符串 action_time?
兼容性。B01 已有 20+ 下游查询基于 action_time,不动它,避免破坏 B01 已跑过的所有验证 SQL。
为什么不只留 ts?
ts 是 Hive 的 timestamp 类型,字符串展示时格式可能带毫秒(2019-07-17 10:00:00.0),与 B01 报告的字符串格式不完全一致。保留字符串便于与 B01 结果做 diff。
代价:多一个字段,多占 ORC 存储。但 ORC 列存 + Snappy 压缩后,增量很小。
3.3 决策三:seq_in_session
做了什么选择?
主表新增 seq_in_session bigint,每个 session 内从 1 开始,按 action_time, raw_line_id 排序。
如果不做,后续哪些项目会难受?
- B06 页面单跳:每次自己
ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY action_time, raw_line_id)。 - B07 会话序列:同上。
- B12 异常检测:同上。
一句话 :B06/B07/B12 都依赖"session 内动作顺序",这个顺序本身已经由 B01 明确了规则,不该每个项目重算。
为什么排序键是 action_time, raw_line_id 双键?
B01 §3.5 实测:2.16% 的行 raw_line_id 顺序与 action_time 不一致。但同秒内的多个动作,只有 raw_line_id 能区分先后 。单用 action_time 会丢信息,单用 raw_line_id 会倒序,必须双键。
为什么算在 DWD 层,不算在 ADS 层?
DWD 层的 seq_in_session 是事实数据(这个动作确实是该 session 的第 N 个),ADS 层才是分析结果。放在 DWD 层,所有下游共享同一份事实。
代价 :灌数据时多一个窗口函数计算,02 脚本耗时增加。但一次性成本。
3.4 决策四:子表补 ts 和 dt
做了什么选择?
四张子表从"5 列无分区"升级到"6 列 + dt 分区",含 ts。
如果不做,后续哪些项目会难受?
- B04 品类 Top10:想按天看品类热度,必须 join 主表取
dt。 - B10 品类共现:想按天看共现趋势,同上。
- B13 ADS 品类主题表:想直接聚合子表,被字段缺失挡住。
一句话 :子表是"按品类/产品聚合"的直接入口,不给它 dt,每次都要 join,慢且易错。
为什么不在子表里也加 seq_in_session?
子表是 explode 后的结果,一行 = 一个品类 ID,本身没有"动作"的概念 。seq_in_session 属于主表行为行,不属于品类行。
代价 :四张子表各多一个 ts 字段,ORC 存储增量小。
4. 脚本逐个讲
4.0 五个脚本的依赖链
五个脚本不是平铺的,是一条有依赖的链:
text
脚本 00:备份 B01 子表
│ 必须先执行(否则 03 重建时会重名冲突)
▼
脚本 01:建主表 dwd_user_behavior(空表)
│ 必须先于 02(表先存在)
▼
脚本 02:灌主表数据(180570 行 → 11 分区)
│ 必须先于 03(03 从主表读)
▼
脚本 03:重建四张子表(从主表 explode)
│
▼
脚本 04:统一验证(读主表 + 四张子表)
│
▼
脚本 05:质量报告(读 04 的输出)
设计原则:
- 每一步只做一件事:建表就是建表,灌数就是灌数。出错好定位。
- 每一步都有回退路径 :00 备份提供回退,02 失败只需
TRUNCATE重跑。 - 后一步依赖前一步产出:形成链条,不会乱序。
为什么不是一个大 SQL 一次跑完?
- 出错时不知道哪一步出问题
- 无法回退到中间状态
- 无法针对单步调优(如 02 的 OOM 问题,只有拆开才能定位)
小步、可验证、可回退,这是数据工程的通用实践。
4.1 脚本 00:备份(可回退)
如果不做这一步,后续哪些项目会难受?
如果 03 的 explode 逻辑写错,改了 35188 行数据,你拿什么对照?只能看 B01 报告里的数字。而备份表能直接跑 SQL 对比。
这一步具体做什么?
把 B01 的四张子表 ALTER TABLE ... RENAME TO ... _b01。rename 是元数据操作,不搬数据,秒级完成,0 成本。
做完怎么验证?
SHOW TABLES 应看到 4 张 _b01 后缀表,原表名消失;备份表行数应与 B01 一致。
实测结果:
text
dwd_order_category_b01 = 35188
dwd_order_product_b01 = 23597
dwd_pay_category_b01 = 24147
dwd_pay_product_b01 = 16096
四条与 B01 完全一致。
为什么这个脚本看起来"多余"?
工程第一原则:改动前先留退路。花 5 秒钟备份,换来后面所有步骤的"敢改"。
4.2 脚本 01:建表(DDL 与 DML 分离)
如果不做这一步,后续哪些项目会难受?
如果直接 CREATE TABLE AS SELECT,把建表和灌数合成一步:
- 列名写错 → 重跑整个 MR,几分钟浪费
- 类型写错 → 同上
- 分区写错 → 同上
这一步具体做什么?
只 CREATE TABLE,不灌数据。表建好后是空的,SHOW PARTITIONS 无输出。
做完怎么验证?
DESC dwd_user_behavior 应看到 27 列(26 普通 + dt 分区)。
实测结果:
text
user_id bigint
session_id string
page_id bigint
action_time string
ts timestamp ← 新增
city_id bigint
behavior_type string
behavior_combo string
search_keyword string
click_category_id bigint
click_product_id bigint
order_category_ids array<string>
order_product_ids array<string>
pay_category_ids array<string>
pay_product_ids array<string>
is_search_field_used boolean
is_click_field_used boolean
is_order_field_used boolean
is_pay_field_used boolean
is_valid_behavior boolean
is_multi_behavior boolean
is_unclassified boolean
is_true_missing boolean
quality_flags string
raw_line_id string
seq_in_session bigint ← 新增
dt string ← 分区字段
27 列全部正确。 SHOW PARTITIONS 空,符合预期。
为什么要 DDL 和 DML 分离?
数仓的通用做法:DDL 快速迭代,DML 一次到位。
CREATE TABLE秒级完成,列名类型分区写错随时DROP重来INSERT要跑 MR,几分钟,一次做对
先结构后数据 = 把试错成本降到最低。
4.3 脚本 02:灌数据(★ 最重一步)
如果不做这一步,后续哪些项目会难受?
这一步是 B02 的核心。主表建了但没数据,等于没做。同时这一步也是唯一生成 ts 和 seq_in_session 的地方------B06/B07/B12 全靠它。
这一步具体做什么?
从 B01 的 dwd_user_behavior_orc 读数据,动态分区 写入 dwd_user_behavior 的 11 个 dt 分区,同时:
CAST(action_time AS TIMESTAMP)生成tsROW_NUMBER() OVER (PARTITION BY session_id ORDER BY action_time, NVL(CAST(raw_line_id AS BIGINT), 0))生成seq_in_session
做完怎么验证?
5 个关键不变量:
- 主表总行数 = 180570
- 分区数 = 11
seq_dup= 0(每 session 内 seq 唯一)seq_not_start_1= 0(每 session seq 从 1 开始)ts_null= 0(ts 无 NULL)
实测结果(首次执行失败,修正后成功):
text
main_total,180570
partition_cnt,11
seq_dup,0
seq_not_start_1,0
ts_null,0
五项全对。
日期分布与 B01 完全一致:
text
2019-07-17 16627
2019-07-18 16694
2019-07-19 16636
2019-07-20 16610
2019-07-21 16773
2019-07-22 16705
2019-07-23 16518
2019-07-24 16772
2019-07-25 16820
2019-07-26 16615
2019-07-27 13800
为什么这个脚本"最重"?
三个原因:
- 一次处理 180570 行,源表 18MB,写入含新字段后约 4.2MB。
- 窗口函数 + 动态分区,对 Hive CLI 和 YARN 都有压力。
- 首次执行失败:本地模式 OOM,需要修正(详见 §6.1)。
执行耗时 :修正后(YARN 模式)约 62 秒。
4.4 脚本 03:重建四张子表
如果不做这一步,后续哪些项目会难受?
B01 已有四张子表,为什么还重建?
核心:新子表带 ts 和 dt。
对比:
| 字段 | B01 子表 | B02 子表 |
|---|---|---|
| user_id / session_id / action_time / city_id / category_id | ✅ | ✅ |
| ts | ❌ | ✅ |
| dt(分区) | ❌ | ✅ |
作用:
- B04 品类 Top10 按天聚合:
GROUP BY dt, category_id直接可查,不用 join 主表。 - B10 品类共现按天看趋势:同样直接查。
这一步具体做什么?
从新的 dwd_user_behavior 主表读,LATERAL VIEW explode(...) 炸裂多值字段,重建四张分区表。
做完怎么验证?
四张子表行数应与 B01 备份表完全一致:
text
dwd_order_category = 35188 (= B01)
dwd_order_product = 23597 (= B01)
dwd_pay_category = 24147 (= B01)
dwd_pay_product = 16096 (= B01)
实测结果:
text
order_category,35188,35188,OK
order_product,23597,23597,OK
pay_category,24147,24147,OK
pay_product,16096,16096,OK
四张子表与 B01 备份完全一致 。每张子表也是 11 个分区,含 ts 字段。
为什么从新主表 explode,不从 B01 备份表 explode?
统一数据源。B02 之后,所有下游都从 dwd_user_behavior 出发。子表从主表派生,dt 分区天然一致,ts 字段天然对齐。
执行耗时 :4 个 MR 串行,约 2~5 分钟。
4.5 脚本 04:统一验证
如果不做这一步,后续哪些项目会难受?
- B15 复盘:手里没有一份"B02 所有不变量一次性验证通过"的记录,只能翻聊天记录。
- 以后任何人质疑 B02 数据:需要一条命令重跑全部验证,而不是凭记忆零散敲 SQL。
- B03 开始前的自检:需要一份"底座健康检查"的固定清单。
这一步具体做什么?
把散落的验证收拢成一条 SQL,输出 24 行 metric,value 两列,可直接重定向为 CSV。
做完怎么验证?
24 个指标逐项对照 B01。
实测结果:
text
main_total,180570
partition_cnt,11
seq_dup,0
seq_not_start_1,0
ts_null,0
behavior_click,120519
behavior_order,11768
behavior_pay,8051
behavior_search,40232
distinct_user,100
distinct_session,9457
distinct_city,26
distinct_page,50
dt_min,2019-07-17
dt_max,2019-07-27
order_category_rows,35188
order_product_rows,23597
pay_category_rows,24147
pay_product_rows,16096
order_category_parts,11
order_product_parts,11
pay_category_parts,11
pay_product_parts,11
dt_0727_rows,13800
24 项全对。
为什么这个脚本"像卡住"?
24 个 UNION ALL 含 4 个嵌套子查询,Hive CLI 的 256MB 堆编译很吃力 。不是数据大,是执行计划复杂 。解决方式:拆成 4 个脚本 04a/04b/04c/04d(详见 §5.4)。
4.6 脚本 05:质量报告
如果不做这一步,后续哪些项目会难受?
- B15 复盘 :需要一个时间戳 + 机器名 + 全部指标的归档件。
- 报告/博客写作:质量报告是"数据事实"的原始来源,避免手写数字出错。
- 以后重跑 B02:拿新旧质量报告 diff,一眼看出哪里变了。
这一步具体做什么?
把 04 的输出加头部(时间戳、机器名、来源、基线文件)+ 尾部(07-27 标注)拼成 CSV。
实测结果:
text
=== B02 Quality Report ===
record_time,2026-10-01 18:53:32
hostname,master
stage,B02
source,B01 dwd_user_behavior_orc
upstream_baseline,output/b02_baseline.txt
behavior_click,120519
behavior_order,11768
behavior_pay,8051
behavior_search,40232
order_category_rows,35188
order_product_rows,23597
pay_category_rows,24147
order_category_parts,11
pay_product_rows,16096
order_product_parts,11
distinct_session,9457
pay_category_parts,11
pay_product_parts,11
dt_0727_rows,13800
main_total,180570
seq_not_start_1,0
distinct_user,100
dt_min,2019-07-17
dt_max,2019-07-27
partition_cnt,11
distinct_city,26
distinct_page,50
seq_dup,0
ts_null,0
dt_0727_anomaly,suspected_truncation
头部 + 24 项 + 标注,完整齐整。
5. 卡顿排查与备用方案(专题)
B02 执行过程中,"卡住"发生了 3 次。这一节把所有排查思路和备用方案集中。
5.1 第一步永远是先看进程
任何时候觉得卡住,先在另一个终端执行:
bash
ps -ef | grep -E 'hive|CliDriver' | grep -v grep
两种结果:
| 结果 | 含义 | 处理 |
|---|---|---|
看到 org.apache.hadoop.hive.cli.CliDriver 进程 |
Hive 正在跑,只是慢 | 回原窗口,再等 30~120 秒 |
只有 grep 那一行,无 hive 进程 |
真卡在 shell 层 | 回原窗口 Ctrl+C |
关键经验:
2GB 小集群上,Hive CLI 启动 + 脚本跑完,慢的话可能要 60~180 秒。
你未必是真卡,可能只是慢。先看进程再判断。
同时看 Hive 日志:
bash
tail -50 /tmp/$(whoami)/hive.log
若日志持续输出新行,说明 Hive 在跑;若 30 秒无新行,可能真卡。
5.2 脚本 02 卡 / OOM:本地模式陷阱
症状:
text
java.lang.OutOfMemoryError: Java heap space
at org.apache.hadoop.mapred.MapTask$MapOutputBuffer.init(MapTask.java:1001)
根因:
- 数据 <
hive.exec.mode.local.auto.inputbytes.max(默认 128MB),Hive 自动切到本地模式。 - 本地模式下,MR 跑在 Hive CLI 自己的 JVM 里 ,
mapreduce.map.memory.mb=512和-Xmx400m被忽略。 - Hive CLI 默认堆只有 256MB。
mapreduce.task.io.sort.mb=128要求一次性分配 128MB 缓冲区。- 256MB − 128MB 缓冲 − Hive 自身开销 = OOM。
修正:
sql
SET hive.exec.mode.local.auto=false; -- 强制走 YARN
SET mapreduce.task.io.sort.mb=64; -- 缓冲减半
验证 :修正后 02 耗时 61.668 秒成功,map=100%, reduce=100%。
备用方案(若 YARN 也 OOM):
逐天静态分区插入,单次压力降到 1/11:
bash
cd /home/hadoop/b02
for d in 2019-07-17 2019-07-18 2019-07-19 2019-07-20 2019-07-21 \
2019-07-22 2019-07-23 2019-07-24 2019-07-25 2019-07-26 2019-07-27; do
echo "=== 插入 dt=$d ==="
hive -S -e "
USE b_group;
SET hive.exec.mode.local.auto=false;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx400m;
SET mapreduce.reduce.memory.mb=512;
SET mapreduce.reduce.java.opts=-Xmx400m;
SET mapreduce.task.io.sort.mb=64;
SET mapreduce.job.reduces=1;
INSERT INTO TABLE dwd_user_behavior PARTITION(dt='$d')
SELECT user_id, session_id, page_id, action_time,
CAST(action_time AS TIMESTAMP), city_id, behavior_type, behavior_combo,
search_keyword, click_category_id, click_product_id,
order_category_ids, order_product_ids, pay_category_ids, pay_product_ids,
is_search_field_used, is_click_field_used, is_order_field_used, is_pay_field_used,
is_valid_behavior, is_multi_behavior, is_unclassified, is_true_missing,
quality_flags, raw_line_id,
ROW_NUMBER() OVER (PARTITION BY session_id
ORDER BY action_time, NVL(CAST(raw_line_id AS BIGINT), 0))
FROM dwd_user_behavior_orc
WHERE is_true_missing=false AND dt='$d';
" 2>&1 | tail -5
done
重要副作用 :静态分区时,ROW_NUMBER() 在每个分区内独立计算 。跨天 session 的 seq 会按天重排,与动态分区插入不一致。
先检查是否有跨天 session:
bash
hive -S -e "
USE b_group;
SELECT COUNT(*) FROM (
SELECT session_id FROM dwd_user_behavior_orc
GROUP BY session_id HAVING COUNT(DISTINCT dt) > 1
) t;
" 2>/dev/null
- 结果 = 0:无跨天 session,静态分区安全。
- 结果 > 0:seq 语义会变,需在报告里说明。
5.3 脚本 03 慢:4 个 MR 串行
症状 :脚本 03 执行时,每张子表一个 MR,总耗时 2~5 分钟。
这不是问题,是正常行为 。Hive 3.1.3 对多语句脚本的调度是串行的,不会并行。
怎么看当前跑到第几张?
bash
# 看 Hive 日志最新的 SQL 是 DROP / CREATE / INSERT 哪一句
tail -30 /tmp/$(whoami)/hive.log | grep -E 'DROP|CREATE|INSERT|Loading data'
# 看 03 的执行日志最新
tail -20 output/b02_03_subtables.log
若看到 Loading data to table b_group.dwd_order_category,说明正在灌第一张。
若看到 Loading data to table b_group.dwd_order_product,说明第一张完成,正在第二张。
以此类推。
备用方案(若某张失败):拆成 4 个独立脚本逐张执行。
bash
# 从 03 脚本中切出第一张子表的部分
awk '/1\. 订单-品类/,/2\. 订单-产品/' sql/03_rebuild_subtables_partitioned.hql > sql/03a_order_category.hql
# 用 head/tail 手工切也可,关键是每个文件只留一个 DROP+CREATE+INSERT
更推荐:用 hive -e 直接跑一张子表(脚本里 SET 和 INSERT 一起写)。这种方式定位问题最快。
5.4 脚本 04 卡:24 个 UNION ALL 编译慢
症状 :04 执行后终端长时间无输出。进程检测发现 Hive 在跑。
根因:
- Hive CLI 默认堆
-Xmx256m。 04有 24 个UNION ALL,其中 4 个含嵌套子查询。- Hive 编译器要整体解析 这 24 个分支,把嵌套子查询展开,生成执行计划,再开始跑。
- 256MB 堆处理 24 个分支的编译 = 慢。
怎么看进度?
bash
# 方式 1:看进程还在不在
ps -ef | grep hive | grep -v grep
# 方式 2:看 Hive 日志
tail -20 /tmp/$(whoami)/hive.log
# 方式 3:看结果文件有没有生成
ls -la output/b02_verify_result.csv
如果进程在、日志有新行、文件未生成 → 还在编译或跑,继续等 。
如果进程还在但日志 30 秒无新行 → 可能 OOM 僵死,Ctrl+C 后拆脚本。
备用方案:拆成 4 个脚本
bash
cd /home/hadoop/b02
cat > sql/04a_verify_core.hql << 'EOF'
USE b_group;
SELECT 'main_total' AS metric, CAST(COUNT(*) AS STRING) AS value FROM dwd_user_behavior
UNION ALL SELECT 'partition_cnt', CAST(COUNT(DISTINCT dt) AS STRING) FROM dwd_user_behavior
UNION ALL SELECT 'seq_dup', CAST(COUNT(*) AS STRING) FROM (SELECT session_id, seq_in_session FROM dwd_user_behavior GROUP BY session_id, seq_in_session HAVING COUNT(*)>1) t
UNION ALL SELECT 'seq_not_start_1', CAST(COUNT(*) AS STRING) FROM (SELECT session_id FROM dwd_user_behavior GROUP BY session_id HAVING MIN(seq_in_session)<>1) t
UNION ALL SELECT 'ts_null', CAST(COUNT(*) AS STRING) FROM dwd_user_behavior WHERE ts IS NULL;
EOF
cat > sql/04b_verify_behavior.hql << 'EOF'
USE b_group;
SELECT CONCAT('behavior_', behavior_type) AS metric, CAST(COUNT(*) AS STRING) AS value
FROM dwd_user_behavior GROUP BY behavior_type;
EOF
cat > sql/04c_verify_dim.hql << 'EOF'
USE b_group;
SELECT 'distinct_user' AS metric, CAST(COUNT(DISTINCT user_id) AS STRING) AS value FROM dwd_user_behavior
UNION ALL SELECT 'distinct_session', CAST(COUNT(DISTINCT session_id) AS STRING) FROM dwd_user_behavior
UNION ALL SELECT 'distinct_city', CAST(COUNT(DISTINCT city_id) AS STRING) FROM dwd_user_behavior
UNION ALL SELECT 'distinct_page', CAST(COUNT(DISTINCT page_id) AS STRING) FROM dwd_user_behavior
UNION ALL SELECT 'dt_min', MIN(dt) FROM dwd_user_behavior
UNION ALL SELECT 'dt_max', MAX(dt) FROM dwd_user_behavior
UNION ALL SELECT 'dt_0727_rows', CAST(COUNT(*) AS STRING) FROM dwd_user_behavior WHERE dt='2019-07-27';
EOF
cat > sql/04d_verify_subtables.hql << 'EOF'
USE b_group;
SELECT 'order_category_rows' AS metric, CAST(COUNT(*) AS STRING) AS value FROM dwd_order_category
UNION ALL SELECT 'order_product_rows', CAST(COUNT(*) AS STRING) FROM dwd_order_product
UNION ALL SELECT 'pay_category_rows', CAST(COUNT(*) AS STRING) FROM dwd_pay_category
UNION ALL SELECT 'pay_product_rows', CAST(COUNT(*) AS STRING) FROM dwd_pay_product
UNION ALL SELECT 'order_category_parts', CAST(COUNT(DISTINCT dt) AS STRING) FROM dwd_order_category
UNION ALL SELECT 'order_product_parts', CAST(COUNT(DISTINCT dt) AS STRING) FROM dwd_order_product
UNION ALL SELECT 'pay_category_parts', CAST(COUNT(DISTINCT dt) AS STRING) FROM dwd_pay_category
UNION ALL SELECT 'pay_product_parts', CAST(COUNT(DISTINCT dt) AS STRING) FROM dwd_pay_product;
EOF
逐张执行:
bash
cd /home/hadoop/b02
hive -S -f sql/04a_verify_core.hql 2>/dev/null > output/b02_verify_a.txt
hive -S -f sql/04b_verify_behavior.hql 2>/dev/null > output/b02_verify_b.txt
hive -S -f sql/04c_verify_dim.hql 2>/dev/null > output/b02_verify_c.txt
hive -S -f sql/04d_verify_subtables.hql 2>/dev/null > output/b02_verify_d.txt
cat output/b02_verify_a.txt output/b02_verify_b.txt output/b02_verify_c.txt output/b02_verify_d.txt \
| sed 's/\t/,/g' > output/b02_verify_result.csv
cat output/b02_verify_result.csv
每个脚本 5~8 个 UNION ALL,编译负担降为原来的 1/6,几秒就完。
5.5 脚本 05 卡:{ } 组合命令
症状 :{ echo ...; hive ...; } > file 粘贴后卡在 > 提示符。
根因:
- 交互式 bash 对多行
{ }和hive -e "多行 SQL"的引号闭合处理不可靠。 - 粘贴时换行符可能被吞 ,
{不闭合 → 永远等下去。
判断:
- 提示符是
>或b02$后无新输出?------见 §5.1 先查进程。 - 进程不存在,但 shell 停在
>?------ 真卡在 PS2 续行,Ctrl+C退出。
修正 :多行逻辑一律写文件执行。
- 多行 SQL →
.hql文件 +hive -f - 多步 shell →
.sh文件 +bash file.sh - 避免
{ }+ 管道组合
最稳的替代:逐行 echo
bash
cd /home/hadoop/b02
echo "=== B02 Quality Report ===" > output/b02_quality_report.csv
echo "record_time,$(date '+%Y-%m-%d %H:%M:%S')" >> output/b02_quality_report.csv
echo "hostname,$(hostname)" >> output/b02_quality_report.csv
echo "stage,B02" >> output/b02_quality_report.csv
echo "source,B01 dwd_user_behavior_orc" >> output/b02_quality_report.csv
echo "upstream_baseline,output/b02_baseline.txt" >> output/b02_quality_report.csv
echo "" >> output/b02_quality_report.csv
cat output/b02_verify_result.csv >> output/b02_quality_report.csv
echo "" >> output/b02_quality_report.csv
echo "dt_0727_anomaly,suspected_truncation" >> output/b02_quality_report.csv
cat output/b02_quality_report.csv
每条命令都是单行,不会卡。
核心经验:
交互式终端不适合处理多行结构。
"一行做一件事"比"一段做所有事"更可靠。
5.6 为什么不改 hive-env.sh 的堆
一个自然的想法:把 Hive CLI 的 -Xmx256m 改成 -Xmx1024m,04 就不用拆了。
为什么 B02 最终没这么做?
2GB 小集群,master 节点上跑着:
- NameNode
- ResourceManager
- SecondaryNameNode
- Hive CLI(临时)
给 Hive CLI 加 1GB 堆,会:
- 挤压 master 上其他常驻进程的内存。
- Hive CLI 跑的时候,YARN 若同时在跑容器,可能触发 master 的 OOM killer。
- 影响集群稳定性------为了省一点脚本拆分,冒集群风险,不划算。
正确取舍:
不改堆,拆脚本。
用工程手段(拆小脚本)解决工程问题,不用配置手段(改全局参数)去适应小集群的物理限制。
这也是 B02 的一条设计原则:
在资源受限环境里,优先降低单次任务的复杂度,而不是给单个任务加资源。
6. 问题与修正
B02 执行中踩了 5 个坑。每个坑都对应一条可复用的经验。
6.1 本地模式 OOM
现象:
text
java.lang.OutOfMemoryError: Java heap space
at org.apache.hadoop.mapred.MapTask$MapOutputBuffer.init(MapTask.java:1001)
FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask
根因:
- 数据 <
local.auto.inputbytes.max(128MB),Hive 自动切本地模式。 - 本地模式下,MR 跑在 Hive CLI 的 256MB 堆里。
SET mapreduce.task.io.sort.mb=128要求 128MB 缓冲,直接 OOM。mapreduce.map.memory.mb=512和-Xmx400m在本地模式下被忽略。
修正:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
验证:修正后 62 秒成功。
教训:
本地模式的资源参数不生效。
数据量在
local.auto.inputbytes.max边界附近时,宁可不走本地,也不要凭感觉设大缓冲区。
6.2 交互式终端 PS2 卡死
现象:
text
hadoop@master:~/b02$ {
> echo "=== B02 Quality Report ==="
> echo "record_time,...
> ...
终端停在 >,永远不结束。
根因 :bash 对多行 { } 和 hive -e "多行 SQL" 的粘贴处理不可靠。
修正:
- 多行 SQL 一律写
.hql+hive -f - 多步 shell 一律写
.sh+bash file.sh - 避免
{ }+ 管道,改逐行echo >>
验证 :gen_b02_report.sh 脚本文件方式执行成功。
教训:
交互式终端 = 单行命令。
多行结构 = 文件执行。
6.3 Hive 日志显示 dt=null
现象 :02 执行成功后,日志显示:
text
Loading data to table b_group.dwd_user_behavior partition (dt=null)
看起来 :像所有数据都灌到了一个 dt=NULL 的默认分区。
真相 :Hive 3.1.3 的日志显示 bug 。动态分区多分区写入时,会显示 dt=null,但实际分区正确。
验证方法(两重保险):
- Hive SQL 侧:
sql
SELECT dt, COUNT(*) FROM dwd_user_behavior GROUP BY dt ORDER BY dt;
-- 返回 11 行,各分区正确
- HDFS 侧:
bash
hdfs dfs -ls /user/hive/warehouse/b_group.db/dwd_user_behavior/
# 看到 11 个 dt=2019-07-xx 目录,无 __HIVE_DEFAULT_PARTITION__
教训:
不要相信 Hive 日志的"文字",要看真实的数据分布。
SQL 查一遍 + HDFS 看一眼,双重确认。
6.4 交叉核对 Ambiguous column
现象:
sql
SELECT 'order_category' AS t, b.cnt AS b01, a.cnt AS b02, ...
FROM (SELECT COUNT(*) AS cnt FROM dwd_order_category) a
CROSS JOIN (SELECT COUNT(*) AS cnt FROM dwd_order_category_b01) b
报:
text
FAILED: SemanticException [Error 10007]: Ambiguous column reference cnt
根因 :两个子查询都用 cnt 作列名,Hive 在 UNION ALL + CROSS JOIN 组合下丢失别名解析。
修正 :给两个子查询列取不同名:
sql
SELECT 'order_category' AS t, b.b01_cnt, a.b02_cnt, ...
FROM (SELECT COUNT(*) AS b02_cnt FROM dwd_order_category) a
CROSS JOIN (SELECT COUNT(*) AS b01_cnt FROM dwd_order_category_b01) b
验证:4 行全 OK。
教训:
多子查询 JOIN 时,列名要唯一。
不要依赖
a.cnt/b.cnt消歧------Hive 未必认。
6.5 04 脚本编译慢
现象 :04 执行后长时间无输出,但 Hive 进程在跑。
根因 :24 个 UNION ALL 含 4 个嵌套子查询,Hive CLI 256MB 堆编译吃力。
修正:
- 短期 :拆成 4 个脚本
04a/04b/04c/04d。 - 长期 :不改
hive-env.sh,因为 2GB 小集群不宜给 Hive CLI 加堆(详见 §5.6)。
教训:
多 UNION ALL 的验证脚本,编译开销 ≠ 数据规模。
拆分是降低编译复杂度的最直接手段。
7. 验证与守恒
7.1 主表守恒
text
dwd_user_behavior = 180570 行
dwd_user_behavior_orc = 180570 行(B01 基线)
守恒式:
text
B01 dwd_user_behavior_orc = B02 dwd_user_behavior
180570 = 180570
7.2 seq 不变量
text
seq_dup = 0 ← 每 session 内 seq 无重复
seq_not_start_1 = 0 ← 每 session seq 从 1 开始
抽样验证(某 session 前 20 行):
text
session_id seq action_time raw_line_id page_id
329b966c-d61b-46ad-949a-7e37142d384a 1 2019-07-22 07:54:50 9574926 35
329b966c-d61b-46ad-949a-7e37142d384a 2 2019-07-22 07:54:56 9575033 3
329b966c-d61b-46ad-949a-7e37142d384a 3 2019-07-22 07:54:59 9575138 31
329b966c-d61b-46ad-949a-7e37142d384a 4 2019-07-22 07:55:03 9575244 22
329b966c-d61b-46ad-949a-7e37142d384a 5 2019-07-22 07:55:05 9575355 11
...
seq 连续递增,action_time 单调不减,raw_line_id 同秒内稳定。
7.3 ts 不变量
text
ts_null = 0 ← action_time 全部可解析为 timestamp
7.4 子表守恒
| 子表 | B01 行数 | B02 行数 | 一致 |
|---|---|---|---|
| dwd_order_category | 35188 | 35188 | ✅ |
| dwd_order_product | 23597 | 23597 | ✅ |
| dwd_pay_category | 24147 | 24147 | ✅ |
| dwd_pay_product | 16096 | 16096 | ✅ |
四张子表分区数均为 11,与主表对齐。
7.5 与 B01 对照
| 类别 | 指标 | B01 | B02 | 一致 |
|---|---|---|---|---|
| 主表 | 总行数 | 180570 | 180570 | ✅ |
| 分区数 | --- | 11 | --- | |
| 行为 | search | 40232 | 40232 | ✅ |
| click | 120519 | 120519 | ✅ | |
| order | 11768 | 11768 | ✅ | |
| pay | 8051 | 8051 | ✅ | |
| 维度 | user | 100 | 100 | ✅ |
| session | 9457 | 9457 | ✅ | |
| city | 26 | 26 | ✅ | |
| page | 50 | 50 | ✅ | |
| 日期 | dt_min | 2019-07-17 | 2019-07-17 | ✅ |
| dt_max | 2019-07-27 | 2019-07-27 | ✅ | |
| 07-27 行数 | 13800 | 13800 | ✅ | |
| 子表 | order_category | 35188 | 35188 | ✅ |
| order_product | 23597 | 23597 | ✅ | |
| pay_category | 24147 | 24147 | ✅ | |
| pay_product | 16096 | 16096 | ✅ |
B02 与 B01 的所有可对照指标,完全一致。
8. B02 对下游的贡献(核心章节)
这一章是 B02 存在的理由证明 。B02 的价值不在"建了几张表",而在为下游省掉了什么。
| 下游项目 | B02 提供了什么 | 不用 B02 会怎样 |
|---|---|---|
| B03 漏斗 | dt 分区直接按天聚合 |
全表扫,慢 10 倍 |
| B04 品类 Top10 | 子表带 dt、ts,直接按天分组 |
每次 join 主表取 dt |
| B05 品类 Top Session | seq_in_session 定序 |
自己写窗口函数,口径分叉 |
| B06 页面单跳 | 直接用 seq_in_session 做 LAG |
每次自己 ROW_NUMBER |
| B07 Session 序列 | 同上,且 ts 直接算时长 |
每次 unix_timestamp |
| B08 搜索分析 | dt 分区,直接按天看搜索趋势 |
全表扫 |
| B09 城市偏好 | 子表带 city_id、dt |
每次 join 主表 |
| B10 品类共现 | 两张 category 子表都带 dt、ts |
按天共现要 join |
| B11 RFM | ts 定 R 时间 |
每次 unix_timestamp |
| B12 异常检测 | ts 秒级间隔 + seq 页序 |
无法做时间序列异常 |
| B13 ADS | 分区主表可直接按天产出 ADS | 每次全表 |
| B15 复盘 | 24 项不变量已一次性验证 | 只能翻聊天记录 |
- 小结 :B02 把 10 个下游项目的公共需求,一次性前置满足。
9. 工程收获
9.1 DDL 与 DML 分离
CREATE TABLE 秒级,INSERT 分钟级。
先立结构,再灌数据 ,是数仓通用做法。列名、类型、分区写错,随时 DROP 重来,成本极低。
9.2 备份是"敢改"的前提
ALTER TABLE ... RENAME 是元数据操作,0 成本。
但心理上,知道随时能回去,改起来才敢下手。
9.3 本地模式是资源参数的"黑洞"
hive.exec.mode.local.auto=true 时:
mapreduce.map.memory.mb被忽略mapreduce.map.java.opts被忽略- 只有 Hive CLI 自己的堆生效
小集群上,宁可不走本地,也不要设大缓冲区。
9.4 交互式终端 = 单行命令
多行 SQL、多步 shell,一律写文件。
.hql+hive -f.sh+bash file.sh- 避免
{ }+ 管道
9.5 不动全局配置,优先拆脚本
2GB 小集群,hive-env.sh 的堆不能随便加,会挤压 master 上其他进程。
降低单次任务的复杂度,比给单次任务加资源更可靠。
9.6 多重验证,不信单一信息源
Hive 日志说 dt=null,但 SQL 查出来 11 个分区,HDFS 看到 11 个目录。
日志、SQL、文件系统------三重对照,才能确认事实。
10. 方法收获
10.1 每一步先问"不做谁会痛"
B02 的每个脚本,开头第一句都回答:
如果不做这一步,谁会痛?
这句话迫使设计者从下游需求出发,而不是从技术出发。
- 脚本 00 备份:不做,改错没退路。
- 脚本 01 建表:不做,DDL 写错要重跑整个 MR。
- 脚本 02 灌数据:不做,主表空。
- 脚本 03 重建子表:不做,下游每次 join 主表。
- 脚本 04 验证:不做,没人证明 B02 是对的。
- 脚本 05 报告:不做,复盘翻聊天记录。
每一条都能追到一个具体的下游痛点。
10.2 数据准备不是"态度问题",是"架构判断问题"
B02 的价值不在"认真",而在判断:
- 公共计算前置(seq、ts)
- 下游零重复(子表带 dt)
- 可回退、可验证(备份、验证脚本)
这三条,是数据准备工作的真正落点。
10.3 小步、可验证、可回退
B02 没有一个大 SQL 跑完所有事。
每一步只做一件事,每步都能独立验证,每步失败都有回退路径。
这是数据工程的通用工程实践。
10.4 资源受限环境要"降复杂度",不是"加资源"
2GB 小集群上:
- 不是给 Hive CLI 加堆
- 而是拆脚本,降低单次编译复杂度
- 不是加大 map 缓冲区
- 而是走 YARN,让资源参数生效
用工程手段适应物理限制,比用配置手段硬撑更可靠。
11. 遗留问题
11.1 07-27 疑截断
B01 已发现,B02 保留并标注。跨天趋势分析时,需排除或标注 07-27。
11.2 跨天 session 的 seq 语义
前提:仅在降级方案(逐天静态分区插入)时存在。
- 动态分区插入:
ROW_NUMBER()全局计算,跨天 session 的 seq 连续。 - 静态分区插入:
ROW_NUMBER()分区内计算,跨天 session 的 seq 按天重排。
B02 最终用动态分区模式,无此问题。
11.3 B03 入口已就绪
B03 及后续所有项目,统一查询入口:
sql
FROM b_group.dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26' -- 建议排除 07-27
序列分析统一:
sql
ORDER BY session_id, seq_in_session
时间间隔统一:
sql
UNIX_TIMESTAMP(ts) - UNIX_TIMESTAMP(LAG(ts) OVER (PARTITION BY session_id ORDER BY seq_in_session))
12. 结语
B02 是 B 组实验的第二阶段。它的任务不是"再造一张 DWD",而是把 B01 的"能用"升级成"好用"。
四个关键设计决策,每个都从一个具体的下游痛点出发:
- dt 分区:让 11 天数据不再全表扫。
- ts timestamp:让时间差分析不再各写各的。
- seq_in_session:让序列分析不再口径分叉。
- 子表补 ts、dt:让子表按天聚合不再 join。
执行过程中踩了 5 个坑,每个坑都对应一次修正:
- 本地模式 OOM → 走 YARN,缓冲减半。
- 交互式 PS2 卡死 → 多行写文件。
- Hive 日志显示 dt=null → SQL + HDFS 双查。
- 交叉核对 Ambiguous column → 列名唯一。
- 04 脚本编译慢 → 拆 4 个脚本,不改全局堆。
B02 真正的价值,不是"多建了 4 张表",而是:
把 B03--B12 的重复劳动前置消掉,把口径统一到唯一来源。
B01 给了 B 组"一套已被验证的数据认知"。
B02 把这套认知固化成结构。
B03 开始,所有下游分析都建立在这套结构之上。
附录 A:B02 交付清单
text
Hive 侧
├── 主表(新)
│ └── b_group.dwd_user_behavior 分区 ORC,180570 行,11 分区,含 ts + seq
├── 子表(新)
│ ├── b_group.dwd_order_category 分区 ORC,35188 行,含 ts
│ ├── b_group.dwd_order_product 分区 ORC,23597 行,含 ts
│ ├── b_group.dwd_pay_category 分区 ORC,24147 行,含 ts
│ └── b_group.dwd_pay_product 分区 ORC,16096 行,含 ts
├── 备份(B01 回退)
│ ├── b_group.dwd_order_category_b01
│ ├── b_group.dwd_order_product_b01
│ ├── b_group.dwd_pay_category_b01
│ └── b_group.dwd_pay_product_b01
└── B01 冻结主表(保留回退)
└── b_group.dwd_user_behavior_orc 非分区 ORC,180570 行
文件侧
/home/hadoop/b02/
├── sql/
│ ├── 00_rename_b01_subtables.hql
│ ├── 01_create_dwd_partitioned.hql
│ ├── 02_insert_dwd_partitioned.hql
│ ├── 03_rebuild_subtables_partitioned.hql
│ └── 04_verify_b02.hql
├── output/
│ ├── b02_baseline.txt ← B01 基线(9 项)
│ ├── b02_quality_report.csv ← B02 质量报告(头部 + 24 项 + 标注)
│ ├── b02_verify_result.csv ← 04 原始输出
│ ├── b02_00_rename.log
│ ├── b02_01_create.log
│ ├── b02_02_insert.log ← 含 OOM 与修复记录
│ ├── b02_03_subtables.log
│ └── b02_progress.txt
└── docs/
└── report_template.md ← B02--B15 报告模板
附录 B:脚本 04 完整验证结果
text
main_total,180570
partition_cnt,11
seq_dup,0
seq_not_start_1,0
ts_null,0
behavior_click,120519
behavior_order,11768
behavior_pay,8051
behavior_search,40232
distinct_user,100
distinct_session,9457
distinct_city,26
distinct_page,50
dt_min,2019-07-17
dt_max,2019-07-27
order_category_rows,35188
order_product_rows,23597
pay_category_rows,24147
pay_product_rows,16096
order_category_parts,11
order_product_parts,11
pay_category_parts,11
pay_product_parts,11
dt_0727_rows,13800
24 项全部与 B01 对照一致。
附录 C:执行环境
| 项 | 版本 |
|---|---|
| Hadoop | 3.3.6 |
| Hive | 3.1.3(on MR) |
| 集群 | 1 master + 3 worker,每节点 2GB |
| Java | 8 |
| Hive CLI 堆 | 默认 256MB(未修改,见 §5.6) |
| 运行日期 | 2026-10-01 |
本文是 B02 阶段完整复盘,也是 B03--B15 的消费层说明。
B01 是"数据认知",B02 是"结构固化"。两者共同构成 B 组的数据底座。