简介:这是一份面向实时数仓岗位的PDF面试题整理,围绕数仓理论、MapReduce、HIVE、SQL、Kafka等高频率考查方向展开,适合正在准备大数据开发或数据仓库面试的工程师对照复习。资源仅1个PDF文件,压缩包约89KB,轻量易下载。目前已有623人学习过。内容覆盖星型模型与雪花模型的优缺点及数仓选型理由、数仓分层与实时数仓方案、MapReduce全流程与HDFS写入机制、数据倾斜与小文件解决方案、Hive文件格式对比、HQL转MR原理、Kafka offset精确一次消费、SQL子句执行顺序及grouping sets/cube/rollup聚合用法;还补充了会员充值表生成30天明细、埋点日志在线时长等现场SQL例题,以及数据异常排查、质量保障、调度交接等开放型应答思路,适合面试前系统查漏补缺。
1. 数仓面试题汇总 PDF:为什么建议你把它当复习主线过一遍
这份数仓面试题汇总 PDF,与其说是题目合集,不如说是一张数仓岗位的能力体检表。它把星型模型、雪花模型、MapReduce 全流程、Hive 数据倾斜、Kafka offset 管理、SQL 执行顺序这些硬知识点串成了一条线,覆盖的正是数据开发面试里出现频率最高、也最容易翻车的那几类问题。适合准备跳槽的数据开发、刚转数仓的后端工程师,以及想系统补一遍底子的在校生。我带的学弟就是照着这份清单逐题复盘,把原来含糊的概念一个个钉死,两周后拿到了两个数仓 offer。这份资源不是标准答案,但把它吃透,你至少能在面试里把「能不能干活」这件事讲清楚。
2. 数仓理论:星型与雪花模型的选型逻辑,以及分层架构有什么坑
2.1 星型模型和雪花模型:冗余换查询,还是规范化换存储
面试官问星型模型和雪花模型的区别,真正想听的不是定义,而是你在业务里怎么权衡。星型模型是中心辐射结构,一张事实表挂多张维表,维表不做二次拆分;雪花模型把维表继续规范化,拆成二级甚至三级维表。这个区别听起来简单,但往深里说有三个层面的差异:冗余度、查询路径长度、维护成本。
星型模型冗余高,但查询路径短。用户维度表里直接放城市和省份是一种冗余,但也正因为这种冗余,你在做GROUP BY 省份时不需要再 join 一张城市维表。雪花模型把省份拆成独立维表,冗余少了、存储省了,但每多一层 join,查询的解析成本、shuffle 的数据量、任务失败的概率都上去了。实际业务里纯雪花极少见,因为数仓的核心矛盾永远是查询性能而不是存储成本,磁盘比 CPU 便宜得多。这一点在面试时可以直接说出来,能区分你是背过定义还是真在业务里选过型。
| 对比维度 | 星型模型 | 雪花模型 |
|---|---|---|
| 表结构 | 事实表 + 一层维表 | 事实表 + 多层维表 |
| 冗余度 | 高 | 低 |
| 查询路径 | 短,join 少 | 长,join 层级多 |
| 维护成本 | 低,维度变更集中在一张表 | 高,子维表之间有依赖 |
| 典型场景 | OLAP、报表引擎加速 | 存储成本敏感、规范化的 ODS 层 |
提示:回答时不要停在「雪花模型冗余少」,补一句「但它引入了额外的 join 开销,OLAP 场景下通常不划算」,这题的深度立马上来。
2.2 你们数仓用什么模型:星型、退化维度还是 Kappa 架构
「你们数仓用了什么模型,为什么」这道题的隐藏考点,是看你有没有真实落地过,而不是背概念。我见过很多人回答「我们用的星型模型」,被追问一句「为什么不用雪花」当场卡住。合格的答法是按分层说:ODS 层原样同步业务库;DWD 层做清洗和维度退化,把多级类目用退化维度压平到事实表;DWS 层按用户、商品、交易主题做轻度汇总;ADS 层面向报表。这样回答的好处是,面试官能顺着你的分层往下追问,而每一层你都有具体的建模依据。
如果追问到实时数仓,你需要能说清 Lambda 和 Kappa 的区别。Lambda 是离线批处理和实时流处理两条链路并行,最终结果合并,好处是稳定、可回溯,坏处是要维护两套口径,开发成本和一致性风险都高。Kappa 只用一套实时流处理链路,日志全量进 Kafka,由 Flink 重放实现历史回刷,好处是架构简单,坏处是消息积压时重放成本很高,对 Kafka 的保留期配置要求苛刻。面试里比较稳的答法是:「离线用 Hive 数仓,实时链路用 Flink + Kafka 走 Kappa 架构,核心指标最终口径以离线为准,实时链路做一致性校验。」既展示了架构视野,也没回避一致性问题。
2.3 数仓结构层次:ODS、DWD、DWS、ADS 的职责边界
「说说你们数仓的结构层次」,考察的是你有没有完整的数仓建设视角。标准分层是 ODS、DWD、DWS、ADS 四层,加一个公共维度层 DIM。ODS 层原样接入业务数据,只做时间分区和增量/全量同步,不做业务逻辑加工;DWD 层做清洗、去重、维度退化、规范化,粒度必须与业务过程保持一致;DWS 层按主题做轻度汇总,粒度通常是天;ADS 层面向报表和应用,只负责输出结果。
各层职责边界最容易犯的错,是把报表口径的过滤条件直接写在 ODS 层的抽取脚本里,或者让 ADS 层直接读取 ODS 的明细表。我在实际项目里守一条原则:ODS 只做物理映射,DWD 只做过程建模,DWS 只做汇总,ADS 只做输出。数据出问题时,顺着分层能快速定位是抽数环节、清洗环节还是汇总环节的问题,而不是翻遍几十个脚本去猜。这道题还有一个加分答法:主动提一句「所有层都建立在统一的维度建模规范上,避免各层各建一套维度表」,面试官会认为你有平台化思维。
3. MapReduce 和 HDFS:全流程拆解、Map 数与 Reduce 数怎么定
3.1 MapReduce 全流程:把 Shuffle 讲透,面试就成功了一半
MapReduce 全流程题基本必考,面试官通常直接说「重点讲 shuffle」。完整链路是:InputFormat 读取数据、InputSplit 切分分片、RecordReader 解析成 key-value、Map 函数处理、环形缓冲区写入、分区、排序、溢写、合并、压缩、Reduce 端拉取、归并排序、Reduce 函数处理、OutputFormat 写出。这条链路里最值得展开的就是 shuffle,它分两个阶段。
Map 端的 shuffle 从环形缓冲区开始:Map 输出写入默认 100MB 的环形缓冲区,达到 80% 阈值触发溢写,溢写前先按分区和 key 排序,多次溢写产生多个小文件后再做合并。Reduce 端分 copy、merge、sort 三个阶段:copy 阶段从多个 Map 任务并行拉取数据,merge 阶段做归并,sort 阶段保证进入 Reduce 的数据按 key 有序。面试官说「越细越好」时,你要说出这几个参数:环形缓冲区大小mapreduce.task.io.sort.mb、溢写阈值mapreduce.map.sort.spill.percent(默认 0.8)、是否开启 Combine(mapreduce.map.combine.class),才算真的细。
讲完流程后可以补一句:「shuffle 是数据倾斜和 OOM 的高发区,Hive 的倾斜大概率出在这里。」一句话把 MapReduce 和 Hive 两道题串起来,面试官会认为你有全局观,而不是一个一个知识点硬背。
3.2 Map 数与 Reduce 数:不是越多越快,背后是切分算法
Map 个数由输入分片数决定,分片大小由 FileInputFormat 的切分算法给出。核心参数是mapreduce.input.fileinputformat.split.minsize(默认 0)和split.maxsize(默认 Long.MAX_VALUE),配合文件的块大小计算。切分逻辑大致如下:
// 伪代码:FileInputFormat 计算分片大小的核心逻辑 // 默认时 blockSize 为 128MB,minSize 为 0,maxSize 为 Long.MAX_VALUE long splitSize = Math.max(minSize, Math.min(maxSize, blockSize)); // 如果配置了期望的 Map 数,goalSize = 文件总大小 / 期望 Map 数 // 分片大小会变成 Math.max(minSize, Math.min(goalSize, maxSize))参数说明:minSize调大可以让每个分片更大,从而减少 Map 数;maxSize调小会把大文件切成更多分片,增加 Map 数。当文件小于 128MB 且没设 maxSize 时,整个文件作为一个分片,这就是为什么大量小文件会制造大量 Map 任务的原因。
Reduce 个数由mapreduce.job.reduces直接控制,默认是 1。但有一个前提容易被忽略:如果 Mapper 里自定义了 Partitioner,分区数与 Reduce 数必须对齐,否则 Reduce 端数据严重不均。另外setNumMapTasks这个方法很多人误用,它只是一个建议值,真正生效的永远是切分算法算出来的分片数。
3.3 FileInputFormat 切分细节与 HDFS 写入流程
FileInputFormat 里边有个SPLIT_SLOP = 1.1机制:当文件剩余部分不足 1.1 倍分片大小时,不再切分,剩余部分整体作为一个分片。比如 300MB 的文件、128MB 块大小,切出来是 3 个分片而不是 2 个整片加一个几 KB 的碎片。这个细节一般面试官不会主动问,你说出来就是加分项。
HDFS 写入流程是另一道高频题,完整链路是:客户端向 NameNode 发起写入请求,NameNode 检查权限和路径后返回可写状态;客户端按块大小切分文件,向 NameNode 申请 DataNode 列表;数据写入第一个 DataNode,再以 pipeline 方式传给第二个、第三个;每个块写完后最后一个 DataNode 向客户端返回 ACK,客户端向 NameNode 汇报块完成。如果写入过程 DataNode 宕机或网络超时,pipeline 会重建,已写块不用重传,这个断点续传机制由 DFSClient 实现。
注意:副本放置策略是「第一副本在客户端所在节点,第二副本在同一机架不同节点,第三副本在不同机架」,这样设计是为了同时兼顾写入带宽和机架容灾。这四个字「机架感知」说出来,面试官会觉得你是写过数据的人。
4. Hive 高频题:数据倾斜、小文件、文件格式与 HQL 转 MR
4.1 数据倾斜:先定位再对症下药,别上来就背方案
数据倾斜是 Hive 面试最经典的题,问法基本都是「你有没有遇到过,怎么解决的」。按「现象、定位、解决」的顺序讲:现象是某个 Reduce 长时间卡在 99%,或任务跑几小时不结束;定位是看 YARN 上各 container 的耗时分布,以及 task 日志里某几个 task 处理的数据量远大于其他;常见原因是 key 分布不均,比如空值过多、热点 key 集中、join 时小表 key 大量重复。
解决路径有四条。第一条,Map 端聚合:SET hive.map.aggr=true;让相同 key 在 Map 端先合并一次,减少 Reduce 压力。第二条,SkewJoin:SET hive.optimize.skewjoin=true;Hive 自动检测倾斜 key 并拆分为两个子任务。第三条,加盐:把热点 key 拆散成多个带随机后缀的 key,最后去盐聚合。第四条,过滤异常 key,空值和默认值这类无意义倾斜源在 where 里直接排除。这里有个血泪经验:数据量不大也会倾斜,key 基数小(比如性别只有两个值)照样倾斜,所以判断的标准不是数据量,而是 key 的分布曲线。
提示:面试官追问「加盐之后怎么保证结果正确」,你要接得上「加盐前先保留一份真实数据,加盐后用 union all 把常规结果合并回来」——这是加盐方案的完整闭环,也是实战和背答案的分水岭。
4.2 小文件问题:合并只是手段,存储格式才是根治办法
小文件问题的本质不是存储空间,而是「文件数太多、每个文件太小」带来的 NameNode 内存压力和任务调度开销。小文件来源有三个:动态分区插入、Reduce 个数过多、shuffle 输出碎片化。解决要从两个层面组织:存储层用 ORC 或 Parquet 列式存储,自带压缩和索引,大幅降低查询扫描量;调度层设置SET hive.merge.mapfiles=true;和SET hive.merge.mapredfiles=true;让 Map 端和 Reduce 端在输出时自动合并小文件。
已经产生的小文件,用INSERT OVERWRITE重写一次表,让任务在写入过程中完成合并。项目里我一般把hive.merge.smallfiles.avgsize设成 128MB,小于平均值的输出文件都会被并到目标大小。注意hive.merge.size.per.task控制单次合并任务能处理的总数据量,调太小合并任务变多,调太大可能 OOM。
4.3 Hive 文件格式选型:TEXTFILE、SequenceFile、ORC、Parquet
文件格式这道题,面试官想看到的是你对存储格式底层机制的理解。TEXTFILE 是默认格式,纯文本存储,好处是通用、可读、能直接 LOAD DATA 加载外部文件,坏处是逐行全量解析,空间占用最高。SequenceFile 是 Hadoop 原生二进制格式,支持记录级和块级压缩,空间优于文本,但查询效率提升有限,不支持列裁剪。ORC 是 Hive 生态最推荐的列式存储,每个 stripe 带索引,支持谓词下推和矢量化查询,压缩率高、扫描快。Parquet 和 ORC 类似,也是列式存储,但跨框架支持更好,Spark 生态里更常见。
| 格式 | 存储类型 | 压缩支持 | 列裁剪 | 查询性能 | 适用场景 |
|---|---|---|---|---|---|
| TEXTFILE | 行式文本 | 外部压缩 | 不支持 | 低 | 临时表、外部数据加载 |
| SequenceFile | 行式二进制 | 记录/块级 | 不支持 | 中 | Hadoop 原生任务 |
| ORC | 列式 | ZLIB/Snappy | 支持 | 高 | 核心事实表、汇总表 |
| Parquet | 列式 | Snappy | 支持 | 高 | 与 Spark 跨框架共享 |
真实项目里几乎不用 TEXTFILE 存核心表,它只是数据接入的临时载体。核心表和汇总表用 ORC + Snappy 压缩,ODS 层从业务库同步的数据偶尔用 Parquet 方便与 Spark 共享。面试时多说一句「ORC 默认用 ZLIB 压缩,改成 Snappy 可以平衡压缩率和解压速度」,这道题的深度就不一样了。
4.4 一条 HQL 怎么变成 MapReduce:优化器起了什么作用
一条 HQL 的旅程大致是:Antlr 解析成 AST → 生成逻辑计划 → 经过谓词下推、列裁剪、分区裁剪等优化 → 生成物理计划 → 编译成 MapReduce 或 Spark 任务。这里有个面试官爱追问的点:不是所有 HQL 都会变成 MapReduce。简单的SELECT * FROM table WHERE dt='2024-01-01'在开启hive.fetch.task.conversion=more时走的是本地抓取模式,根本不会提交 YARN 任务。
优化器里值得展开讲的是谓词下推和列裁剪:WHERE dt='2024-01-01'会被下推到表扫描阶段,避免全表数据进入 Map 端;SELECT a, b会被列裁剪成只读这两列,在 ORC、Parquet 下效果尤其明显。再主动举一个 JOIN 的例子:「两表 JOIN 时,Hive 默认把小表放进 Map 端缓存做 MapJoin,避免 Reduce 阶段的 shuffle」——这说明你理解执行计划,而不是只背了流程。
5. 避坑指南:SQL 与 Kafka 考点里最容易翻车的 5 个细节
5.1 充值表 30 天价格明细:日期基准算错,整张表废掉
- 现象:用
date_add(charge_date, pos)生成明细时,充值当天被漏掉,或者跨月的日期对不上,结果和业务预期差一天。 - 原因:没有明确「后 30 天」的起算点。
date_add(..., 0)返回的是当天,很多人默认把它当成第 1 天,结果整个序列整体偏移一天。 - 解决:用 POSEXPLODE 生成序号,并把起算规则显式写清楚。代码参考如下:
-- 根据用户充值记录生成未来 30 天的价格明细 -- 充值当天算第 0 天;如果要充值次日起算,把 space(29) 改成 space(28) SELECT user_id, date_add(charge_date, pos) AS dt, 30.0 / 30 AS price_per_day FROM ( SELECT user_id, charge_date FROM recharge_table WHERE charge_date = '2020-03-01' -- 示例:本次只处理这一天的充值记录 ) t LATERAL VIEW POSEXPLODE(split(space(29), ' ')) pe AS pos, dummy;逻辑说明:split(space(29), ' ')生成 30 个空格组成的数组,POSEXPLODE把它展开成序号 0 到 29,date_add算出每一天的日期。参数说明:space(29)对应 30 天,如果要「充值次日起算」,改成space(28)。这种边界差异改需求时最容易翻车,写完后把首尾两条记录拉出来人工核对一遍是值得的。还要注意,这里按 30 天均摊每天 1 元;如果业务要求按自然月天数均摊,需要先用LAST_DAY(charge_date)算出当月实际天数再除,这是面试官常追问的变体。
5.2 SQL 执行顺序:HAVING 里引用 SELECT 别名直接报错
- 现象:在 HAVING 里写
HAVING total_cnt > 100,total_cnt 是 SELECT 中的别名,Hive 直接报「Invalid table alias or column reference」。 - 原因:语义执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT,HAVING 在 SELECT 投影之前执行,看不到 SELECT 里新起的别名。
- 解决:HAVING 里用原始聚合表达式,或者用子查询包一层再过滤。
这个顺序还解释了另一个高频错误:WHERE 里使用COUNT(*)是无效的,因为 WHERE 执行时聚合还没发生。理解了执行顺序之后,GROUPING SETS、ROLLUP、CUBE 的用法也会顺理成章:它们生成的 NULL 表示「该行不包含这个维度」,业务字段本身有 NULL 时必须用GROUPING__ID区分,这也是一个容易答漏的点。一个实用的习惯是:写 SQL 前先在脑子里过一遍执行顺序,把聚合条件用 WHERE 或子查询前置,不要在 HAVING 里做临时换算,这样既避免报错,也让执行计划更清晰。
5.3 行转列、列转行:COLLECT_SET 不保证顺序
- 现象:行转列后同一分组里的明细顺序乱掉,拼接出的字符串和业务预期不一致。
- 原因:
COLLECT_SET基于集合实现,天然不保证顺序;COLLECT_LIST在 Reduce 端并行处理时顺序也不稳定。 - 解决:先对排序字段在子查询里排好序,再在外层聚合。要保留重复值就别用
COLLECT_SET,改用COLLECT_LIST。
列转行相对简单:LATERAL VIEW EXPLODE把数组或 Map 展开成多行。注意 explode 后行数是数组长度的倍数,如果原表还有其他字段,必须先限制数据量再展开。一个含 100 个元素的数组会把千万级表扩成十亿行,这种场景我在真实项目里见过一次,直接把队列跑挂了。
5.4 Kafka offset:自动提交容易丢,手动提交又怕重复
- 现象:消费者重启后,一批消息被消费两次,或一批消息永远消失,下游报表对不上。
- 原因:
enable.auto.commit=true时提交时机不受业务控制。业务处理前提交,崩溃就丢;业务处理后还没提交,重启就重复消费。 - 解决:改手动提交,
commitSync()保证提交成功后再处理下一批,commitAsync()配合回调记录失败;下游保留幂等消费能力,比如用唯一键去重。
实时数仓场景里,我一般用 Flink 连接 Kafka 时开启 checkpoint,让 Flink 统一管理 offset。这个方案的优点是 offset 提交时机由 checkpoint 决定,业务失败重试不会出现重复或丢失。面试时如果被问到「有没有手动管过 offset」,答「我用 Flink 接管了 offset 管理并开启 exactly once」比答「我手动调用 commitSync」更能体现实时数仓的实战经验。
5.5 大表全局排序:ORDER BY 直接把集群跑死
- 现象:对千万级大表执行
ORDER BY,任务长时间卡住甚至 OOM。 - 原因:
ORDER BY保证全局有序,Hive 只能用单个 Reducer 接收全部数据,数据量一大必然崩溃。 - 解决:按业务诉求拆分——分区内有序用
SORT BY;控制数据分布用DISTRIBUTE BY;确实要全局有序但数据太大,先按月份或业务维度分桶,桶内排序后拼接。
这道题的隐藏考点是区分ORDER BY和SORT BY:全局有序 vs 分区内有序。大数据量下全局排序基本不可接受,业界常用「分桶 + 桶内排序 + 结果合并」把全局排序降维成分区问题,这句话可以作为这题的收尾。
6. 开放题应答套路:把「数据异常排查」答成加分项
6.1 报表数据异常:先止血,再定位,最后补防
「某一天你的报表数据异常,你会怎样解决」表面考排查能力,实际考处置顺序。我的套路是三步走:第一步看任务状态,去调度平台确认是否失败、依赖表是否产出;第二步看数据源,确认上游业务库有没有变更、同步有没有中断;第三步做数据质量核查,对比昨天的数据量、空值率、去重率,快速圈出异常范围。回答这类题要带上具体指标,比如「我先看今天的 GMV 环比是否断崖式下跌,再顺着查调度日志」,这比「我会先查日志」这种空话强得多。
6.2 数据质量:把测试流程做成自动化防线
数据质量保证的答案是分层校验:ODS 层做数据量校验和主键唯一性校验,DWD 层做一致性校验,DWS 层做指标合理性校验(GMV 不为负、转化率不大于 1),ADS 层做报表逻辑校验。我在项目里会把校验 SQL 写成脚本,挂在调度任务之后,异常自动告警到工作群。这套思路的得分点不在「我们有测试流程」这几个字,而在于「我有一套具体的校验规则和载体」,面试官一听就知道你做过真实的数据保障。
6.3 调度交接与角色边界:系统大于个人
人员流动时调度任务的处理,核心是「先文档化,再平台化」。文档化是把任务血缘、依赖表、调度频率、负责人写进数据字典;平台化是把任务统一托管到 Airflow、DolphinScheduler,配置负责人和告警人双重绑定,而不是挂在一台个人电脑上跑 crontab。这道题的本质是工程意识:任何个人维护的任务最终都要变成系统的一部分。从那以后,我每次接手别人的调度任务,第一件事永远是补齐血缘图谱和数据字典,再去看任务代码本身,这个习惯帮我避开了太多半夜告警的坑,希望帮到你。
本文还有配套的精品资源,点击获取