****你遇到的****“****经典问题****”****变体:****这是一个****“****模板迁移式课程设计****”****问题——给定一个样例(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`),方便你直接开始做题。