一、核心面试问题集锦 🎯
1. 数据倾斜 🛡️
🔍 考察重点:数据倾斜的识别、产生原因、针对性解决思路(结合 Hive/Spark 实际应用场景)
📌 核心解答:
① 定义:数据倾斜是大数据处理中常见问题,指大量数据集中分配到少数任务节点,导致这些节点执行缓慢(拖慢整个作业进度),其他节点却处于空闲状态的现象。
② 产生原因(高频场景):
-
键值分布不均:如某类用户 ID、null 值占比极高(如 99% 数据的分区字段为 null),导致对应 reduce 任务数据量过大;
-
join 操作不当:大表与小表 join 时,小表未广播,或大表 join 键分布不均;
-
聚合操作热点:如统计某热门商品的销量,该商品的聚合任务承担大量数据。
③ 解决方法(分场景落地):
-
针对 null 值 / 热点键:
-
null 值打散:将 null 值替换为随机数(如 concat ('null_', rand ())),分散到不同任务,后续再聚合;
-
热点键单独处理:将热点键数据单独过滤出来,用 mapjoin 或特殊逻辑计算,非热点数据正常处理,最后合并结果;
-
-
针对 join 倾斜:
-
小表广播(mapjoin):Hive 设置 hive.auto.convert.join=true,Spark 使用 broadcastJoin,将小表加载到内存广播,避免 shuffle;
-
大表拆分:将大表按热点键拆分,热点部分与小表单独 join,非热点部分正常 join;
-
-
针对聚合倾斜:
-
两阶段聚合:先对数据加随机前缀做局部聚合(map 端聚合),再去掉前缀做全局聚合(如 count→局部 count + 全局 count);
-
增大并行度:Hive 设置 mapred.reduce.tasks,Spark 设置 spark.sql.shuffle.partitions,提升任务并行处理能力。
-
2. CTE 的执行计划 📊
🔍 考察重点:CTE(公共表表达式)的执行机制、与子查询的区别、性能影响
📌 核心解答:
① 定义:CTE 通过 WITH 子句定义临时结果集,可在后续 SQL 中多次引用,语法:WITH cte_name AS (SELECT ...) SELECT ... FROM cte_name;
② 执行计划核心逻辑:
-
多数主流数据库(Hive 2.0+/Spark SQL/MySQL 8.0+)对 CTE 采用 “优化合并” 策略,若 CTE 被多次引用,会将其物化(Materialize)为临时表,避免重复计算(对比子查询:多次引用的子查询可能被重复执行);
-
若 CTE 仅被引用一次,优化器可能将其与主查询合并,生成与子查询类似的执行计划,无额外性能开销;
-
特殊场景:复杂 CTE(含多表 join / 聚合)若被多次引用,物化会占用临时存储,需权衡引用次数与计算复杂度。
③ 与子查询的区别:
-
可读性:CTE 更清晰,适合复杂查询(如多阶段数据处理);
-
性能:多次引用时 CTE 更优(避免重复计算),单次引用时性能基本一致;
-
作用域:CTE 仅在当前 SQL 语句中有效,子查询作用域局限于自身所在的查询块。
3. Spark Stage 怎么划分 🚀
🔍 考察重点:Spark 核心执行机制、Stage 划分依据、宽依赖与窄依赖的理解
📌 核心解答:
① 核心原则:Stage 是 Spark 任务的基本执行单元,按 “窄依赖(Narrow Dependency)” 聚合,按 “宽依赖(Wide Dependency)” 拆分,一个 Stage 内包含多个连续的窄依赖算子。
② 关键概念:
-
窄依赖:父 RDD 的一个分区仅被子 RDD 的一个分区依赖(如 map、filter、union、sample),数据无需跨节点传输;
-
宽依赖:父 RDD 的一个分区被子 RDD 的多个分区依赖(如 shuffle、groupByKey、reduceByKey、join),需触发 shuffle 操作(数据跨节点传输)。
③ 划分流程:
-
从最终的 Action 算子(如 count、collect、saveAsTextFile)反向推导;
-
遇到宽依赖算子时,以此为边界拆分 Stage(宽依赖算子作为下一个 Stage 的开始);
-
同一个 Stage 内的算子按顺序组成 TaskSet,每个 Task 对应一个数据分区;
④ 示例:
-
代码:sc.textFile ("data.txt").map (.split(",")).filter(.length>2).groupByKey().count()
-
划分结果:
Stage 1:textFile(输入)→ map → filter(均为窄依赖,无 shuffle);
Stage 2:groupByKey(宽依赖,触发 shuffle)→ count(Action 算子)。
4. 什么算子会触发宽依赖 🔗
🔍 考察重点:宽依赖的识别、对应算子的应用场景、与窄依赖算子的区分
📌 核心解答:
① 宽依赖核心特征:触发 shuffle 操作(数据跨 Executor / 节点传输),父 RDD 分区与子 RDD 分区为 “一对多” 关系;
② 高频触发宽依赖的算子(Spark):
-
聚合类:groupByKey、reduceByKey、aggregateByKey、combineByKey;
-
关联类:join(内连接 / 外连接,除非是 broadcastJoin)、cogroup;
-
重分区类:repartition(重新分区,会 shuffle)、coalesce(当参数 shuffle=true 时,默认 false 不 shuffle);
-
排序类:sortByKey、sortBy;
③ 注意:部分算子是否触发宽依赖需看场景:
-
join:若其中一个表是小表(满足广播条件,如默认小于 10MB),使用 broadcastJoin 时为窄依赖;否则为宽依赖;
-
coalesce:默认 shuffle=false,仅合并分区(窄依赖);当需要增加分区数或跨节点合并时,需设置 shuffle=true,变为宽依赖。
5. Hive 内部表和外部表区别 🗂️
🔍 考察重点:两种表的本质差异、适用场景、数据生命周期管理
📌 核心解答(对比梳理):
💡 面试应答技巧:结合实际场景说明选择逻辑,如 “原始日志数据用外部表,避免误删;数仓中间层的聚合表用内部表,方便清理过期数据”。
6. HDFS 小文件过多会有什么问题 📁
🔍 考察重点:HDFS 架构理解、小文件的影响、解决方案
📌 核心解答:
① 定义:HDFS 中小文件指文件大小远小于 Block 块大小(默认 128MB)的文件(如 KB 级、MB 级文件);
② 核心问题:
-
NameNode 压力过大:NameNode 存储文件元数据(文件名、路径、Block 位置等),每个小文件对应一条元数据记录,大量小文件会耗尽 NameNode 内存,影响集群稳定性和并发处理能力;
-
读写效率低:每个小文件的读写都需要建立独立的 TCP 连接,频繁的连接建立 / 关闭会产生大量开销;同时,MapReduce 任务中,一个小文件对应一个 Map 任务,大量小文件会导致 Map 任务数量激增,任务调度开销大于计算开销;
-
存储利用率低:HDFS 中文件最小存储单位是 Block,即使小文件小于 Block 大小,也会占用一个 Block 的存储空间(如 1KB 文件占用 128MB Block)。
③ 解决方案:
-
预处理合并:用 Hive 的 INSERT OVERWRITE 将小文件合并为大文件(如按天 / 小时聚合),或用 Shell 脚本(cat)、Spark 程序合并;
-
开启 Hive 小文件合并参数:
-
输出时合并:hive.merge.mapfiles=true(Map 任务输出合并)、hive.merge.mapredfiles=true(Reduce 任务输出合并);
-
设定合并文件大小:hive.merge.size.per.task=128000000(合并后文件大小 128MB);
-
-
采用 HAR 归档:将小文件归档为 HAR 文件(Hadoop Archive),减少元数据数量(HAR 文件视为一个文件,内部包含多个小文件);
-
调整 Block 大小:针对特定小文件场景,适当减小 Block 大小(需权衡其他大文件存储效率)。
7. 对数仓分层的理解 📊
🔍 考察重点:数仓分层的核心思想、各层作用、设计原则(结合实际业务场景)
📌 核心解答:
① 核心思想:数仓分层是为了 “数据有序管理、减少重复计算、提升数据质量、支撑灵活业务需求”,按数据加工复杂度和用途从低到高分层,每层数据单向流转(从原始数据到应用数据)。
② 主流分层及作用(行业通用五层架构):
-
ODS 层(操作数据存储层):
作用:存储原始数据(如用户行为日志、业务数据库同步数据),结构与源系统一致,不做清洗或少量清洗(去重、过滤无效字符);
特点:数据量大、保留原始信息,用于数据追溯和重新加工;
-
DWD 层(数据明细层):
作用:对 ODS 层数据进行清洗、转换、整合,生成结构化的明细数据(如用户行为明细、订单明细);
特点:消除数据冗余、统一数据口径,为上层提供高质量的明细数据;
-
DWS 层(数据汇总层):
作用:按业务主题(如用户、商品、订单)对 DWD 层数据进行聚合,生成轻度汇总指标(如用户近 7 天活跃度、商品近 30 天销量);
特点:减少上层应用的重复聚合计算,提升查询效率;
-
ADS 层(应用数据服务层):
作用:针对具体业务需求(如报表统计、大屏展示、标签输出),对 DWS 层数据进一步聚合或加工,生成直接可用的结果数据;
特点:数据量小、针对性强,直接对接业务系统;
-
维度层(DIM 层):
作用:存储维度信息(如用户维度、商品维度、时间维度),为 DWD/DWS 层的关联分析提供支撑;
特点:数据相对稳定,可重复复用。
③ 设计原则:
-
单向流转:数据只能从下层向上层流动,不允许跨层直接访问(如 ADS 层不直接读取 ODS 层);
-
口径统一:同一指标在不同层的计算口径一致(如 “活跃用户” 定义统一);
-
可追溯性:每层数据都能追溯到原始数据来源。
8. 数仓业务域和主题域的区别 🏷️
🔍 考察重点:数仓主题建模理解、业务域与主题域的划分逻辑、实际应用价值
📌 核心解答:
① 核心定义:
-
业务域:按企业业务线划分的大类,对应企业的核心业务板块(如电商企业的 “交易域”“用户域”“商品域”“营销域”),聚焦 “做什么业务”;
-
主题域:在业务域下,按数据主题(分析视角)划分的子类别,聚焦 “分析什么数据”,一个业务域可包含多个主题域。
② 核心区别(举例说明,电商场景):
| 维度 | 业务域 | 主题域 |
|--------------|-----------------------|---------------------------------|
| 划分依据 | 企业业务线、组织架构 | 数据主题、分析需求 |
| 粒度 | 较粗(对应核心业务板块) | 较细(对应具体分析视角) |
| 示例 | 交易域、用户域 | 交易域下的 “订单主题”“支付主题”;用户域下的 “用户注册主题”“用户行为主题” |
| 核心作用 | 界定数仓的整体业务范围 | 细分数据类别,支撑针对性分析 |
③ 关联与应用:
-
层级关系:业务域是主题域的父层级,先划分业务域,再在业务域内拆分主题域;
-
建模价值:通过 “业务域→主题域” 的划分,使数仓结构清晰,数据归属明确,方便数据管理和业务对接(如运营同学需分析订单数据,可直接定位到 “交易域 - 订单主题”)。
二、手撕题目集锦 💻
1. SQL:查询某段时间借书的 uid
📌 题目分析:核心是筛选指定时间范围内有借书记录的用户 ID,需明确表结构(假设表名:borrow_record,核心字段:uid、borrow_time、book_id 等),注意时间范围的边界处理。
📝 解题思路:
-
明确筛选条件:borrow_time 在目标时间段内(如 2024-01-01 00:00:00 至 2024-01-31 23:59:59);
-
去重:若同一用户同一时间段多次借书,需去重(distinct uid);
💻 参考代码:
sql
-- 假设目标时间段:2024年1月1日至2024年1月31日
SELECT DISTINCT uid
FROM borrow_record
WHERE borrow_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 23:59:59'
-- 若borrow_time为日期类型(无时分秒),可简化为:
-- WHERE borrow_time BETWEEN '2024-01-01' AND '2024-01-31'
💡 面试注意点:
-
主动询问表结构和字段类型(如 borrow_time 是 datetime 还是 date);
-
考虑时间边界:如是否包含起始 / 结束当天,若业务要求 “不包含 1 月 31 日”,可改为 borrow_time < '2024-02-01';
-
去重逻辑:若题目允许返回重复 uid 则无需 distinct,否则必须加。
2. SQL:查询至少连续两天登录的用户
📌 题目分析:核心是识别用户登录记录中存在 “连续两天” 的情况,需用到窗口函数(lag/lead)获取相邻登录日期,或用自连接匹配连续日期。
📝 解题思路(两种常用方法):
方法一:使用 lag 窗口函数(推荐,逻辑清晰)
-
步骤 1:对用户登录记录按 uid 分组、login_time 排序,用 lag (login_time, 1) 获取上一次登录日期;
-
步骤 2:计算当前登录日期与上一次登录日期的差值,若差值 = 1 天,则该用户满足条件;
-
步骤 3:对满足条件的 uid 去重(避免同一用户多次连续登录导致重复)。
方法二:自连接(适用于不熟悉窗口函数的场景)
-
步骤 1:将登录表自连接(a join b on a.uid = b.uid);
-
步骤 2:筛选 b.login_time = a.login_time + 1 天的记录;
-
步骤 3:去重得到满足条件的 uid。
💻 参考代码(方法一:lag 窗口函数):
sql
-- 假设登录表:user_login,核心字段:uid、login_time(日期类型,如'2024-01-01')
WITH login_with_prev AS (
SELECT
uid,
login_time,
-- 获取同一用户上一次登录日期
LAG(login_time, 1) OVER (PARTITION BY uid ORDER BY login_time) AS prev_login_time
FROM user_login
-- 可选:去重同一用户同一天多次登录的记录(若存在)
GROUP BY uid, login_time
)
SELECT DISTINCT uid
FROM login_with_prev
-- 计算两次登录日期差值为1天
WHERE DATEDIFF(login_time, prev_login_time) = 1;
💻 参考代码(方法二:自连接):
sql
SELECT DISTINCT a.uid
FROM user_login a
JOIN user_login b
ON a.uid = b.uid
AND DATEDIFF(b.login_time, a.login_time) = 1
-- 去重同一用户同一天多次登录的记录
GROUP BY a.uid, a.login_time;
💡 面试注意点:
-
处理同一用户同一天多次登录:需先按 uid 和 login_time 分组去重,否则会导致连续日期判断错误;
-
日期函数:不同数据库日期差函数不同(Hive/MySQL 用 DATEDIFF,Oracle 用 TRUNC (login_time) - TRUNC (prev_login_time)),需主动说明;
-
边界情况:若用户登录记录只有一条,或无连续日期,不纳入结果。
3. 算法:有序重复数组返回 n 出现的次数
📌 题目分析:数组有序(升序 / 降序),需高效查询目标值 n 的出现次数;若用暴力遍历(O (n))效率低,最优解为二分查找(O (log n)),符合大厂对 “高效算法” 的考察要求。
📝 解题思路(二分查找):
核心逻辑:有序数组中,目标值 n 的所有出现位置是连续的,只需找到 n 的 “第一个出现位置” 和 “最后一个出现位置”,次数 = 最后位置 - 第一个位置 + 1(若存在 n),否则为 0。
步骤:
-
实现二分查找找第一个 n 的索引(left_idx):
-
当 nums [mid] == n 时,不停止查找,继续向左搜索(right = mid - 1),记录当前 mid 为候选 left_idx;
-
-
实现二分查找找最后一个 n 的索引(right_idx):
-
当 nums [mid] == n 时,不停止查找,继续向右搜索(left = mid + 1),记录当前 mid 为候选 right_idx;
-
-
计算次数:若 left_idx <= right_idx(存在 n),则次数 = right_idx - left_idx + 1;否则为 0。
💻 参考代码(Python,升序数组):
python
运行
def count_target(nums, n):
if not nums:
return 0
# 找第一个n的索引
left, right = 0, len(nums) - 1
left_idx = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] == n:
left_idx = mid
right = mid - 1 # 向左找更前的n
elif nums[mid] < n:
left = mid + 1
else:
right = mid - 1
# 找最后一个n的索引
left, right = 0, len(nums) - 1
right_idx = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] == n:
right_idx = mid
left = mid + 1 # 向右找更后的n
elif nums[mid] < n:
left = mid + 1
else:
right = mid - 1
# 计算次数
return right_idx - left_idx + 1 if left_idx != -1 else 0
# 测试用例
print(count_target([1,2,2,2,3,4], 2)) # 输出3
print(count_target([1,3,5,7], 2)) # 输出0
print(count_target([2,2,2,2], 2)) # 输出4
💡 面试注意点:
-
时间复杂度:O (log n),优于暴力遍历的 O (n),适合大数据量场景;
-
边界处理:数组为空、n 不存在、n 是数组第一个 / 最后一个元素、数组全是 n 等情况;
-
降序数组适配:只需修改二分查找的条件(nums [mid] < n 时,right=mid-1;nums [mid] > n 时,left=mid+1)。