1. 这套笔试卷到底在考什么
每年秋招季,B站的数据开发岗位笔试题都会被很多人拿出来反复刷。2020年这套卷子已经算是这类岗位笔试里的一个典型样本了,即便放到现在来看,它的考点分布和出题思路依然有很强的参考价值。我陆陆续续带过几个实习生,也帮他们修改过简历和模拟面试,普遍反馈是:这套卷子不像部分互联网大厂那样偏向纯算法题,而是更看重你对数据开发整个链条的理解是否完整。
数据开发方向和其他技术岗有一个本质区别:它不是一个单纯的“编程岗位”,而是横跨数据采集、数据清洗、数据建模、数据计算、数据服务的综合性岗位。笔试如果只靠背SQL语法,或者只靠刷LeetCode,很容易在某几个特定模块上翻车。B站的这套笔试卷(二)基本就把这个问题暴露得很彻底。
先给一个整体定位。这套笔试卷的题型大致可以分为四类:
| 题型类别 | 考查方向 | 题目占比 |
|---|---|---|
| SQL编写与优化 | 数据提取、聚合计算、窗口函数、join优化 | 约35% |
| 大数据组件原理 | HDFS、MapReduce、Spark、Hive等核心机制 | 约25% |
| 数据仓库建模 | 维度建模、分层架构、事实表和维度表设计 | 约20% |
| 算法与数据结构 | 常见算法题、离线计算/实时计算中涉及的算法思路 | 约20% |
为什么是这个分布?因为B站的数据开发团队日常处理的核心场景就是:内容生态分析(视频投稿、播放、弹幕、评论)、用户增长分析(注册、留存、活跃)、商业化分析(广告投放效果、营收归因)。这些场景对应的技术栈实际上非常稳定,笔试出题自然也会往这些方向去靠。
所以如果你是准备投递B站数据开发岗位的应届生,或者刚转行想进入大数据领域,这套卷子实际上是很好的“体检报告”。它不只是在测试你会不会写代码,而是在测试你有没有真正理解一个数据平台是怎么运转的。接下来我会按照这套卷子的核心知识点逐块拆解,顺带把对应的准备方法和易错点一起讲清楚。
2. SQL题目:看似送分,实则全是坑
2.1 窗口函数是绝对重点
B站这套笔试卷里,SQL题占比最高,而窗口函数又是SQL题里的高频考点。基本可以确定的是,凡是考SQL,必然会有一道题和排名、分组TopN、连续登录天数有关。
举个例子,试卷里非常容易出现类似这样的题目:
有一张用户视频观看记录表 watch_log,字段包括:user_id(用户ID)、video_id(视频ID)、watch_date(观看日期)、watch_duration(观看时长,单位秒)。请统计每个用户观看时长最长的前3个视频,以及对应的排名。
这题如果不会窗口函数,硬用group by去做,逻辑会绕一大圈。正确解法是用row_number()或rank():
select user_id, video_id, watch_duration, rn from ( select user_id, video_id, watch_duration, row_number() over (partition by user_id order by watch_duration desc) as rn from watch_log ) t where rn <= 3;这里面有一个很容易被忽略的点:row_number()和rank()的区别。row_number()遇到相同值会随机分配序号,比如两个视频观看时长一样,一个排第1一个排第2;rank()则会并列排名,比如两个视频并列第1,那下一个视频的排名就是第3。具体选哪个,要看业务诉求是“严格取前3条记录”还是“取排名前3的所有记录”。这个是很多人在笔试里丢分的地方,不是不会写,而是没想清楚业务含义。
2.2 连续登录问题的两种解法
连续登录天数是另一道高分值SQL题。它的核心逻辑是:把用户登录日期和用户的“分组序号”相减,如果日期是连续的,相减得到的日期应该是不变的。
select user_id, min(login_date) as start_date, max(login_date) as end_date, count(*) as continuous_days from ( select user_id, login_date, date_sub(login_date, row_number() over (partition by user_id order by login_date)) as group_id from user_login_log ) t group by user_id, group_id;如果数据量特别大,比如一张登录表里面有上亿条记录,那这个方案会在date_sub计算和分组聚合上消耗较多资源。实际生产环境中,更推荐用按天去重后再计算的思路,先把同一用户一天内多次登录去重,再套上面的连续分组逻辑。笔试题目里如果没说明,默认一天只记一条,但实际工作中这种细节必须自己考虑到。
2.3 JOIN优化容易被忽视
这张笔试卷里还有一道比较典型的JOIN优化题,考点很直接:大表JOIN小表怎么优化?
很多人的第一反应是“给关联字段加索引”。这在传统关系型数据库里没错,但在Hive/Spark里完全不适用,因为底层是分布式计算,不走索引机制。正确的优化方向应该是:
- 使用MapJoin,把小表加载到每个Mapper节点的内存里,避免Shuffle
- 合理设置
spark.sql.autoBroadcastJoinThreshold,让Spark自动识别小表并广播 - 如果两个表都特别大,就需要考虑分桶Join或Skew Join
举一个实际场景:B站做视频推荐分析时,经常需要把“几十亿条用户行为日志”和“几百个视频分类维度表”做关联。这种场景如果不走MapJoin,整个任务跑几个小时都不奇怪;走MapJoin之后,基本几分钟就能搞定。笔试里遇到这类问题,不要只答一个“用小表驱动大表”,要把底层原理讲透,让面试官看到你不是在背答案,而是真的理解分布式计算的执行过程。
3. 大数据组件原理:不能只停留在“会用”
3.1 HDFS读写流程必须要能画出来
大数据组件原理这部分的考点非常固定,HDFS的读写流程基本是必考。这套卷子里也少不了。
为什么总考这个?因为HDFS是几乎所有离线大数据平台的存储底座,如果连它的工作原理都不清楚,后面根本没法排查问题。我面试候选人的时候,最怕听到的回答是“我平时就是hadoop fs -put上传文件”,这个回答在笔试里也过不去。
HDFS写入流程的核心步骤是:
- 客户端向NameNode发起写请求
- NameNode检查权限、目录是否存在,并返回可用的DataNode列表
- 客户端将文件分成Block(默认128MB),按顺序写入第一个DataNode
- 第一个DataNode收到数据后,复制给第二个DataNode,第二个复制给第三个,形成流水线复制
- 所有副本写完后,客户端通知NameNode写入完成
这里面常考的细节是:副本放置策略。默认策略是第一个副本放在客户端所在的节点(如果客户端不在集群内,则随机选一个节点),第二个副本放在与第一个副本不同机架的节点,第三个副本放在与第二个副本相同机架的另一个节点。这个策略的核心目的是在“容灾”和“写入性能”之间取平衡,既要保证数据不会因为整个机架断电而全部丢失,又要尽量避免跨机架复制带来的带宽消耗。
3.2 Spark的Shuffle机制:理解它就理解了Spark性能
Spark相关的考点里,Shuffle是绝对绕不开的。B站这套笔试卷里有一道题问的是Spark作业运行缓慢的排查思路,核心指向就是Shuffle。
Shuffle简单理解就是:数据在多个节点之间重新分配的过程。比如group by key,相同的key必须被分到同一个节点上去处理,这就需要把各个节点上的数据打乱重排。
Spark的Shuffle过程有几个关键词需要理解:
- Shuffle Write:每个Mapper任务把结果写到本地磁盘,按照key进行分区
- Shuffle Read:每个Reducer任务从所有Mapper节点拉取属于自己的那部分数据
- Shuffle Spill:内存不够时,把数据溢写到磁盘,会有额外的序列化和IO开销
实际操作中,Shuffle相关的调优手段很多,比如调整spark.shuffle.file.buffer(默认32KB,调高可以减少磁盘IO次数)、开启spark.shuffle.consolidateFiles(合并小文件)、设置合理的分区数等。但笔试里更重要的是能够说清楚,为什么Shuffle是Spark作业性能瓶颈的高发区——因为Shuffle涉及磁盘IO、网络传输、序列化等多个环节,任何一个环节出现短板,整个作业都会被拖慢。
3.3 Hive与Spark SQL的差别不能搞混
试卷里有一类题喜欢“钓鱼”:给你一段Hive SQL,问它在Spark SQL上跑需要注意什么。这个考点很实际,因为很多团队在做离线数仓时,底层的计算引擎已经从Hive on MR切换到了Spark SQL,但SQL语法和调优思维却没有完全跟上。
最大的差异在于执行模型。Hive on MapReduce把每个SQL操作拆分成一个或多个MapReduce任务,每一步的结果都写入HDFS,容错性好但是慢;Spark SQL则会在内存中构建DAG(有向无环图),尽量把多个操作串联起来,减少落盘次数。
这导致一个常见问题:同样的SQL,在Hive上跑得很好,拿到Spark SQL上反而报内存溢出。原因往往是Hive里SQL产生了大量中间结果,这些结果在Hive里是落盘到HDFS的,不会占用执行内存;但在Spark里,如果Join或GroupBy的数据量超过了executor内存,就会直接OOM。解决思路是增加分区数,或者在合适的场景下使用broadcast join把大表Join小表的Shuffle直接省掉。
4. 数据仓库建模:这是区分“SQL Boy/Coder”和“数据开发工程师”的分水岭
4.1 为什么要分层
B站这套笔试卷中,数据仓库建模的题目占比不算特别高,但分值很重。有一道题直接问“你们理解的数据仓库分层结构是什么?每一层的作用是什么?”
这道题看似简单,很多人也能背出“ODS、DWD、DWS、ADS”这些缩写,但真正能把每层的作用和设计原则讲清楚的候选人比例很低。
我打个比方,数据仓库分层就像是厨房的流水线:
- ODS层(原始数据层)相当于“买菜回来不做任何加工”,生肉是生肉,蔬菜是蔬菜,原样放进冰箱。这一层的数据和源系统保持一致,用于追溯和数据备份
- DWD层(明细数据层)相当于“洗菜切菜”,把数据做清洗、去重、标准化,产出干净的、可复用的明细数据
- DWS层(汇总数据层)相当于“配菜”,按照业务维度把常用指标提前聚合好,比如按天、按用户维度统计好的PV/UV等,查询的时候不用重新跑明细
- ADS层(应用数据层)相当于“上桌的菜”,直接服务于具体报表或业务产品的数据
这个分层结构对应的核心思想就是:**每一层解决一类问题,避免数据关系像蜘蛛网一样纠缠不清。**如果没有分层,所有需求都直接查ODS原始数据,那么每条业务线都要自己解析日志、自己清洗、自己计算指标。一旦口径变了,所有下游应用全部要改,维护成本极高。
4.2 维度建模中的退化维度陷阱
这套卷子里有一道比较有区分度的题目,关于维度建模中“退化维度”的处理。
退化维度(Degenerate Dimension)指的是原本应该存在于维度表中的维度字段,直接放在了事实表中,不再单独建维度表。典型的例子就是:订单表中的“订单号”字段。订单号既是一个事实(每个订单产生一次),又包含了维度属性(订单状态、下单渠道、支付方式等),但它只跟当前这个订单一一对应,拆成一张独立的订单维度表反而会导致无谓的JOIN。
这里真正容易犯错的地方在于:很多初学者分不清哪些字段该退化为事实表的属性,哪些该保留为外键关联维度表。
一个比较实用的判断标准是看字段的“基数”和“复用性”:
- 订单号、交易流水号:基数极大,但每次查询都是单独关联,适合退化到事实表
- 商品ID、用户ID:基数也很大,但这边的商品名称、用户等级等属性被多个业务场景复用,应该独立建维度表
- 支付渠道、订单状态这类枚举值:可以退化到事实表,直接存code,通过字典表解释即可
这样设计的核心逻辑是:让事实表尽量“胖”,减少查询时需要JOIN的维表数量,从而提升查询性能;让维度表尽量稳定,集中管理可复用的描述性属性。
4.3 指标口径的一致性怎么保证
B站这套卷子的数仓建模部分还涉及一个特别实际的问题:同一个指标,在不同报表里口径不一致怎么办?
这个问题本质上就是数仓领域的“指标治理”问题。举个例子:视频的“播放量”这个指标,A部门定义的是“用户点击播放按钮并至少播放3秒”,B部门定义的是“视频加载完成即算一次播放”。两边统计口径不一样,出来的数字自然对不上,业务方就会困惑到底该信哪个。
解决方案一般有三种层级:
- 在DWD层统一清洗逻辑,定义好“有效播放”的边界
- 在DWS层建设统一的指标字典,将指标名称、计算公式、统计粒度、更新频率全部登记在册
- 在ADS层做数据校验,确保同一指标在不同报表中输出一致
笔试或者面试中答这类题,重点不是罗列概念,而是要体现出“这个问题我实际遇到过,并且有对应的解决办法”。
5. 算法题:并不只是LeetCode,而是数据开发的日常缩影
5.1 TopN问题的多种实现方式
B站这套笔试卷中,算法题目的数量和难度都控制在一个合理的范围内,不会像算法岗那样考困难题,但基础的数据结构和算法思维是必考的。其中TopN问题几乎是每个数据开发岗位笔试的标配。
TopN问题在业务里非常常见:播放量Top100的视频、涨粉最快的UP主、弹幕热度Top50等。笔试里面可能直接让你写代码,也可能把它包装成一个情景题。
如果只是单机的数据量,一个堆排序就能解决问题:
import heapq def top_n(nums, n): return heapq.nlargest(n, nums)但如果是海量数据,比如几十亿条播放记录要统计Top100的视频,单机的思路就行不通了。这时就需要用分治法:先对数据进行分片,每片分别计算TopN,最后再合并各片的TopN结果。
Spark里做这件事就更方便了,直接用orderBy+limit,底层框架会自动优化执行计划。但笔试里的算法题更想看到的是,你能不能在不依赖框架的情况下,用基础的数据结构和算法把思路表达清楚。
5.2 海量数据处理的高频套路
数据开发笔试里的算法题,很多时候并不是纯粹的算法题,而是“海量数据处理题”。这类题的核心套路其实就那么几类:
- 哈希分治:把大文件通过哈希取模拆成多个小文件,每个小文件单独处理
- 位图法:判断一个数是否在某个集合里,比如统计UV时给每个用户分配一个偏移量
- 布隆过滤器:判断某个元素“一定不存在”或者“可能存在”,在大规模去重场景下能节省大量内存
- Trie树:处理字符串前缀匹配的场景,比如敏感词过滤、搜索词推荐
举个具体的场景:假设B站一天有2亿条弹幕数据,需要过滤掉重复的弹幕内容。如果直接用一个HashSet去重,内存可能会爆掉。如果用布隆过滤器,初始化一个适当大小的位数组,每来一条弹幕就计算多个哈希函数并映射到位数组中的位置,如果所有位都被置为1,说明这条弹幕可能在之前出现过;只要有一个位是0,就一定没出现过。整个过程内存占用只有HashSet的几十分之一。
笔试里能够把这些思路表达出来,同时说清楚它们的优缺点(比如布隆过滤器有误判率,不能删除元素),就比死记硬背强太多了。
6. 业务场景题:B站特色考题背后的数据思维
6.1 如何统计视频的“有效播放”
B站这套笔试卷里最让我印象深刻的,是一道业务场景题:如何定义和统计一个视频的“有效播放”。
这个题目很有B站特色,也同样出现在很多内容平台的数据开发笔试中。它考察的不是你能不能写SQL,而是你怎么把一个模糊的业务概念转化成清晰的技术方案。
“有效播放”的定义并不是唯一的,需要产品、运营和技术一起协商确定。常见的有两种口径:
- 播放时长阈值法:播放时长超过一定秒数(比如3秒、10秒、30秒)才算有效播放
- 播放进度比例法:播放进度超过视频总时长的某个百分比(比如5%)才算有效播放
两种口径各有优劣。时长阈值法实现简单,但对长视频和短视频的公平性不足——一个10分钟的视频刷了10秒可能还没进入正片,一个15秒的短视频刷10秒就已经看了一大半。进度比例法则更贴近“用户真的在看”这个语义,但需要join视频信息表,计算逻辑更复杂一些。
实际实现中,很多团队会在上报日志里同时记录:video_id、play_start_time、play_duration、video_total_duration。之后在DWD层做清洗时,根据约定的口径统一打标is_valid_play,下游所有应用只需要基于这个标签做聚合即可。这里就涉及到前面讲到的指标口径一致性——如果没有在DWD层统一打标,每个部门自己算自己的,数据一定对不齐。
6.2 实时计算场景的选型思路
B站这套笔试卷里还有一道实时计算相关的应用题,问的是:直播场景下,如何实时统计在线人数?
这题对没有接触过实时计算的同学来说会有点懵。因为在线人数的统计和普通PV/UV不一样,它天然带有“时间窗口”属性,而且有进出两个动作。
常见的实现方案有两种:
方案一:基于Flink的滚动窗口聚合
直播间的进房、退房行为都会产生事件流,每来一个事件就更新当前房间人数,然后按固定时间窗口(比如每5秒)输出一次当前在线人数:
DataStream<LiveEvent> stream = env.addSource(new FlinkKafkaConsumer<>("live_event", ...)); stream .keyBy(event -> event.getRoomId()) .window(TumblingProcessingTimeWindows.of(Time.seconds(5))) .aggregate(new CountAggregate()) .addSink(...);这个方案实现简单,但窗口边界会有误差。假设某用户在10:00:03进入直播间,10:00:07退出,这两个事件可能落在两个不同的窗口里。结果是A窗口加了1,B窗口减了1,但A窗口统计时实际用户已经不在线了。
方案二:维护实时状态 + 定时输出
使用Flink的KeyedProcessFunction,为每个直播间维护一个当前在线人数的状态值。进房事件到达时状态加1,退房事件到达时状态减1,同时注册一个定时器,每隔5秒把当前状态值输出一次。
这个方案更精准,但需要处理状态过期、事件乱序等问题,实现复杂度更高。
笔试里能把这个场景的两种方案都说清楚,并且说明各自的优缺点,面试官基本就能判断出你是有真实项目经验的。这也是为什么这套卷子被很多人称之为“有水平”——它不是死记硬背就能通过的考试,而是真的在考察数据开发的核心思维。
7. 备考建议:如何高效准备这类数据开发笔试卷
7.1 知识点优先级排序
结合B站这套笔试卷(二)的考点分布,我建议准备方向按以下优先级来:
- SQL:窗口函数、分组聚合、JOIN,每天至少写3道题保持手感
- Hive/Spark原理:执行流程、Shuffle机制、常见报错排查
- 数仓建模:分层架构、维度建模、事实表和维度表的区分
- 业务场景题:多总结内容平台、电商平台常见的分析口径
- 算法与数据结构:海量数据处理套路为主,LeetCode中等难度为辅
7.2 刷题之外的准备工作
除了刷题还有一个很多人会忽略的准备:在简历中梳理至少一个完整的数据项目。笔试之后紧接着就是面试,如果笔试分数不错但简历上的项目一问三不知,那基本就止步于此了。
一个能够拿出来讲的项目,至少要包含这些环节:
- 项目背景:解决什么业务问题
- 数据来源:埋点日志、业务库同步还是第三方数据
- 技术架构:用到了哪些组件、为什么这么选型
- 建模过程:事实表和维度表怎么设计的,指标口径怎么定的
- 性能优化:遇到过什么性能问题、怎么解决的
- 最终效果:给业务带来了什么价值(最好有量化数据)
我在之前的文章里也反复强调过,数据开发这个岗位最看重的是“能不能把业务问题翻译成技术方案”。这套笔试卷的每一道题,本质上都是在做这种翻译工作。
7.3 保持好奇心和总结习惯
回到这套2020年的笔试卷,现在回头看,有些技术选型可能已经过时了,比如Hive on MapReduce在越来越多的场景下被Spark SQL替代,实时计算也从当年的Storm、Flink并存变成Flink基本一统天下。但这套卷子背后考察的数据开发核心能力——SQL功底、组件原理理解、数仓建模思维、业务理解能力——从来都没有变过。
我的经验是,准备这类笔试不要只把它当成求职的敲门砖,而要把每个知识点都理解透。B站这套卷子之所以值得反复研究,就是因为它能让认真准备的人构建出一套完整的数据开发知识体系,这套体系会在你整个职业生涯中持续发挥作用。
8. 常见问题和避坑经验
8.1 SQL题最容易被扣分的三个细节
SQL题是B站这类笔试卷的得分主力,但很多人在非技术细节上丢分,非常可惜。
第一,没有处理NULL值。统计一个用户的总观看时长时,如果某条记录的时间字段是NULL,sum()函数会直接忽略它,但count()函数会把NULL也计数进去。业务上如果希望NULL按0处理,就要用nvl()或coalesce()显式转换。
第二,去重逻辑不严谨。统计UV时忘了用count(distinct user_id),或者用了去重但没搞清楚distinct放在count()里和放在查询列表里的区别。
第三,没有考虑数据倾斜。试卷里的SQL题虽然不会真的给你跑一个巨大的集群,但如果你能在答案里主动指出“这个SQL在真实数据量下可能导致某个key数据倾斜,可以这样优化”,会大大加分。
8.2 大数据组件题目不要死记硬背
很多人在准备大数据组件原理时,喜欢背一些概念,比如“NameNode负责元数据管理”“DataNode负责数据存储”。这些当然没有错,但笔试的题目通常不会停留在这种层面,而是会问:
- “如果某个DataNode节点宕机了,HDFS会发生什么?”
- “NameNode重启的时候,大量DataNode同时上报Block报告怎么办?”
- “Spark作业频繁Full GC可能是什么原因?”
这些问题考察的是在真实环境下,你对这些组件运行机制的理解程度。建议在准备时,多问自己几个“如果……会怎样”。如果有条件的话,搭个单机伪分布式环境,自己把NameNode停掉,观察DataNode的行为,比背十遍文档都管用。
8.3 业务场景题:先给结论再展开论证
业务场景题是最容易拉开分差的题型。因为这类题没有标准答案,考察的是分析思路和表达能力。
我建议采用“总-分-总”的结构来回答:
- 先给出明确的处理思路(第一句话就说清楚)
- 用数据比如“一个10分钟的视频,用户看了8秒”来举例
- 按步骤拆解技术方案
- 最后做一个小结,说明这个方案的优势和可能的潜在问题
这一套下来,不仅逻辑清晰,还给后续的面试提问留了更多讨论空间,不会一下把话说死。
8.4 关于2020年这套卷子的最后一点说明
这两年陆续带着新人复盘过几遍这套B站笔试卷(二),每次都能发现一些新的收获。笔试卷在变、技术在迭代,但数据开发这个岗位的本质没有变:它永远需要你在业务理解和技术实现之间找到那个最合适的平衡点。如果说有什么备考心得值得分享,那一定是:把每一个题目背后对应的业务场景想清楚,所有的技术都只是实现手段而已。