大数据开发岗面试必备:SQL 高频题、Spark 性能调优、数仓建模实战、项目经验梳理,覆盖初中级到高级岗位
📌 前言
为什么面试总被问倒?
为什么项目经验说不清楚?
为什么调优问题总是泛泛而谈?
根本原因:没有系统准备面试题库!
本篇整理大数据开发岗高频面试题,涵盖:
- 🔍 SQL 高频面试题(窗口函数、行转列)
- 🔍 Spark 性能调优实战
- 🔍 数仓建模设计题
- 🔍 项目经验梳理方法
- 🔍 行为面试题回答模板
题目均来自真实面试,附详细答案!
📊 SQL 高频面试题
题目 1:连续登录问题
题目描述:
用户登录表 user_login_log,字段:user_id, login_date
找出连续登录 3 天及以上的用户。
表结构:
CREATE TABLE user_login_log (
user_id STRING,
login_date STRING -- 格式:yyyy-MM-dd
)
PARTITIONED BY (dt STRING);
示例数据:
| user_id | login_date |
|---|---|
| U001 | 2026-03-01 |
| U001 | 2026-03-02 |
| U001 | 2026-03-03 |
| U001 | 2026-03-05 |
| U002 | 2026-03-01 |
| U002 | 2026-03-03 |
| U002 | 2026-03-05 |
预期结果:
| user_id | start_date | end_date | days |
|---|---|---|---|
| U001 | 2026-03-01 | 2026-03-03 | 3 |
解答:
WITH login_rank AS (
SELECT
user_id,
login_date,
-- 关键:日期减去行号,连续日期的差值相同
DATE_SUB(login_date, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
)) AS date_group
FROM user_login_log
WHERE dt >= '2026-03-01'
),
continuous_groups AS (
SELECT
user_id,
date_group,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS days
FROM login_rank
GROUP BY user_id, date_group
HAVING COUNT(*) >= 3
)
SELECT * FROM continuous_groups;
考察点:
- 窗口函数 ROW_NUMBER 使用
- 连续问题的解题思路(日期 - 行号)
- 分组聚合
题目 2:TopN 问题
题目描述:
订单表 order_info,字段:order_id, user_id, category_id, pay_amount, create_time
求每个品类下消费金额最高的 Top3 用户。
解答:
WITH user_category_amount AS (
SELECT
user_id,
category_id,
SUM(pay_amount) AS total_amount
FROM order_info
WHERE dt = '2026-03-24'
GROUP BY user_id, category_id
),
ranked_users AS (
SELECT
user_id,
category_id,
total_amount,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY total_amount DESC
) AS rn
FROM user_category_amount
)
SELECT
user_id,
category_id,
total_amount,
rn AS rank
FROM ranked_users
WHERE rn <= 3;
考察点:
- 分组聚合
- ROW_NUMBER 排名
- TopN 问题通用解法
扩展:
- 如果金额相同怎么处理? → 使用 DENSE_RANK
- 如何取前 10%? → 使用 NTILE(10)
题目 3:行转列问题
题目描述:
学生成绩表 student_score,字段:student_id, subject, score
将科目转为列,输出:student_id, chinese, math, english
示例数据:
| student_id | subject | score |
|---|---|---|
| S001 | chinese | 85 |
| S001 | math | 92 |
| S001 | english | 78 |
| S002 | chinese | 90 |
| S002 | math | 88 |
| S002 | english | 95 |
预期结果:
| student_id | chinese | math | english |
|---|---|---|---|
| S001 | 85 | 92 | 78 |
| S002 | 90 | 88 | 95 |
解答:
SELECT
student_id,
MAX(CASE WHEN subject = 'chinese' THEN score ELSE 0 END) AS chinese,
MAX(CASE WHEN subject = 'math' THEN score ELSE 0 END) AS math,
MAX(CASE WHEN subject = 'english' THEN score ELSE 0 END) AS english
FROM student_score
GROUP BY student_id;
考察点:
- CASE WHEN 条件表达式
- 行转列的核心思路(GROUP BY + 聚合)
- MAX/SUM 的选择(单值用 MAX,多值用 SUM)
扩展:列转行
-- 使用 UNION ALL
SELECT student_id, 'chinese' AS subject, chinese AS score FROM score_wide
UNION ALL
SELECT student_id, 'math' AS subject, math AS score FROM score_wide
UNION ALL
SELECT student_id, 'english' AS subject, english AS score FROM score_wide;
-- 或使用 LATERAL VIEW explode (Hive 2.2+)
题目 4:留存率计算
题目描述:
用户登录表,计算次日留存率、7 日留存率。
留存率公式:
次日留存率 = 第 1 天登录且第 2 天也登录的用户数 / 第 1 天登录的用户数
解答:
WITH daily_users AS (
SELECT
user_id,
TO_DATE(login_time) AS login_date
FROM user_login_log
WHERE dt >= '2026-03-01'
GROUP BY user_id, TO_DATE(login_time)
),
retention AS (
SELECT
t1.login_date AS stat_date,
COUNT(DISTINCT t1.user_id) AS day1_users,
COUNT(DISTINCT CASE WHEN t2.user_id IS NOT NULL THEN t1.user_id END) AS day2_users,
COUNT(DISTINCT CASE WHEN t3.user_id IS NOT NULL THEN t1.user_id END) AS day7_users
FROM daily_users t1
LEFT JOIN daily_users t2
ON t1.user_id = t2.user_id
AND t2.login_date = DATE_ADD(t1.login_date, 1)
LEFT JOIN daily_users t3
ON t1.user_id = t3.user_id
AND t3.login_date = DATE_ADD(t1.login_date, 6)
GROUP BY t1.login_date
)
SELECT
stat_date,
day1_users,
day2_users,
day7_users,
ROUND(day2_users * 100.0 / day1_users, 2) AS retention_rate_1d,
ROUND(day7_users * 100.0 / day1_users, 2) AS retention_rate_7d
FROM retention;
考察点:
- 自连接查询
- 留存率计算逻辑
- 日期函数使用
题目 5:GMV 同比环比
题目描述:
订单表,计算每日 GMV 的同比(去年同天)、环比(昨天)、周环比(上周同天)。
解答:
WITH daily_gmv AS (
SELECT
TO_DATE(create_time) AS stat_date,
SUM(pay_amount) AS gmv
FROM order_info
WHERE dt >= '2026-01-01'
GROUP BY TO_DATE(create_time)
),
gmv_with_lag AS (
SELECT
stat_date,
gmv,
LAG(gmv, 1) OVER (ORDER BY stat_date) AS prev_day_gmv,
LAG(gmv, 7) OVER (ORDER BY stat_date) AS prev_week_gmv,
LAG(gmv, 365) OVER (ORDER BY stat_date) AS prev_year_gmv
FROM daily_gmv
)
SELECT
stat_date,
gmv,
ROUND((gmv - prev_day_gmv) * 100.0 / prev_day_gmv, 2) AS day_over_day,
ROUND((gmv - prev_week_gmv) * 100.0 / prev_week_gmv, 2) AS week_over_week,
ROUND((gmv - prev_year_gmv) * 100.0 / prev_year_gmv, 2) AS year_over_year
FROM gmv_with_lag;
考察点:
- LAG 窗口函数
- 同比环比计算
- 时间序列分析
🔥 Spark 性能调优面试题
题目 1:数据倾斜如何处理?
面试官追问:
- 怎么发现数据倾斜?
- 有哪些解决方案?
- 你实际用过哪种?
参考答案:
1. 现象识别:
- 大部分 Task 很快,少数 Task 特别慢
- Spark UI 查看 Stage 详情,Task 耗时差异大
- 查看 Shuffle Read/Write 数据量差异
2. 解决方案:
// 方案 1:Key 加盐(随机前缀)
val saltedRDD = originalRDD.map { case (key, value) =>
val salt = scala.util.Random.nextInt(10)
(s"${salt}_${key}", value)
}
// 局部聚合
val localAgg = saltedRDD.reduceByKey(_ + _)
// 去除盐前缀,全局聚合
val globalAgg = localAgg.map { case (key, value) =>
(key.split("_")(1), value)
}.reduceByKey(_ + _)
// 方案 2:使用广播 Join(小表)
val broadcastSmallTable = spark.sparkContext.broadcast(smallTable.collect())
val result = largeTable.join(broadcastSmallTable.value)
// 方案 3:调整并行度
spark.conf.set("spark.sql.shuffle.partitions", "2000")
3. 实际案例:
场景:用户行为日志,某些热门用户数据量极大
解决:Key 加盐 + 两阶段聚合
效果:任务耗时从 2 小时降到 20 分钟
题目 2:Spark OOM 如何排查?
面试官追问:
- Driver OOM 和 Executor OOM 有什么区别?
- 如何定位内存泄漏?
参考答案:
1. OOM 类型:
Driver OOM:
- collect() 数据量过大
- 广播表太大
- 解决:增加 driver-memory,避免 collect
Executor OOM:
- 分区数据量过大
- 缓存过多
- 解决:增加 executor-memory,调整并行度
2. 排查步骤:
# 1. 查看 Spark UI 内存使用
Spark UI → Executors → Memory/Dis
# 2. 查看 GC 日志
--conf "spark.executor.extraJavaOptions=-XX:+PrintGCDetails"
# 3. 查看 Task 数据量
Spark UI → SQL → 查看 Shuffle Read/Write
# 4. 分析 Dump 文件(生产环境)
jmap -dump:format=b,file=heap.hprof <pid>
3. 解决方案:
// 调整内存配置
--conf spark.driver.memory=4g
--conf spark.executor.memory=8g
--conf spark.executor.memoryOverhead=2g
// 调整并行度
--conf spark.sql.shuffle.partitions=2000
// 避免 collect
// 错误:val data = df.collect()
// 正确:val data = df.take(1000)
// 及时清理缓存
df.unpersist()
题目 3:Shuffle 优化有哪些手段?
参考答案:
1. 减少 Shuffle:
// 使用广播 Join
val result = largeDF.join(broadcast(smallDF), "key")
// 预聚合
df.groupBy("key").agg(sum("value")) // 会产生 Shuffle
df.reduceByKey(_ + _) // Map 端预聚合
2. 调整 Shuffle 参数:
# 增加 Shuffle 分区数
spark.sql.shuffle.partitions=2000
# 调整 Shuffle 缓冲区
spark.shuffle.file.buffer=128k
spark.shuffle.spill.compress=true
spark.io.compression.codec=snappy
3. 使用高效 Shuffle:
# Spark 2.x+ 默认使用 SortShuffleManager
spark.shuffle.manager=sort
# 启用 Tungsten 优化
spark.sql.codegen.wholeStage=true
题目 4:Spark SQL 调优经验?
参考答案:
1. 启用 CBO(基于成本的优化):
spark.sql.cbo.enabled=true
spark.sql.cbo.joinReorder.enabled=true
spark.sql.statistics.histogram.enabled=true
2. 收集统计信息:
-- 表级别
ANALYZE TABLE order_info COMPUTE STATISTICS;
-- 列级别
ANALYZE TABLE order_info COMPUTE STATISTICS FOR COLUMNS user_id, pay_amount;
3. 谓词下推:
-- 错误:先 Join 再过滤
SELECT * FROM (
SELECT t1.*, t2.user_name
FROM order_info t1
JOIN user_info t2 ON t1.user_id = t2.user_id
) WHERE dt = '2026-03-24';
-- 正确:先过滤再 Join(Spark 会自动优化)
SELECT t1.*, t2.user_name
FROM order_info t1
JOIN user_info t2 ON t1.user_id = t2.user_id
WHERE t1.dt = '2026-03-24';
4. 使用合适的数据格式:
Parquet > ORC > Avro > JSON/CSV
- Parquet 支持列式存储
- 支持谓词下推
- 压缩比高
🏗️ 数仓建模设计题
题目 1:设计一个电商数仓
面试官要求:
- 画出分层架构
- 说明每层作用
- 给出关键表设计
参考答案:
1. 分层架构:
┌─────────────────────────────────────────────────────────────┐
│ ADS 应用层 │
│ GMV 大屏、用户留存报表、商品排行榜 │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ DWS 聚合层 │
│ 用户日汇总、商品日汇总、品类日汇总 │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ DWD 明细层 │
│ 订单事实表、用户维度表、商品维度表 │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ ODS 原始层 │
│ 业务数据库同步表(MySQL Binlog) │
└─────────────────────────────────────────────────────────────┘
2. 关键表设计:
-- 订单事实表(DWD)
CREATE TABLE dwd_fact_order (
order_id STRING COMMENT '订单 ID',
user_id STRING COMMENT '用户 ID',
product_id STRING COMMENT '商品 ID',
category_id STRING COMMENT '品类 ID',
city_id STRING COMMENT '城市 ID',
pay_amount DECIMAL(18,2) COMMENT '支付金额',
quantity INT COMMENT '数量',
order_status INT COMMENT '订单状态',
create_time TIMESTAMP COMMENT '下单时间',
pay_time TIMESTAMP COMMENT '支付时间',
dt STRING COMMENT '分区字段'
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET;
-- 用户维度表(DWD,SCD2)
CREATE TABLE dwd_dim_user (
user_key BIGINT COMMENT '代理键',
user_id STRING COMMENT '用户 ID',
user_name STRING COMMENT '用户名',
gender INT COMMENT '性别',
city_id STRING COMMENT '城市 ID',
user_level INT COMMENT '用户等级',
start_date STRING COMMENT '生效开始日期',
end_date STRING COMMENT '生效结束日期',
is_current INT COMMENT '是否当前版本'
)
STORED AS PARQUET;
-- 用户日汇总(DWS)
CREATE TABLE dws_user_daily_stat (
user_id STRING COMMENT '用户 ID',
stat_date STRING COMMENT '统计日期',
gmv DECIMAL(18,2) COMMENT 'GMV',
order_count INT COMMENT '订单数',
pay_amount DECIMAL(18,2) COMMENT '支付金额',
refund_count INT COMMENT '退款次数',
dt STRING COMMENT '分区字段'
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET;
题目 2:如何设计缓慢变化维?
参考答案:
1. SCD 类型选择:
| 类型 | 处理方式 | 适用场景 |
|---|---|---|
| Type 1 | 直接覆盖 | 纠错、不重要属性 |
| Type 2 | 新增行(推荐) | 用户等级、地址等 |
| Type 3 | 新增列 | 只保留上一版本 |
2. Type 2 实现:
-- 表结构增加时间范围
CREATE TABLE dwd_dim_user_scd2 (
user_key BIGINT,
user_id STRING,
user_level INT,
start_date STRING,
end_date STRING,
is_current INT
);
-- ETL 逻辑
-- 步骤 1:关闭旧记录
UPDATE dwd_dim_user_scd2
SET is_current = 0, end_date = '2026-03-23'
WHERE user_id IN (SELECT user_id FROM ods_user WHERE dt = '2026-03-24')
AND is_current = 1;
-- 步骤 2:插入新记录
INSERT INTO dwd_dim_user_scd2
SELECT
ROW_NUMBER() OVER () + max_key,
user_id,
user_level,
'2026-03-24' AS start_date,
'9999-12-31' AS end_date,
1 AS is_current
FROM ods_user
CROSS JOIN (SELECT MAX(user_key) AS max_key FROM dwd_dim_user_scd2);
💼 项目经验梳理
如何介绍项目?
STAR 法则:
S(Situation):背景
- 公司业务:电商平台,日订单 100 万
- 数据痛点:数据分散、口径不一、查询慢
T(Task):任务
- 搭建实时数仓
- 统一数据口径
- 提升查询性能
A(Action):行动
- 技术选型:Flink + Kafka + Doris
- 分层设计:ODS/DWD/DWS/ADS
- 性能优化:并行度调整、状态管理
R(Result):结果
- 数据延迟:从小时级降到秒级
- 查询性能:提升 10 倍
- 业务价值:支持实时运营决策
项目难点及解决
准备 3 个难点:
难点 1:数据倾斜
- 现象:热门用户数据量极大
- 解决:Key 加盐 + 两阶段聚合
- 效果:任务耗时从 2h 降到 20min
难点 2:数据一致性
- 现象:实时与离线数据不一致
- 解决:Exactly-Once + 去重逻辑
- 效果:准确率 99.9%+
难点 3:状态管理
- 现象:状态过大导致 OOM
- 解决:RocksDB + 状态 TTL
- 效果:稳定运行 30 天 +
🎯 行为面试题
"你的优缺点是什么?"
优点(结合岗位):
- 学习能力强:1 个月掌握 Flink 技术栈
- 责任心强:生产问题 30 分钟内响应
- 团队协作:主动分享技术文档
缺点(真实但可改进):
- 公开演讲紧张:正在通过内部分享锻炼
- 过于追求完美:学习时间管理,优先完成
"为什么离职?"
回答模板:
- 感谢前公司培养(感恩)
- 个人职业规划(发展)
- 寻求更大挑战(积极)
示例:
"感谢前公司给我成长的机会,但业务相对稳定,
我希望寻求更大挑战,接触更复杂的技术场景。"
"期望薪资多少?"
回答策略:
- 先了解岗位预算范围
- 给出区间而非具体数字
- 强调能力匹配
示例:
"我相信贵公司有成熟的薪酬体系,
根据我的经验和技术水平,期望在 X-Y 范围,
具体可以根据面试表现调整。"
📋 面试准备清单
技术准备
- SQL 窗口函数熟练
- Spark 调优原理
- 数仓分层设计
- 实时计算架构
- 项目难点梳理
项目准备
- STAR 法则整理
- 3 个技术难点
- 量化成果数据
- 架构图画出来
行为准备
- 优缺点回答
- 离职原因
- 职业规划
- 反问问题
📚 推荐资源
刷题平台
- LeetCode 数据库题(前 50 道)
- 牛客网 SQL 专项练习
- HackerRank SQL
技术博客
- Apache Spark 官方博客
- Flink 中文社区
- 美团技术团队
书籍推荐
- 《Spark 性能调优实战》
- 《数据仓库工具箱》
- 《流式架构原理与设计》
📌 总结
面试核心
1. SQL 是基础(窗口函数必考)
2. 调优是加分项(至少准备 3 个案例)
3. 项目要量化(用数据说话)
4. 表达要清晰(STAR 法则)
复习计划
| 时间 | 内容 |
|---|---|
| 第 1 周 | SQL 刷题(每天 5 道) |
| 第 2 周 | Spark/Flink 原理 |
| 第 3 周 | 项目梳理 + 模拟面试 |
| 第 4 周 | 行为面试 + 薪资谈判 |
💡 面试是双向选择,保持自信,展示真实的自己!