Loading......

文章背景图

🚀 字节跳动大数据开发面经 2️⃣

2025-12-24
12
-
- 分钟

一、核心面试问题集锦 🎯

1. 数据倾斜 🛡️

🔍 考察重点:数据倾斜的识别、产生原因、针对性解决思路(结合 Hive/Spark 实际应用场景)

📌 核心解答:

① 定义:数据倾斜是大数据处理中常见问题,指大量数据集中分配到少数任务节点,导致这些节点执行缓慢(拖慢整个作业进度),其他节点却处于空闲状态的现象。

② 产生原因(高频场景):

  • 键值分布不均:如某类用户 ID、null 值占比极高(如 99% 数据的分区字段为 null),导致对应 reduce 任务数据量过大;

  • join 操作不当:大表与小表 join 时,小表未广播,或大表 join 键分布不均;

  • 聚合操作热点:如统计某热门商品的销量,该商品的聚合任务承担大量数据。

    ③ 解决方法(分场景落地):

  • 针对 null 值 / 热点键:

    1. null 值打散:将 null 值替换为随机数(如 concat ('null_', rand ())),分散到不同任务,后续再聚合;

    2. 热点键单独处理:将热点键数据单独过滤出来,用 mapjoin 或特殊逻辑计算,非热点数据正常处理,最后合并结果;

  • 针对 join 倾斜:

    1. 小表广播(mapjoin):Hive 设置 hive.auto.convert.join=true,Spark 使用 broadcastJoin,将小表加载到内存广播,避免 shuffle;

    2. 大表拆分:将大表按热点键拆分,热点部分与小表单独 join,非热点部分正常 join;

  • 针对聚合倾斜:

    1. 两阶段聚合:先对数据加随机前缀做局部聚合(map 端聚合),再去掉前缀做全局聚合(如 count→局部 count + 全局 count);

    2. 增大并行度: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 操作(数据跨节点传输)。

    ③ 划分流程:

  1. 从最终的 Action 算子(如 count、collect、saveAsTextFile)反向推导;

  2. 遇到宽依赖算子时,以此为边界拆分 Stage(宽依赖算子作为下一个 Stage 的开始);

  3. 同一个 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 内部表和外部表区别 🗂️

🔍 考察重点:两种表的本质差异、适用场景、数据生命周期管理

📌 核心解答(对比梳理):

对比维度

内部表(Managed Table)

外部表(External Table)

数据归属

数据由 Hive 管理,存储在 Hive 默认仓库(/user/hive/warehouse)

数据不由 Hive 管理,存储在用户指定路径

删除表操作

DROP TABLE 时,同时删除表元数据和实际数据

DROP TABLE 时,仅删除表元数据,实际数据保留

适用场景

临时数据处理、中间结果表(数据生命周期与表绑定)

多工具共享数据(如 Spark/Hive 同时访问)、原始数据存储(避免误删数据)

数据导入

支持 LOAD DATA INPATH(移动数据到仓库)、INSERT INTO

需指定 LOCATION,数据可直接放在指定路径,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 小文件合并参数:

    1. 输出时合并:hive.merge.mapfiles=true(Map 任务输出合并)、hive.merge.mapredfiles=true(Reduce 任务输出合并);

    2. 设定合并文件大小: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。

步骤:

  1. 实现二分查找找第一个 n 的索引(left_idx):

    • 当 nums [mid] == n 时,不停止查找,继续向左搜索(right = mid - 1),记录当前 mid 为候选 left_idx;

  2. 实现二分查找找最后一个 n 的索引(right_idx):

    • 当 nums [mid] == n 时,不停止查找,继续向右搜索(left = mid + 1),记录当前 mid 为候选 right_idx;

  3. 计算次数:若 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)。

评论交流