你遇到的“经典问题”变体:这是一个“模板迁移式课程设计”问题——给定一个样例(NumPy 的 Exercism 风格分步练习),要求在同一体例下为新领域(Hive / 大数据技术栈)构建练习驱动课程,包含小步迭代、代码框架与可验证结果。
---
# Hive 与大数据技术栈入门(Exercism 风格 · 约 10 小时)
> 面向已有 SQL 基础的学习者。每个步骤包含理论介绍 → 编程任务 → 验证机制 → 参考代码。
> 为便于自测,文中提供小型数据集与期望结果(多为行数或取前若干行的值)。
> 建议运行环境:Apache Hive 3.x,Hadoop 3.x;执行引擎可使用 MR/Tez/Spark 任一(默认 MR/Tez 皆可)。客户端建议 `beeline`。
## 目录
0. 练习准备:数据与目录
1. [[HDFS]] 基础:放数与查看
2. 数据库与表:Managed vs External
3. 分区表:静态/动态分区与元数据修复
4. 查询基础:筛选、排序、限制
5. 聚合与去重:按月营收与城市排行
6. 窗口函数:找出用户最近一单与生命周期消费
7. 连接与半连接:数据对齐、反连接挖“脏数据”
8. 半结构化处理:JSON 提取与展开
9. 存储格式与压缩:Text vs ORC/Parquet、统计信息
10. 分桶与采样、EXPLAIN:性能与可解释性
—附录:大数据技术栈鸟瞰与延伸练习
---
## Step 0(准备 · 30min)—练习目录与数据
目标:在本地创建小样本数据并放到 HDFS 目录,后续步骤复用。
### 0.1 建议目录
```bash
# 本地(存放样本文件)
mkdir -p ~/ex_hive_data/{users,orders,events,events_json}
# HDFS(练习区)
hdfs dfs -mkdir -p /user/$USER/ex_hive/raw/{users,orders,events,events_json}
```
### 0.2 写入本地样本文件
users.csv(8 行)→ `~/ex_hive_data/users/users.csv`
```Java
1,Alice,Beijing,2023-12-05
2,Bob,Shanghai,2023-12-10
3,Carol,Beijing,2024-01-02
4,David,Shenzhen,2024-02-14
5,Eva,Chengdu,2024-03-01
6,Frank,Beijing,2024-03-15
7,Grace,Shanghai,2024-04-09
8,Heidi,Beijing,2024-05-20
```
orders.csv(12 行)→ `~/ex_hive_data/orders/orders.csv`
```Java
1001,1,2024-01-03 10:23:00,paid,120.50
1002,2,2024-01-15 17:05:10,cancelled,0.00
1003,3,2024-02-08 09:10:22,paid,310.00
1004,1,2024-03-12 12:01:05,paid,89.90
1005,4,2024-03-20 19:22:31,paid,450.00
1006,5,2024-04-05 08:00:00,paid,39.00
1007,6,2024-04-18 21:15:45,refunded,99.00
1008,2,2024-05-01 11:11:11,paid,560.00
1009,7,2024-05-02 14:55:00,paid,230.00
1010,8,2024-05-20 07:30:30,paid,120.00
1011,3,2024-06-03 13:00:00,paid,45.00
1012,1,2024-06-30 20:20:20,paid,300.00
```
events(按分区日期存放的 tsv)
将下列内容分别保存为:
`~/ex_hive_data/events/dt=2024-05-01/events.tsv`
```Java
2024-05-01 10:00:00 2 /home 2024-05-01
2024-05-01 10:05:00 2 /product/sku_123 2024-05-01
2024-05-01 10:07:20 2 /cart 2024-05-01
2024-05-01 11:11:11 2 /checkout 2024-05-01
2024-05-01 12:00:00 9 /home 2024-05-01
```
`~/ex_hive_data/events/dt=2024-05-02/events.tsv`
```Java
2024-05-02 09:00:00 7 /home 2024-05-02
2024-05-02 09:10:00 7 /search?q=shoes 2024-05-02
2024-05-02 09:12:00 7 /product/sku_999 2024-05-02
```
`~/ex_hive_data/events/dt=2024-05-20/events.tsv`
```Java
2024-05-20 07:29:50 8 /home 2024-05-20
```
`~/ex_hive_data/events/dt=2024-06-03/events.tsv`
```Java
2024-06-03 12:45:00 3 /home 2024-06-03
```
`~/ex_hive_data/events/dt=2024-06-30/events.tsv`
```Java
2024-06-30 20:05:00 1 /home 2024-06-30
```
events\_json(行式 JSON)→ `~/ex_hive_data/events_json/events.json`
```Java
{"ts":"2024-05-01T10:05:00","uid":2,"event":"view","props":{"sku":"sku_123","price":560.0}}
{"ts":"2024-05-02T09:10:00","uid":7,"event":"search","props":{"q":"shoes"}}
{"ts":"2024-06-03T13:00:00","uid":3,"event":"purchase","props":{"order_id":1011,"amount":45.0}}
```
### 0.3 上传到 HDFS
```bash
hdfs dfs -put -f ~/ex_hive_data/users/users.csv /user/$USER/ex_hive/raw/users/
hdfs dfs -put -f ~/ex_hive_data/orders/orders.csv /user/$USER/ex_hive/raw/orders/
hdfs dfs -put -f ~/ex_hive_data/events/dt=2024-05-01 /user/$USER/ex_hive/raw/events/
hdfs dfs -put -f ~/ex_hive_data/events/dt=2024-05-02 /user/$USER/ex_hive/raw/events/
hdfs dfs -put -f ~/ex_hive_data/events/dt=2024-05-20 /user/$USER/ex_hive/raw/events/
hdfs dfs -put -f ~/ex_hive_data/events/dt=2024-06-03 /user/$USER/ex_hive/raw/events/
hdfs dfs -put -f ~/ex_hive_data/events/dt=2024-06-30 /user/$USER/ex_hive/raw/events/
hdfs dfs -put -f ~/ex_hive_data/events_json/events.json /user/$USER/ex_hive/raw/events_json/
```
---
## Step 1(30min)—HDFS 基础:放数与查看
理论:学会 `-ls/-cat/-du` 等操作,理解路径与权限。
任务
1. 列出练习根目录与子目录。
2. 查看 `orders.csv` 前几行。
3. 统计 `events` 目录下的总文件大小。
验证
`hdfs dfs -ls -R /user/$USER/ex_hive/raw | wc -l` 行数应 $\ge 8$(各目录与文件)。
`hdfs dfs -cat /user/$USER/ex_hive/raw/orders/orders.csv | head -3` 能看到 3 行订单。
`hdfs dfs -du -h /user/$USER/ex_hive/raw/events` 能显示每个分区目录大小(非空)。
参考命令
```bash
hdfs dfs -ls -R /user/$USER/ex_hive/raw
hdfs dfs -cat /user/$USER/ex_hive/raw/orders/orders.csv | head -3
hdfs dfs -du -h /user/$USER/ex_hive/raw/events
```
---
## Step 2(45min)—数据库与表:Managed vs External
理论
Managed 表:数据受 Hive 管理,删表会删数据。
External 表:仅管理元数据,删表不删 HDFS 中的数据。
行存储 Text,字段分隔符用 `FIELDS TERMINATED BY`。
任务
1. 建库并切换:`ex_hive`。
2. 建 managed 表 `users_text`(TextFile)。
3. 建 external 表 `orders_ext` 指向 HDFS 路径。
参考代码(HQL)
```sql
CREATE DATABASE IF NOT EXISTS ex_hive;
USE ex_hive;
-- Managed: users
DROP TABLE IF EXISTS users_text;
CREATE TABLE users_text(
user_id INT,
name STRING,
city STRING,
signup_dt DATE
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE;
LOAD DATA INPATH '/user/${hiveconf:user}/ex_hive/raw/users/users.csv'
INTO TABLE users_text;
-- External: orders
DROP TABLE IF EXISTS orders_ext;
CREATE EXTERNAL TABLE orders_ext(
order_id INT,
user_id INT,
order_ts TIMESTAMP,
status STRING,
total_amount DECIMAL(10,2)
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE
LOCATION '/user/${hiveconf:user}/ex_hive/raw/orders';
```
> 运行 beeline 前可带 `--hiveconf user=$USER`,或在会话里执行:`SET user=$USER;`
验证
```sql
SELECT COUNT() FROM users_text; -- 期望:8
SELECT COUNT() FROM orders_ext; -- 期望:12
DESCRIBE FORMATTED orders_ext; -- 期望:Table Type: EXTERNAL_TABLE
```
---
## Step 3(45min)—分区表与元数据修复
理论
分区能让查询裁剪特定目录,减少扫描量。
使用 `MSCK REPAIR TABLE` 扫描分区目录并修复元数据。
任务
1. 基于 events 按 `dt` 分区建 external 表 `events_log`。
2. 使用 `MSCK REPAIR TABLE` 自动加载已有分区。
3. 查看分区与总行数。
参考代码
```sql
USE ex_hive;
DROP TABLE IF EXISTS events_log;
CREATE EXTERNAL TABLE events_log(
ts STRING,
user_id INT,
url STRING
)
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE
LOCATION '/user/${hiveconf:user}/ex_hive/raw/events';
MSCK REPAIR TABLE events_log; -- 扫描 dt=... 目录并登记分区
```
验证
```sql
SHOW PARTITIONS events_log; -- 期望有 5 个分区:2024-05-01, 2024-05-02, 2024-05-20, 2024-06-03, 2024-06-30
SELECT COUNT() FROM events_log; -- 期望:11
SELECT COUNT() FROM events_log WHERE dt='2024-05-01'; -- 期望:5
```
---
## Step 4(45min)—查询基础:WHERE / ORDER BY / LIMIT
理论:SARGable 条件、隐式转换与时间函数(此处 TS 已为 TIMESTAMP)。
任务
1. 统计 已支付 订单条数与总金额。
2. 取金额最高的 Top-3 已支付订单(展示 `order_id,total_amount`)。
3. 统计 `users_text` 中不同城市数。
参考代码
```sql
USE ex_hive;
-- 1) 已支付
SELECT COUNT() AS paid_cnt,
SUM(total_amount) AS paid_sum
FROM orders_ext
WHERE status='paid';
-- 2) Top-3 paid
SELECT order_id, total_amount
FROM orders_ext
WHERE status='paid'
ORDER BY total_amount DESC
LIMIT 3;
-- 3) 城市去重
SELECT COUNT(DISTINCT city) AS city_cnt
FROM users_text;
```
验证(期望)
`paid_cnt = 10`,`paid_sum = 2264.40`
Top-3 `total_amount`:`560.00, 450.00, 310.00`(对应 `1008,1005,1003`)
`city_cnt = 4`
---
## Step 5(60min)—聚合与去重:按月营收与城市排行
理论:`GROUP BY`、`DATE_FORMAT`/`from_unixtime`、`HAVING`。
任务
1. 统计 2024-01 \~ 2024-06 各月已支付营收(`yyyy-MM`)。
2. 计算各城市(按用户归属)已支付营收并排序(降序)。
参考代码
```sql
-- 1) 月营收
WITH paid AS (
SELECT order_id, user_id, order_ts, total_amount
FROM orders_ext
WHERE status='paid'
)
SELECT DATE_FORMAT(order_ts,'yyyy-MM') AS ym,
ROUND(SUM(total_amount),2) AS revenue
FROM paid
GROUP BY DATE_FORMAT(order_ts,'yyyy-MM')
ORDER BY ym;
-- 2) 城市营收
SELECT u.city,
ROUND(SUM(o.total_amount),2) AS revenue
FROM orders_ext o
JOIN users_text u ON o.user_id=u.user_id
WHERE o.status='paid'
GROUP BY u.city
ORDER BY revenue DESC, u.city;
```
验证(期望)
月营收(ym → revenue)
`2024-01 → 120.50`,`2024-02 → 310.00`,`2024-03 → 539.90`,`2024-04 → 39.00`,`2024-05 → 910.00`,`2024-06 → 345.00`
城市营收(降序)
`Beijing → 985.40`,`Shanghai → 790.00`,`Shenzhen → 450.00`,`Chengdu → 39.00`
---
## Step 6(60min)—窗口函数:最近一单与生命周期消费
理论
`ROW_NUMBER()/RANK()`:排序内分组。
`FIRST_VALUE()/LAST_VALUE()` 与累积 `SUM(...) OVER (PARTITION BY...)`。
任务
1. 为每个用户找出最近一单(`last_order_ts,last_status,last_amount`)。
2. 统计每个用户生命周期已支付总额(含退款不计入)。
3. 计算“最近一单状态为 paid”的用户数。
参考代码
```sql
WITH ord AS (
SELECT o.,
CASE WHEN status='paid' THEN total_amount ELSE 0 END AS paid_amt
FROM orders_ext o
),
ranked AS (
SELECT user_id, order_id, order_ts, status, total_amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_ts DESC) AS rn,
SUM(paid_amt) OVER (PARTITION BY user_id) AS lifetime_paid
FROM ord
)
SELECT user_id,
MAX(CASE WHEN rn=1 THEN order_ts END) AS last_order_ts,
MAX(CASE WHEN rn=1 THEN status END) AS last_status,
MAX(CASE WHEN rn=1 THEN total_amount END) AS last_amount,
MAX(lifetime_paid) AS lifetime_paid
FROM ranked
GROUP BY user_id
ORDER BY user_id;
```
验证(期望)
“最近一单状态为 paid”的用户数 $= 7$(仅 `user_id=6` 最近一单为 `refunded`)。
---
## Step 7(45min)—连接与半连接:数据对齐与脏数据定位
理论
`LEFT/RIGHT/INNER JOIN` 与 `LEFT SEMI/ANTI`。
通过反连接查“游离记录”。
任务
1. 检查订单中是否存在无用户档案的记录(订单表的 user\_id 不在用户表)。
2. 检查访问日志中是否存在未建档用户(events 有 user\_id,但用户表无此人)。
参考代码与验证
```sql
-- 1) 订单是否有“孤儿用户”(期望:0)
SELECT COUNT() AS orphan_orders
FROM orders_ext o
LEFT ANTI JOIN users_text u
ON o.user_id = u.user_id;
-- 2) 访问日志中的“未知用户”(期望:1个 user_id=9)
SELECT DISTINCT e.user_id
FROM events_log e
LEFT ANTI JOIN users_text u
ON e.user_id = u.user_id
ORDER BY e.user_id;
```
期望结果:`orphan_orders = 0`;未知用户列表仅 9。
---
## Step 8(60min)—半结构化处理:JSON 提取与展开
理论
使用 `get_json_object`/`json_tuple` 从 JSON 字符串提取字段。
`LATERAL VIEW` 配合 `explode/json_tuple` 进行列展开。
任务
1. 创建原始 JSON 行表并读取。
2. 解析 `ts, uid, event` 与 `props` 中的键值。
3. 统计 `event='purchase'` 的条数与对应 `order_id, amount`。
参考代码
```sql
USE ex_hive;
DROP TABLE IF EXISTS events_json_raw;
CREATE EXTERNAL TABLE events_json_raw(line STRING)
LOCATION '/user/${hiveconf:user}/ex_hive/raw/events_json';
-- 解析(使用 get_json_object)
WITH parsed AS (
SELECT
get_json_object(line,'$.ts') AS ts,
CAST(get_json_object(line,'$.uid') AS INT) AS uid,
get_json_object(line,'$.event') AS event,
get_json_object(line,'$.props.order_id') AS order_id,
get_json_object(line,'$.props.amount') AS amount,
get_json_object(line,'$.props.q') AS q,
get_json_object(line,'$.props.sku') AS sku
FROM events_json_raw
)
SELECT FROM parsed;
-- 统计 purchase
SELECT COUNT() AS purchase_cnt
FROM (
SELECT
FROM events_json_raw
) t
WHERE get_json_object(line,'$.event')='purchase';
-- 查看 purchase 明细(期望 order_id=1011, amount=45.0)
SELECT
get_json_object(line,'$.uid') AS uid,
get_json_object(line,'$.props.order_id') AS order_id,
get_json_object(line,'$.props.amount') AS amount
FROM events_json_raw
WHERE get_json_object(line,'$.event')='purchase';
```
验证(期望)
`purchase_cnt = 1`;对应 `(uid=3, order_id=1011, amount=45.0)`。
---
## Step 9(60min)—存储格式与压缩:Text vs ORC/Parquet、统计信息
理论
列式存储(ORC/Parquet)在扫描少量列、聚合/压缩时通常优于 Text。
`ANALYZE TABLE... COMPUTE STATISTICS` 让优化器更聪明。
可调整压缩:`SET hive.exec.compress.output=true;`,ORC 常用 `SNAPPY`。
任务
1. 用 CTAS 生成 ORC 表 `orders_orc` 与 Parquet 表 `orders_parq`(二选一或都做)。
2. 收集表与列统计信息。
3. 对比 `DESCRIBE FORMATTED` 观察 InputFormat/SerDe。
参考代码
```sql
USE ex_hive;
SET hive.exec.compress.output=true;
SET orc.compress=SNAPPY;
DROP TABLE IF EXISTS orders_orc;
CREATE TABLE orders_orc
STORED AS ORC
AS
SELECT FROM orders_ext;
DROP TABLE IF EXISTS orders_parq;
CREATE TABLE orders_parq
STORED AS PARQUET
AS
SELECT FROM orders_ext;
-- 统计信息
ANALYZE TABLE orders_orc COMPUTE STATISTICS;
ANALYZE TABLE orders_orc COMPUTE STATISTICS FOR COLUMNS order_id, user_id, status;
DESCRIBE FORMATTED orders_orc; -- 期望看到 OrcInputFormat / OrcSerde
DESCRIBE FORMATTED orders_parq; -- 期望看到 ParquetInputFormat / ParquetHiveSerDe
```
验证(期望)
`SELECT COUNT() FROM orders_orc;` → 12
`DESCRIBE FORMATTED` 中的 InputFormat/SerDe 分别为 ORC/Parquet 对应实现。
---
## Step 10(75min)—分桶与采样、EXPLAIN:性能与可解释性
理论
分桶(Bucketing)在按桶键连接或抽样时更高效;`TABLESAMPLE (BUCKET x OUT OF y)`。
需要 `SET hive.enforce.bucketing=true;` 插入时按桶写出。
`EXPLAIN` 查看计划(Map Join / Bucket Map Join / 分区裁剪等)。
任务
1. 创建按 `user_id` 分 4 桶的 ORC 表 `orders_bkt_orc`,从 `orders_orc` 写入。
2. 用 `TABLESAMPLE` 抽取 $1/4$ 数据(近似)。
3. `EXPLAIN` 一条带 `WHERE status='paid'` 的查询,确认分区裁剪/谓词下推等优化(至少看到扫描的表与过滤条件)。
参考代码
```sql
USE ex_hive;
SET hive.enforce.bucketing=true;
DROP TABLE IF EXISTS orders_bkt_orc;
CREATE TABLE orders_bkt_orc(
order_id INT,
user_id INT,
order_ts TIMESTAMP,
status STRING,
total_amount DECIMAL(10,2)
)
CLUSTERED BY (user_id) INTO 4 BUCKETS
STORED AS ORC;
INSERT OVERWRITE TABLE orders_bkt_orc
SELECT FROM orders_orc;
-- 采样(1/4 桶)
SELECT COUNT() AS sample_cnt
FROM orders_bkt_orc TABLESAMPLE(BUCKET 1 OUT OF 4);
-- 计划分析
EXPLAIN
SELECT user_id, SUM(total_amount) AS rev
FROM orders_bkt_orc
WHERE status='paid'
GROUP BY user_id;
```
验证(期望)
`SELECT COUNT() FROM orders_bkt_orc;` → 12
`sample_cnt` $\approx$ 3(接近 $1/4$,总数小可能偏差)
`EXPLAIN` 输出中应包含对 `status='paid'` 的过滤说明(不同引擎/版本呈现略有差异,重点是能看到扫描对象与过滤条件)。
---
# 附录 A—大数据技术栈鸟瞰(把 Hive 放在地图上)
存储层:HDFS / 对象存储(S3/OSS)
资源调度:YARN / Kubernetes
执行引擎:MapReduce(基线)、Tez(Hive 常用)、Spark(Hive on Spark / Spark SQL)
SQL 引擎:Hive(批/ETL 强项)、Presto/Trino(交互式查询)、Impala(MPP)
数据格式:ORC/Parquet/Avro(列式首选)、Text/CSV(落地/对接方便)
元数据:Hive Metastore / AWS Glue
采集/对接:Kafka(实时)、Flume、Sqoop(关系库导入)
调度编排:Oozie / Airflow
治理与质量:Ranger(权限)、Atlas(血缘)、Deequ/Great Expectations(质量)
> 经验法则:ODS(原始层)→ DWD(明细层)→ DWS(汇总层)→ ADS(应用层)的分层建模,在 Hive 中常以External 表 + 分区/列式配合落地。
---
# 附录 B—延伸练习(选做)
1. 动态分区插入
```sql
SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
-- 构造 stage 表(含 dt 字段),然后动态插入到按 dt 分区表中
-- 这里可将 events_log 的数据转入一个新的分区模型表
```
2. ACID 表与事务(Hive 3.x):创建 `transactional=true` 的 ORC 表,尝试 `INSERT/UPDATE/DELETE`,并观察 compaction。
3. Map Join / Bucket Map Join:在小表侧启用 `SET hive.auto.convert.join=true;`,观察 `EXPLAIN` 的 join 策略变化。
4. 成本对比:同一查询在 Text/ORC/Parquet 上测试 `elapsed time`,理解列式优势。
---
## 提交与组织(仿 Exercism)
建议每一小节建一个目录,放入:
`instructions.md`(本节“理论/任务/验证”的摘录)
`starter.hql`(留有 `-- TODO:` 注释位置)
`verify.hql`(验证查询)
`README.md`(记录你运行的命令与观察)
---
### 小结
通过上述 10 个练习,你已完成从 HDFS 基操 → Hive 表管理 → 分区/聚合/窗口/连接 → 半结构化 JSON → 列式/统计/分桶/EXPLAIN 的闭环实践,并在大数据技术栈中定位了 Hive 的位置。
如果你愿意,我可以把这些步骤打包为一组文件骨架(目录 + `starter.hql/verify.hql` 模板 + `instructions.md`),方便你直接开始做题。