快速实验篇(B02)DWD 生产级升级

小肥柴的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 不做什么(划清边界)

明确边界,避免范围膨胀:

  1. 不重建 dwd_user_behavior_dirty:B01 实测真缺失 = 0,脏表为空,无重建价值。
  2. 不引入城市/品类维表:维表是 B09 的任务,B02 只做结构升级,不做语义补全。
  3. 不改子表字段语义 :保持 category_id / product_id 独立,不重新尝试配对(B01 已推翻配对假设)。
  4. 不动 07-27 数据:疑截断只是标注,不剔除、不修正。
  5. 不改 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 的输出)

设计原则:

  1. 每一步只做一件事:建表就是建表,灌数就是灌数。出错好定位。
  2. 每一步都有回退路径 :00 备份提供回退,02 失败只需 TRUNCATE 重跑。
  3. 后一步依赖前一步产出:形成链条,不会乱序。

为什么不是一个大 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 分区,同时:

  1. CAST(action_time AS TIMESTAMP) 生成 ts
  2. ROW_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

为什么这个脚本"最重"?

三个原因:

  1. 一次处理 180570 行,源表 18MB,写入含新字段后约 4.2MB。
  2. 窗口函数 + 动态分区,对 Hive CLI 和 YARN 都有压力。
  3. 首次执行失败:本地模式 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 堆,会:

  1. 挤压 master 上其他常驻进程的内存。
  2. Hive CLI 跑的时候,YARN 若同时在跑容器,可能触发 master 的 OOM killer。
  3. 影响集群稳定性------为了省一点脚本拆分,冒集群风险,不划算。

正确取舍:

不改堆,拆脚本。

用工程手段(拆小脚本)解决工程问题,不用配置手段(改全局参数)去适应小集群的物理限制。

这也是 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" 的粘贴处理不可靠。

修正:

  1. 多行 SQL 一律写 .hql + hive -f
  2. 多步 shell 一律写 .sh + bash file.sh
  3. 避免 { } + 管道,改逐行 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,但实际分区正确。

验证方法(两重保险):

  1. Hive SQL 侧:
sql 复制代码
SELECT dt, COUNT(*) FROM dwd_user_behavior GROUP BY dt ORDER BY dt;
-- 返回 11 行,各分区正确
  1. 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 组的数据底座。

相关推荐
阿狗童鞋1 小时前
Elasticsearch实战指南
大数据·elasticsearch·搜索引擎
老A的AI实验室1 小时前
赛博月刊 #2026年9月
大数据·人工智能·深度学习·ai·llm
卷毛迷你猪1 小时前
快速实验篇(B03) 用户行为漏斗与转化率
大数据·hive·hadoop
径硕科技JINGdigital2 小时前
出海企业想要借助统一平台接入国际主流基础模型,可选哪些云上生成式 AI 平台?
大数据·人工智能
Elastic 中国社区官方博客2 小时前
使用 Jev 作为 search reranker:基准测试与实现方法
大数据·运维·elasticsearch·全文检索
卷毛迷你猪3 小时前
快速实验篇(B01)用户行为日志画像与 DWD 标准化
大数据·hive·hadoop
四季豆333 小时前
中翰软件调整战略定位:以数据与知识“双治理”搭建企业AI落地桥梁
大数据
T06205143 小时前
【2026年更新】全国自然保护区矢量数据,列表+边界+功能区
大数据
ha_lydms4 小时前
MaxCompute中JSON函数
大数据·数据库·阿里云·json·dataworks·maxcompute·odps