Hive 的 SQL 练习,选什么数据集性价比最高?我练下来的感受是:影评数据几乎是最合适的选择。它自带用户、电影、评分、时间四个核心维度,天然适合练多表关联、聚合统计、窗口函数、日期处理这些高频操作。影评案例既是 Hive SQL 练习的经典入门题,也是很多数仓开发岗面试题的素材来源。这篇文章我会拆解一套完整的影评案例练习,从建表、数据准备、基础清洗到窗口函数和复杂业务场景,每一步都给出可直接运行的 Hive SQL,并把我踩过的坑一并写出来。适合正在学 Hive 的初学者,也适合准备转岗数仓开发的同学照着自测。
1. 项目背景与需求拆解
1.1 为什么选影评数据练手
我见过很多人练 SQL 喜欢用订单表、用户表,但那些数据总缺一点“业务感”。影评数据的妙处在于,它天生就是一个简单的星型模型:中间是评分事实表,两边是用户维度和电影维度。你既要会查明细,又得会按维度聚合,还得能把“用户连续评分”这种问题翻译成 SQL。
从技术点看,影评案例几乎覆盖了日常数仓开发的高频操作:
- 基础查询:过滤、排序、去重、空值处理
- 聚合统计:分组、均值、计数、双 GROUP BY
- 多表关联:事实表和维度表 JOIN
- 窗口函数:ROW_NUMBER、RANK、DENSE_RANK、LAG
- 行转列:split + explode + lateral view
- 日期处理:TO_DATE、DATE_FORMAT、DATE_SUB、DATEDIFF
- 业务分析:榜单、留存、连续行为、分层打标
更重要的是,影评数据结果容易验证。比如统计一部电影的平均评分,你手算一下就能知道 SQL 对不对。这种“即时反馈”对建立信心特别有帮助。我用这套案例带过不少新人,基本上从建表到连续评分那道题练完,Hive SQL 的常见语法就能打通一大半。
1.2 业务场景与需求清单
我模拟的业务背景是:某在线影视平台积累了一批用户对电影的评分记录,现在需要做一轮 UV 层面的评分行为分析。整个练习拆成 7 个需求,按难度递进:
| 编号 | 需求描述 | 核心考察点 |
|---|---|---|
| 1 | 统计每部电影的评分人数、平均分、最高分、最低分 | GROUP BY + 聚合函数 |
| 2 | 找出评分人数 Top 10 的电影 | 子查询 + ORDER BY + LIMIT |
| 3 | 统计每个用户的评分次数、平均分、活跃天数 | COUNT DISTINCT + TO_DATE |
| 4 | 给每个用户按评分次数分层打标 | CASE WHEN |
| 5 | 对每部电影按评分排名,列出评分最高的用户 | ROW_NUMBER / RANK |
| 6 | 计算每个用户相邻两次评分的时间间隔 | LAG + DATEDIFF |
| 7 | 判断每个用户是否存在连续 3 天评分行为 | DATE_SUB + ROW_NUMBER |
这些需求看起来简单,真写起来会踩不少语法坑。比如 Hive 里窗口函数和 GROUP BY 的执行顺序、LEFT JOIN 后 COUNT 字段的口径差异、日期字符串的格式统一,每一个都是新手容易翻车的地方。
1.3 数据表结构设计
为了把多表关联练透,我建议建三张表,而不是一张宽表。三张表分别是:
- 用户表:users
- 电影表:movies
- 评分明细表:ratings
字段设计如下:
users user_id 用户ID user_name 用户昵称 gender 性别 city 所在城市 register_date 注册日期 movies movie_id 电影ID movie_name 电影名称 genre 电影类型,多个类型用逗号分隔 release_year 上映年份 duration_min 时长(分钟) ratings user_id 用户ID movie_id 电影ID rating 评分,采用5分制,支持0.5分 rating_time 评分时间,格式 yyyy-MM-dd HH:mm:ss电影表里故意把genre设计成逗号分隔的多值字段,是为了练 lateral view 行转列。这个设计在生产环境里偶尔会遇到,影评数据用来练拆分比订单表自然得多。
2. 建表与数据准备
2.1 内部表还是外部表
很多人在练习时不太在意 Hive 表的类型,直接CREATE TABLE,默认建的就是内部表。内部表和外部表的区别在于:删除表时,内部表会连同 HDFS 上的数据一起删除,外部表只删除元数据,数据文件还在。
我建议在练习环境里也用外部表。原因很简单:如果有天你写DROP TABLE时手滑,内部表数据就没了,重新造数很麻烦。外部表的数据还在 HDFS 上,重新执行一次建表语句就能找回元数据。生产环境就更不用说了,数据资产通常都在数仓外部目录,必须用外部表管理。
不过,如果只是练语法,内部表也不是不能用。关键是知道两者的边界。下面的建表脚本我用外部表,并且把数据统一放在一个目录下。
2.2 建表语句实例
先建一个练习库,然后建三张表。为了保证在普通 Hive 环境能直接执行,这里用最基础的写法:
CREATE DATABASE IF NOT EXISTS movie_rating; USE movie_rating; CREATE EXTERNAL TABLE IF NOT EXISTS users ( user_id BIGINT COMMENT '用户ID', user_name STRING COMMENT '用户昵称', gender STRING COMMENT '性别', city STRING COMMENT '城市', register_date STRING COMMENT '注册日期,yyyy-MM-dd' ) COMMENT '用户信息表' ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/warehouse/movie_rating/users'; CREATE EXTERNAL TABLE IF NOT EXISTS movies ( movie_id BIGINT COMMENT '电影ID', movie_name STRING COMMENT '电影名称', genre STRING COMMENT '电影类型,逗号分隔', release_year INT COMMENT '上映年份', duration_min INT COMMENT '时长(分钟)' ) COMMENT '电影信息表' ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/warehouse/movie_rating/movies'; CREATE EXTERNAL TABLE IF NOT EXISTS ratings ( user_id BIGINT COMMENT '用户ID', movie_id BIGINT COMMENT '电影ID', rating DECIMAL(2,1) COMMENT '评分,5分制,支持0.5', rating_time STRING COMMENT '评分时间,yyyy-MM-dd HH:mm:ss' ) COMMENT '评分明细表' ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/warehouse/movie_rating/ratings';字段类型有两点值得注意。第一,rating用DECIMAL(2,1)而不是FLOAT,因为浮点计算容易出 4.9000001 这种结果,后面 AVG 和 ROUND 会很难看。第二,日期时间我用STRING存,因为 Hive 对复杂时间格式的 TO_DATE 解析能力很强,字符串反而灵活。只要保证格式统一,后续用TO_DATE(rating_time)就能取到日期部分。
2.3 准备模拟数据并导入
练习数据不需要太大,但要有“脏”数据。我当初造了 500 条评分记录,覆盖 20 部电影、30 个用户,故意在 ratings 里塞了一些重复记录和异常评分,就是为了后面能练去重和清洗。
数据文件用 CSV 格式,逗号分隔,注意不要有表头。分别放到本地目录后,通过 Hive 的 load 命令导入:
hive -e "LOAD DATA LOCAL INPATH '/tmp/data/users.csv' INTO TABLE movie_rating.users;" hive -e "LOAD DATA LOCAL INPATH '/tmp/data/movies.csv' INTO TABLE movie_rating.movies;" hive -e "LOAD DATA LOCAL INPATH '/tmp/data/ratings.csv' INTO TABLE movie_rating.ratings;"导入完成后,先做三件事:看总行数、看重复、看空值。
SELECT COUNT(*) FROM movie_rating.ratings; SELECT user_id, movie_id, COUNT(*) AS cnt FROM movie_rating.ratings GROUP BY user_id, movie_id HAVING COUNT(*) > 1; SELECT COUNT(*) AS null_rating_cnt FROM movie_rating.ratings WHERE rating IS NULL OR rating NOT BETWEEN 0 AND 5;这一步极其重要。很多人在练习时直接进入统计,等结果对不上才怀疑数据有问题。实际上,造数时故意留一些重复和脏值,能让后面的去重练习有真实对象,而不是为了写函数而写函数。
2.4 数据质量体检的细节
查看总行数时,我更喜欢用COUNT(*)而不是COUNT(1),两者结果一致,但COUNT(*)更直观。如果需要看明细,建议先SELECT * ... LIMIT 10确认字段顺序对不对,因为 CSV 导入是按位置匹配的,字段顺序错了会整列污染。
另一个容易忽略的是字段空值。COUNT(rating)不会统计 NULL,而COUNT(*)会计数所有行。这个差异在 LEFT JOIN 场景里特别容易踩坑,我后面会专门讲。数据体检阶段只要确认异常范围,不用急着清洗,清洗动作放到正式练习里做。
3. 基础 SQL 练习:从清洗到统计
3.1 排重与空值处理
在实际业务里,用户对同一部电影多次评分是有可能的,比如先打了低分,看完全片后又改高分。业务上一般取最新一次评分。Hive 里最通用的排重方式是ROW_NUMBER:
SELECT user_id, movie_id, rating, rating_time FROM ( SELECT user_id, movie_id, rating, rating_time, ROW_NUMBER() OVER (PARTITION BY user_id, movie_id ORDER BY rating_time DESC) AS rn FROM movie_rating.ratings ) t WHERE rn = 1;这里的关键点是PARTITION BY user_id, movie_id,它定义了一部电影一个用户的唯一组合;ORDER BY rating_time DESC保证最新时间排在前面,rn=1 就是最新评分。如果你想保留最早评分,改ASC即可。
空值处理要特别注意业务口径。很多人习惯用NVL(rating, 0)把空值补成 0,这其实很危险。因为某个用户确实没给评分,补 0 等于人为拉低平均分。更合理的做法是:分析平均分时过滤掉空值,或者在填充前先看一眼空值比例。如果是测试数据,直接过滤掉最干净:
SELECT * FROM movie_rating.ratings WHERE rating IS NOT NULL AND rating_time IS NOT NULL;3.2 电影维度统计
先做最简单的聚合需求:统计每部电影的评分人数、平均分、最高分、最低分。
SELECT m.movie_id, m.movie_name, COUNT(r.rating) AS rating_cnt, ROUND(AVG(r.rating), 2) AS avg_rating, MAX(r.rating) AS max_rating, MIN(r.rating) AS min_rating FROM movie_rating.movies m LEFT JOIN movie_rating.ratings r ON m.movie_id = r.movie_id GROUP BY m.movie_id, m.movie_name HAVING COUNT(r.rating) > 0 ORDER BY avg_rating DESC;我特意用了LEFT JOIN而不是INNER JOIN,目的是保留所有电影,包括那些还没有人评分的电影。注意COUNT(r.rating)不会统计右表为 NULL 的行,所以没评分的电影 rating_cnt 是 0,恰好符合业务语义。如果你写成COUNT(*),没评分的电影会因为左表字段补 NULL 被算成 1 行,结果就错了。
找出评分人数 Top 10 电影,可以直接在外面套一层子查询:
SELECT movie_id, movie_name, rating_cnt, avg_rating FROM ( SELECT m.movie_id, m.movie_name, COUNT(r.rating) AS rating_cnt, ROUND(AVG(r.rating), 2) AS avg_rating FROM movie_rating.movies m LEFT JOIN movie_rating.ratings r ON m.movie_id = r.movie_id GROUP BY m.movie_id, m.movie_name ) t ORDER BY rating_cnt DESC LIMIT 10;注意 Hive 对非聚合列的 GROUP BY 要求很严格,SELECT里的非聚合字段必须全部出现在 GROUP BY 中。所以不能只GROUP BY m.movie_id然后查m.movie_name。
3.3 用户维度统计
用户维度重点看活跃度和评分偏好。一条 SQL 能统计出每个用户的评分次数、平均分、活跃天数和首次/末次评分时间:
SELECT user_id, COUNT(*) AS rating_cnt, ROUND(AVG(rating), 2) AS avg_rating, COUNT(DISTINCT TO_DATE(rating_time)) AS active_days, MIN(rating_time) AS first_rating_time, MAX(rating_time) AS last_rating_time FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id ORDER BY rating_cnt DESC;TO_DATE(rating_time)会把2023-05-01 12:30:00转成2023-05-01,然后COUNT(DISTINCT ...)就能得到活跃天数。这里我用的是清洗后的数据,所以不需要再写NVL之类的东西。
再做一层用户分层,按评分次数分成高频、中频、低频:
SELECT t.user_id, t.rating_cnt, CASE WHEN t.rating_cnt >= 20 THEN '高频用户' WHEN t.rating_cnt >= 10 THEN '中频用户' ELSE '低频用户' END AS user_level FROM ( SELECT user_id, COUNT(*) AS rating_cnt FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id ) t;这个 CASE WHEN 语法很简单,但要注意边界值。我习惯把>=20和>=10分开,这样不会出现重叠。边界条件写反了会导致分类不正确,比如WHEN rating_cnt > 20会把恰好 20 条的用户漏出去。
4. 进阶 SQL 练习:窗口函数与多表关联
4.1 三种排序函数实操对比
窗口函数是 Hive SQL 进阶的分水岭。影评案例里,最常见的需求是“每部电影评分最高的用户是谁”。先看三个函数放一起的效果:
SELECT movie_id, user_id, rating, ROW_NUMBER() OVER (PARTITION BY movie_id ORDER BY rating DESC) AS rn, RANK() OVER (PARTITION BY movie_id ORDER BY rating DESC) AS rk, DENSE_RANK() OVER (PARTITION BY movie_id ORDER BY rating DESC) AS dr FROM movie_rating.ratings WHERE rating IS NOT NULL;三者区别如下:
ROW_NUMBER():按顺序排,不管分数是否相同,名次一定不重复。RANK():分数相同时名次相同,但下一个名次会跳跃。比如两个并列第一,下一个就是第三。DENSE_RANK():分数相同时名次相同,且下一个名次不跳跃。比如两个并列第一,下一个还是第二。
实际业务中,如果只是取固定的业务行,比如“每部电影评分最高的用户”,通常用 ROW_NUMBER。如果榜单强调并列关系,比如“评分最高的三名用户”,用 DENSE_RANK 更合理。很多面试官喜欢问这个区别,所以建议把三条 SQL 都跑一遍,用 LIMIT 看结果,记忆会非常牢。
取每部电影评分最高的用户:
SELECT movie_id, user_id, rating FROM ( SELECT movie_id, user_id, rating, ROW_NUMBER() OVER (PARTITION BY movie_id ORDER BY rating DESC, rating_time DESC) AS rn FROM movie_rating.ratings WHERE rating IS NOT NULL ) t WHERE rn = 1;这里我多加了rating_time DESC作为并列时的次级排序,保证同一个用户如果多次评分也能得到确定结果。
4.2 lag 看用户的评分节奏
“计算每个用户相邻两次评分的时间间隔”这类问题,用LAG非常方便。LAG(rating_time, 1)表示取当前行之前的一行,也就是上一次评分时间:
SELECT user_id, rating_time, LAG(rating_time, 1) OVER (PARTITION BY user_id ORDER BY rating_time) AS prev_rating_time FROM movie_rating.ratings WHERE rating IS NOT NULL;然后在外层算时间差:
SELECT user_id, ROUND(AVG(day_diff), 1) AS avg_interval_days FROM ( SELECT user_id, DATEDIFF( TO_DATE(rating_time), TO_DATE(LAG(rating_time, 1) OVER (PARTITION BY user_id ORDER BY rating_time)) ) AS day_diff FROM movie_rating.ratings WHERE rating IS NOT NULL ) t WHERE day_diff IS NOT NULL GROUP BY user_id;这里有三个坑。第一,每个用户的第一条评分记录没有前一行,LAG 返回 NULL,DATEDIFF 结果也是 NULL,所以在聚合前要过滤day_diff IS NOT NULL。第二,Hive 窗口函数不能直接嵌套在 DATEDIFF 的参数里?实际上我上面的写法是可行的,但如果你的 Hive 版本较老,可以先在子查询里先算出prev_rating_time,再在下一层算 DATEDIFF,可读性更好。第三,LAG的第三个参数可以指定默认值,比如LAG(rating_time, 1, rating_time),但这里等于把第一条差值算成 0,业务上没意义,不如直接过滤。
4.3 类型拆分与多表关联实战
影评数据里,一部电影通常有多个类型,比如“动作,科幻”。要分析不同电影类型的评分偏好,就得先把类型拆开。Hive 里的标准做法是split+explode+lateral view。
我先把电影表拆成一行一个类型,再关联评分表:
SELECT g.genre, COUNT(r.rating) AS rating_cnt, ROUND(AVG(r.rating), 2) AS avg_rating FROM ( SELECT movie_id, genre FROM movie_rating.movies LATERAL VIEW explode(split(genre, ',')) t AS genre ) g LEFT JOIN movie_rating.ratings r ON g.movie_id = r.movie_id WHERE r.rating IS NOT NULL GROUP BY g.genre ORDER BY rating_cnt DESC;如果某个电影类型写成“动作, 科幻”,中间带空格,split(genre, ',')会把空格也带进来,导致同一个类型被拆成两个。稳妥的办法是拆完后再用TRIM清理:
SELECT movie_id, TRIM(genre) AS genre FROM movie_rating.movies LATERAL VIEW explode(split(genre, ',')) t AS genre;这类行转列操作在日志解析、标签拆解场景非常常见。影评数据的好处是你能直观看到“动画”类型平均分是不是真的比恐怖片高,从而验证 SQL 是否正确。
5. 复杂业务场景实战
5.1 计算连续评分天数
连续行为判断是 SQL 经典题,Hive 里最通用的解法是“日期减行号”。思路分三步:
第一步,把每个用户每天的多条评分记录去重成一条,得到用户活跃日期表:
SELECT user_id, TO_DATE(rating_time) AS rating_date FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id, TO_DATE(rating_time);第二步,按用户分组,对日期排序,同时用 DATE_SUB 把日期减去行号:
SELECT user_id, rating_date, DATE_SUB(rating_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY rating_date)) AS date_group FROM ( SELECT user_id, TO_DATE(rating_time) AS rating_date FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id, TO_DATE(rating_time) ) t;为什么连续日期的 date_group 会相同?因为连续日期每天递增 1,行号也递增 1,相减之后自然保持不变。比如 1 号和 2 号,1 号减 1 等于 12 月 31 号,2 号减 2 也等于 12 月 31 号。一旦断档,差值就会变化。
第三步,按 user_id 和 date_group 分组,统计连续天数,并过滤出连续 3 天及以上的用户:
SELECT user_id, COUNT(*) AS consecutive_days FROM ( SELECT user_id, rating_date, DATE_SUB(rating_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY rating_date)) AS date_group FROM ( SELECT user_id, TO_DATE(rating_time) AS rating_date FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id, TO_DATE(rating_time) ) t ) t2 GROUP BY user_id, date_group HAVING COUNT(*) >= 3 ORDER BY consecutive_days DESC;这道题的训练价值不在于记住解法,而在于理解“日期减行号”背后的数学逻辑。做完一遍后,很多连续登录、连续打卡的题目都能套用。
5.2 月度评分活跃度与月度 Top3 电影
先按月统计评分次数和参与用户数:
SELECT DATE_FORMAT(TO_DATE(rating_time), 'yyyy-MM') AS month, COUNT(*) AS rating_cnt, COUNT(DISTINCT user_id) AS user_cnt FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY DATE_FORMAT(TO_DATE(rating_time), 'yyyy-MM') ORDER BY month;注意 Hive 的DATE_FORMAT格式是yyyy-MM,不要用YYYY-MM。虽然 Hive 某些版本能容忍,但规范起见全用小写 yyyy。
如果要做每个月的评分 Top 3 电影,需要先用子查询按月聚合,再用窗口函数排名:
SELECT month, movie_id, rating_cnt FROM ( SELECT month, movie_id, rating_cnt, ROW_NUMBER() OVER (PARTITION BY month ORDER BY rating_cnt DESC) AS rn FROM ( SELECT DATE_FORMAT(TO_DATE(rating_time), 'yyyy-MM') AS month, movie_id, COUNT(*) AS rating_cnt FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY DATE_FORMAT(TO_DATE(rating_time), 'yyyy-MM'), movie_id ) t ) t2 WHERE rn <= 3 ORDER BY month, rn;这里能很明显看到窗口函数和 GROUP BY 的执行顺序:窗口函数是在 GROUP BY 聚合之后计算的,所以在窗口子查询里可以直接用 COUNT(*) 的结果,但不能在窗口函数内引用一个还没有定义的别名rating_cnt,需要重复写一遍聚合表达式。
5.3 用户分层与类型偏好交叉分析
最后做一个综合业务场景:不同分层用户的电影类型偏好。这个场景要用到前面创建的用户分层结果,以及类型拆分后的电影表。
首先是把用户分层和类型拆分都封装成子查询:
SELECT lu.user_level, tg.genre, COUNT(*) AS rating_cnt FROM ( SELECT u.user_id, CASE WHEN t.rating_cnt >= 20 THEN '高频用户' WHEN t.rating_cnt >= 10 THEN '中频用户' ELSE '低频用户' END AS user_level FROM ( SELECT user_id, COUNT(*) AS rating_cnt FROM movie_rating.ratings WHERE rating IS NOT NULL GROUP BY user_id ) t JOIN movie_rating.users u ON t.user_id = u.user_id ) lu JOIN ( SELECT r.user_id, g.genre FROM movie_rating.ratings r JOIN ( SELECT movie_id, TRIM(genre) AS genre FROM movie_rating.movies LATERAL VIEW explode(split(genre, ',')) t AS genre ) g ON r.movie_id = g.movie_id WHERE r.rating IS NOT NULL ) tg ON lu.user_id = tg.user_id GROUP BY lu.user_level, tg.genre ORDER BY lu.user_level, rating_cnt DESC;这个 SQL 比较长,但拆开看并不复杂:先得到“带层级的用户”,再得到“用户对每个类型的评分记录”,最后做 JOIN 和 GROUP BY。真正的复杂业务场景基本都是若干个子查询的堆叠,关键是每一层都只做一件事。
6. 常见问题与排查技巧
6.1 空值、NULL 和 COUNT 的口径
这是影评练习里出现频率最高的坑。第一,COUNT(*)和COUNT(rating)在 LEFT JOIN 场景结果不同。统计“每部电影评分人数”时,如果用COUNT(*),没评分电影也会被算成 1,应该用COUNT(r.rating)或COUNT(1)过滤掉右表 NULL。第二,ROUND(AVG(rating), 2)会自动忽略 NULL,不需要手动WHERE rating IS NULL排除。第三,筛选空值时不要写rating = NULL,要用IS NULL。这几个问题,练完影评案例基本都能形成肌肉记忆。
6.2 窗口函数常见报错
窗口函数练习中常见两个问题。一是子查询里给窗口函数的结果加 WHERE 过滤,比如WHERE rn = 1,Hive 不允许在同一层直接写,必须额外嵌套一层。二是窗口函数里用了聚合结果别名,比如ORDER BY COUNT(*) DESC,这个可以写,但如果你在窗口函数里引用已经定义好的列别名,Hive 不一定支持,稳妥做法是把聚合表达式重复写一遍。这两个点我在 5.2 的月度 Top3 里都实际用到了,建议对照着看。
6.3 数据倾斜与小文件
影评数据里如果有一部现象级电影被大量评分,按电影分组聚合时就会遇到数据倾斜。练习数据只有几百条看不出来,但生产环境要具备这个意识。针对 GROUP BY 倾斜,简单做法是加盐打散到多个并行任务,再聚合一次;针对 JOIN 倾斜,如果小表能做内存,就用 MAPJOIN。不过这些属于优化话题,新手阶段不用硬啃,先跑对结果更重要。
小文件问题同样容易被忽略。频繁 INSERT 会产生大量小文件,影响后续查询效率。如果练习环境允许,可以在建表时设置TBLPROPERTIES或使用INSERT OVERWRITE合并小文件。最直接的习惯是:练习结束后把中间临时表清掉,不给 Hadoop 集群制造垃圾。
6.4 SQL 自查方法
我在带 A 同学做这套影评练习时,最常提醒他的不是语法,而是自查方式。第一,写完一条复杂 SQL,先加LIMIT 10看明细,确认字段值符合预期,再跑全量。第二,遇到报错,先用子查询把问题缩小到某一段,不要整体查。第三,善用EXPLAIN看执行计划,能发现笛卡尔积、无效 JOIN 这类隐患。第四,给所有表起别名,尤其是多表 JOIN 和 lateral view 混用的时候,没有别名的 SQL 几乎没法读。
这些习惯比多背几个函数重要得多。练影评案例时,我建议你把每道题都刻意写两遍:第一遍用最直观的写法,第二遍尝试用窗口函数或子查询优化。这样你才能真正理解不同写法的性能差异和可读性差异。
整套影评案例做下来,我个人最受用的不是学会某条具体函数,而是把业务问题拆解成 SQL 步骤的方法。A 同学每次卡住时,我都会让他先把“我要算什么”用一句话说清楚,再翻译成“从哪张表、过滤什么、按什么分组、用什么窗口”。重复几次,Hive SQL 就熟练了。最后再分享一个小技巧:练习数据不要造得太干净,故意留一些重复评分、缺失值和异常时间,这样排重的坑、空值的坑、日期解析的坑你都会真实踩一遍,比看十篇文档都有用。