数据开发常问的技术性问题及解答/示例(附目录)
本文整理数据开发岗位面试及日常工作中高频技术问题,每个问题均提供解答要点 与代码/场景示例,覆盖数据建模、SQL优化、Hive/Spark、ETL调度、数据治理、实时计算等领域。
📑 目录
- 数据仓库建模
- 1.1 缓慢变化维度(SCD)类型2的实现
- 1.2 事实表的类型与选择
- 1.3 数仓分层架构及每层职责
- SQL开发与优化
- 2.1 分组取Top N(窗口函数)
- 2.2 连续登录天数计算
- 2.3 数据倾斜的定位与处理
- 2.4 大表Join大表优化策略
- Hive专题
- 3.1 Hive SQL执行流程
- 3.2 动态分区使用注意事项
- 3.3 Hive ACID事务机制
- Spark专题
- 4.1 Stage划分与宽窄依赖
- 4.2 Spark内存管理配置
- 4.3 动态资源分配原理与配置
- ETL与调度
- 5.1 全量同步与增量同步设计
- 5.2 数据回刷(回溯历史)方案
- 5.3 调度任务依赖、重试与告警配置
- 数据质量与治理
- 6.1 数据质量监控体系设计
- 6.2 数据血缘的采集与应用
- 实时计算与流批一体
- 7.1 Lambda架构 vs Kappa架构
- 7.2 Flink如何保证Exactly-Once语义
- 综合场景设计题
- 8.1 百亿级日志实时分析平台设计
1. 数据仓库建模
1.1 问题:什么是缓慢变化维度(SCD)?请用示例实现类型2(拉链表)。
解答要点:
- SCD指维度属性随时间变化的处理策略。
- 类型1:直接覆盖原值,不保留历史。
- 类型2:新增一行记录,通过
start_date、end_date、is_current标识有效时段。 - 类型3:增加"旧值"列,仅保留上一次变化。
示例:用户等级拉链表设计与每日更新
sql
-- 1. 创建拉链表
CREATE TABLE dim_user (
user_id BIGINT COMMENT '用户ID',
user_name STRING,
level STRING COMMENT '会员等级',
start_date DATE COMMENT '生效日期',
end_date DATE COMMENT '失效日期',
is_current STRING COMMENT 'Y=当前有效,N=历史'
) STORED AS ORC;
-- 2. 每日ETL:合并新增/变化数据
-- 假设源表为 ods_user(每日全量快照)
WITH
-- 标记需要关闭的旧记录(当前有效且等级发生变化)
to_close AS (
SELECT old.user_id
FROM dim_user old
JOIN ods_user new ON old.user_id = new.user_id
WHERE old.is_current = 'Y' AND old.level <> new.level
),
-- 生成新的当前记录
new_current AS (
SELECT
user_id, user_name, level,
current_date AS start_date,
'9999-12-31' AS end_date,
'Y' AS is_current
FROM ods_user
)
INSERT OVERWRITE TABLE dim_user
SELECT * FROM dim_user WHERE is_current = 'N' -- 保留已关闭历史
UNION ALL
SELECT user_id, user_name, level, start_date, end_date, is_current
FROM new_current
UNION ALL
-- 将需要关闭的旧记录的is_current改为'N', end_date改为昨天
SELECT
user_id, user_name, level, start_date,
date_sub(current_date, 1) AS end_date,
'N' AS is_current
FROM dim_user
WHERE user_id IN (SELECT user_id FROM to_close) AND is_current = 'Y';
1.2 问题:事实表有哪些类型?如何选择?
解答要点:
- 事务事实表:每行对应一个业务事件(如订单下单),粒度最细,支持任意维度分析。
- 周期快照事实表:定期记录状态(如每日账户余额),用于存量分析。
- 累积快照事实表:跟踪流程全生命周期(如订单从下单到签收),包含多个时间字段。
选择原则:
- 业务过程为事件型 → 事务事实表。
- 需要统计周期性状态(如月末库存)→ 周期快照。
- 需要跟踪流程完成率/耗时 → 累积快照。
示例:订单累积快照表
sql
CREATE TABLE fact_order_lifecycle (
order_id BIGINT,
user_id BIGINT,
create_time TIMESTAMP,
pay_time TIMESTAMP,
ship_time TIMESTAMP,
receive_time TIMESTAMP,
order_status STRING,
amount DECIMAL(10,2)
) PARTITIONED BY (dt STRING);
1.3 问题:数仓分层架构及每层职责?
解答要点:
- ODS(贴源层):原始数据,保持源系统结构,仅做简单清洗。
- DWD(明细层):对ODS进行维度退化、数据清洗、统一编码、事实拉宽。
- DWS(汇总层):按主题轻度聚合,构建宽表,以空间换时间。
- ADS(应用层):面向具体报表或服务,高度聚合。
分层价值:解耦ETL流程、复用公共计算、统一口径、控制权限。
2. SQL开发与优化
2.1 问题:如何实现分组取Top N(例如每个部门薪资最高的3人)?
解答要点 :使用窗口函数ROW_NUMBER()(无并列)、RANK()或DENSE_RANK()(处理并列)。
示例:
sql
SELECT dept, emp_name, salary
FROM (
SELECT dept, emp_name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM emp
) t
WHERE rn <= 3;
追问:
ROW_NUMBER与RANK区别?答:
ROW_NUMBER顺序唯一,并列时随机排序;RANK并列后续名次跳跃(如1,1,3)。
2.2 问题:如何计算用户的连续登录天数?
解答要点 :使用LAG获取上次登录日期,构建连续组标识,再分组统计。
示例:
sql
WITH login_seq AS (
SELECT user_id, login_date,
LAG(login_date) OVER (PARTITION BY user_id ORDER BY login_date) AS prev_date
FROM user_login_log
GROUP BY user_id, login_date -- 一天去重
),
group_flag AS (
SELECT user_id, login_date,
SUM(CASE WHEN DATEDIFF(login_date, prev_date) = 1 THEN 0 ELSE 1 END)
OVER (PARTITION BY user_id ORDER BY login_date) AS group_id
FROM login_seq
)
SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date,
COUNT(*) AS continuous_days
FROM group_flag
GROUP BY user_id, group_id
HAVING COUNT(*) >= 3; -- 筛选连续3天以上
2.3 问题:如何发现并处理数据倾斜?
解答要点:
- 发现 :查看任务日志中个别Reducer处理数据量远超平均;
EXPLAIN分析Join或Group By阶段。 - 处理 :
- MapJoin :小表广播(
hive.auto.convert.join=true)。 - 倾斜Key随机化:给热点Key加随机前缀打散。
- 两阶段聚合:先局部聚合,再全局聚合(适用于Group By倾斜)。
- 倾斜Join优化 :
hive.optimize.skewjoin=true,自动拆分倾斜Key。
- MapJoin :小表广播(
示例:倾斜Key随机化处理Join
sql
-- 原SQL(大表log与小表user join,user_id存在大量0值)
SELECT * FROM log a JOIN user b ON a.user_id = b.user_id;
-- 优化:将空值或热点Key随机打散
SELECT * FROM log a
JOIN user b
ON CASE WHEN a.user_id = 0 THEN CONCAT(0, '_', CAST(RAND()*100 AS INT))
ELSE a.user_id END = b.user_id;
2.4 问题:大表Join大表如何优化?
解答要点:
- 分桶预Join:两表按Join Key分桶,且桶数成倍数,可避免Shuffle。
- 倾斜Key单独处理:将热点Key与非热点Key分开Join,最后Union。
- 使用Bloom Filter:预先过滤大表不存在于另一大表的数据。
- 转换为MapJoin:如果数据经过过滤后小表变小,动态转为MapJoin。
示例:分桶表设计
sql
-- 建表时指定分桶
CREATE TABLE t1 (id INT, val STRING) CLUSTERED BY (id) INTO 16 BUCKETS;
CREATE TABLE t2 (id INT, info STRING) CLUSTERED BY (id) INTO 16 BUCKETS;
-- 两表分桶列相同且桶数成倍数,Join时无需Shuffle
SELECT * FROM t1 JOIN t2 ON t1.id = t2.id;
3. Hive专题
3.1 问题:Hive SQL的执行流程是怎样的?
解答要点:
- Parser:将SQL解析为抽象语法树(AST)。
- Semantic Analyzer:结合元数据(Metastore)进行语义分析,生成逻辑计划(算子树)。
- Logical Optimizer:应用规则优化(如谓词下推、投影裁剪)。
- Physical Planner:将逻辑计划转换为物理计划(MapReduce/Tez/Spark任务DAG)。
- Physical Optimizer:物理层面优化(如MapJoin选择、并行度调整)。
- Execution:提交作业到执行引擎运行。
示例 :通过EXPLAIN查看执行计划。
sql
EXPLAIN EXTENDED SELECT COUNT(*) FROM orders WHERE dt='2026-04-01';
3.2 问题:动态分区使用时有哪些注意事项?
解答要点:
- 注意事项 :
- 必须设置
hive.exec.dynamic.partition.mode=nonstrict。 - 避免分区字段基数过高(如user_id),否则会产生成千上万个小分区。
- 限制分区数:
hive.exec.max.dynamic.partitions(默认1000),hive.exec.max.dynamic.partitions.pernode。 - 最后一个SELECT字段的顺序必须与分区字段一致。
- 数据倾斜风险:如果分区值分布不均,可能导致某分区数据量巨大。
- 必须设置
示例:
sql
SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
SET hive.exec.max.dynamic.partitions=10000;
INSERT OVERWRITE TABLE sales PARTITION(dt)
SELECT product, amount, sale_date AS dt -- dt放在最后
FROM raw_sales;
3.3 问题:Hive的ACID特性如何实现?有何限制?
解答要点:
- 实现机制 :基于基础文件 (base file)+增量文件 (delta file),通过Compactor定期合并。
- 必要条件 :
- 表格式为
ORC。 - 表属性
transactional=true。 - 分桶表(
CLUSTERED BY)。 - 开启并发支持:
hive.support.concurrency=true。
- 表格式为
- 限制:不支持外部表;性能有一定损耗;跨分区更新效率较低。
示例创建ACID表:
sql
CREATE TABLE acid_table (
id INT, name STRING
) CLUSTERED BY (id) INTO 4 BUCKETS
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
4. Spark专题
4.1 问题:Spark中Stage如何划分?宽依赖与窄依赖区别?
解答要点:
- 窄依赖 :父RDD每个分区最多被子RDD一个分区使用(如
map、filter)。无需Shuffle,可在一个Stage内完成。 - 宽依赖 :父RDD一个分区被子RDD多个分区使用(如
reduceByKey、join)。需要Shuffle,划分新Stage。 - Stage划分:从行动算子倒推,遇到宽依赖则切分Stage。
示例:通过代码查看DAG。
scala
val rdd = sc.textFile("hdfs://...")
.flatMap(_.split(" "))
.map((_,1))
.reduceByKey(_+_) // 宽依赖,切分Stage
.filter(_._2 > 10) // 窄依赖,同Stage
4.2 问题:Spark内存管理如何配置?Execution和Storage内存如何分配?
解答要点:
- 统一内存管理(Spark 1.6+):内存分为
Execution(Shuffle、Join、排序)和Storage(缓存),可动态抢占。 - 关键参数:
spark.memory.fraction:默认0.6,用于Execution+Storage,剩余0.4用于用户代码。spark.memory.storageFraction:默认0.5,Storage固定占Execution+Storage的50%,Execution不足时可强行借用Storage内存。
- 调优建议 :频繁GC时可降低
spark.memory.fraction;缓存多则提高spark.memory.storageFraction。
示例配置:
sql
SET spark.memory.fraction=0.7;
SET spark.memory.storageFraction=0.3;
4.3 问题:Spark动态资源分配原理及配置?
解答要点:
- 原理:根据任务负载动态增减Executor。当有等待任务时申请新Executor;空闲超时时释放Executor。
- 必要条件 :
- 启用
spark.dynamicAllocation.enabled=true。 - 启用外部Shuffle服务:
spark.shuffle.service.enabled=true(保存Executor shuffle数据)。
- 启用
- 参数 :
spark.dynamicAllocation.minExecutors/maxExecutorsspark.dynamicAllocation.initialExecutorsspark.dynamicAllocation.executorIdleTimeout(默认60s)
示例:
sql
SET spark.dynamicAllocation.enabled=true;
SET spark.shuffle.service.enabled=true;
SET spark.dynamicAllocation.minExecutors=2;
SET spark.dynamicAllocation.maxExecutors=100;
5. ETL与调度
5.1 问题:如何设计全量同步与增量同步?如何保证一致性?
解答要点:
- 全量同步:适合小表或首次初始化,每日覆盖目标分区。
- 增量同步:依赖源表时间戳字段或CDC(Canal读取binlog)。
- 一致性保证 :
- 事务表 :使用分区覆盖写入(
INSERT OVERWRITE),失败则回滚分区。 - 拉链表:采用"双表切换"或基于ACID表的Merge操作。
- 端到端校验:每日对比源端与目标端记录数、关键指标。
- 事务表 :使用分区覆盖写入(
示例:基于时间戳的增量抽取:
sql
-- 假设源表有updated_time,目标表记录上次同步时间
INSERT INTO target_table
SELECT * FROM source_table
WHERE updated_time > '${last_sync_time}'
AND updated_time <= '${current_sync_time}';
5.2 问题:数据回刷(回溯历史)方案设计?
解答要点:
- 按分区回刷:对于分区表,直接覆盖指定分区数据。
- 拉链表的版本回滚 :保留历史数据,通过修改
end_date恢复。 - Lambda架构:流处理实时结果+批处理重算历史,合并输出。
- 工具支持:使用支持时间旅行的表格式(Iceberg/Hudi),可查询历史快照。
示例:覆盖历史分区(Hive):
sql
-- 重新计算2024-01-01至2024-01-31的汇总数据
INSERT OVERWRITE TABLE dws_sales PARTITION(dt='2024-01-15')
SELECT ... FROM dwd_detail WHERE dt BETWEEN '2024-01-01' AND '2024-01-31';
5.3 问题:调度任务如何设置依赖、重试和告警?
解答要点:
- 依赖 :DAG设计,上游任务成功触发下游。常用调度器:Airflow(
>>)、DolphinScheduler(前置任务)。 - 重试 :设置重试次数(
retries)和重试延迟(retry_delay),采用指数退避策略。 - 告警:任务失败、超时、数据质量异常时触发。支持钉钉/邮件/Webhook。
示例:Airflow DAG定义:
python
default_args = {
'retries': 3,
'retry_delay': timedelta(minutes=5),
'email_on_failure': True,
'email': ['data@example.com']
}
task1 = BashOperator(task_id='extract', bash_command='...', dag=dag)
task2 = BashOperator(task_id='transform', bash_command='...', dag=dag)
task1 >> task2 # 依赖关系
6. 数据质量与治理
6.1 问题:如何设计数据质量监控体系?
解答要点:
- 完整性:检查关键字段空值率、分区数据量波动。
- 准确性:抽样比对源系统与数仓结果,或计算业务指标(如总金额)与源系统对账。
- 一致性:跨表关联字段一致性(如用户ID在订单表与用户表是否都存在)。
- 及时性:监控数据产出延迟(分区产生时间 vs 调度时间)。
- 唯一性:主键重复检测。
示例:自定义质量校验SQL:
sql
-- 检查订单表当天分区数据量是否在合理范围(历史均值±30%)
WITH stats AS (
SELECT AVG(cnt) AS avg_cnt, STDDEV(cnt) AS std_cnt
FROM (SELECT COUNT(*) AS cnt FROM orders WHERE dt < '2026-04-16' GROUP BY dt) h
)
SELECT CASE WHEN ABS(cnt - avg_cnt) > 3*std_cnt THEN '异常' ELSE '正常' END AS status
FROM (SELECT COUNT(*) AS cnt FROM orders WHERE dt = '2026-04-16') t, stats;
6.2 问题:数据血缘如何采集和应用?
解答要点:
- 采集方式 :
- 解析SQL:使用Antlr或Hive Lineage工具(如Apache Atlas)解析执行计划。
- 运行时Hook:Hive Hook或Spark Listener监听作业,记录输入输出表。
- 调度系统日志:从Airflow/DolphinScheduler任务上下文中解析。
- 应用场景 :
- 影响分析:上游表变更时快速找出下游依赖。
- 问题排查:数据异常时逆向溯源。
- 合规审计:追踪敏感数据流向。
示例:Atlas血缘展示(无需代码,展示概念):
- 用户通过Atlas UI可看到表A → 转换任务 → 表B的完整链路,点击表可查看所有上下游。
7. 实时计算与流批一体
7.1 问题:Lambda架构与Kappa架构的区别及选型?
解答要点:
- Lambda:同时维护批处理层和流处理层,结果合并。优点:高准确度,历史数据可重算;缺点:维护两套代码,逻辑可能不一致。
- Kappa:只用流处理,历史数据通过重放Kafka消息重新计算。优点:代码统一,运维简单;缺点:消息存储成本高,对回溯时间窗口有限制。
- 选型:需要高准确性且历史数据量巨大 → Lambda;事件溯源方便,能接受有限回溯窗口 → Kappa。
7.2 问题:Flink如何保证Exactly-Once语义?
解答要点:
- 核心机制:轻量级分布式快照(Checkpoint) + 两阶段提交(2PC)Sink。
- 流程 :
- 周期性插入Barrier到数据流中。
- 算子接收到Barrier后快照状态。
- 下游Sink在Checkpoint完成前预提交事务,等待JobManager通知。
- 所有算子快照成功后,通知Sink提交事务。
- 必要条件:Source支持数据重放(如Kafka),Sink支持事务(如Kafka、JDBC)。
示例:Flink Kafka端到端Exactly-Once配置:
java
// 开启Checkpoint
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
// Kafka Source
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("...")
.setTopics("input")
.setStartingOffsets(OffsetsInitializer.earliest())
.setDeserializer(new SimpleStringSchema())
.build();
// Kafka Sink with exactly-once
KafkaSink<String> sink = KafkaSink.<String>builder()
.setBootstrapServers("...")
.setRecordSerializer(KafkaRecordSerializationSchema.builder()
.setTopic("output")
.setValueSerializationSchema(new SimpleStringSchema())
.build())
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
.build();
8. 综合场景设计题
8.1 问题:设计一个数据开发任务,每天处理100亿条日志,要求支持近实时(5分钟延迟)分析。
解答要点:
整体架构:
- 采集层:Nginx日志 → Kafka(分区数≥日志源分区)。
- 存储层:Kafka保留7天,同时写入HDFS/Hudi按小时分区。
- 实时处理:Flink消费Kafka,做窗口聚合(5分钟滚动窗口),结果写入Redis/ClickHouse。
- 批处理:Spark每2小时重跑上一时段数据,与实时结果合并,修正误差。
- 服务层:ClickHouse存储聚合结果,支持OLAP查询;Redis存储最新TopN等。
关键设计:
- 反压控制:Flink设置水位线,Kafka限流。
- 去重:使用事件唯一ID + Redis BloomFilter或利用Flink状态去重。
- 延迟数据处理:设置允许延迟1小时,迟到数据更新到侧输出流,异步合并。
示例:Flink 5分钟窗口聚合代码片段
java
DataStream<LogEvent> stream = env.addSource(kafkaSource);
stream
.keyBy(e -> e.getUserId())
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.allowedLateness(Time.minutes(1))
.aggregate(new CountAgg(), new WindowResult())
.addSink(clickHouseSink);
以上覆盖了数据开发岗位的核心技术问题,每个问题提供了可直接参考的解答要点与示例。建议结合自身项目经历,将示例改造为具体业务场景的答案。