“B站校招数据仓库方向的笔试卷”,是最近我后台收到的高频提问。不少准备投递数据研发、数据仓库、大数据开发岗位的同学,都会拿这套题来练手。我得说,B站这套笔试卷在圈内口碑挺有意思——它不像一般互联网公司那样纯考SQL语法,也不是单纯背概念,而是把数据仓库建模、明细表设计、指标口径、SQL实战全揉在一起,题量不大但每道题都踩在数仓日常工作的核心节点上。今天就把我对这套试卷的理解、拆解和一套完整的高分答题思路整理出来,希望能帮正在准备校招的同学少走弯路。

这篇文章适合三类读者:正在准备数据仓库/数据研发校招笔面试的应届生,准备跳槽到数仓方向的初级开发,以及想借一套真实题目检验自己数仓基本功的从业者。内容会覆盖数仓建模方法论、核心维度表和事实表设计、SQL笔试题套路,以及大量实际项目中才遇得到的避坑经验。

1. 笔试卷的整体定位与考点分布

1.1 试卷结构:这不是一份“刷题数据库”

先说结论:B站这套笔试卷,更像一份“数仓工程师上岗能力自测表”,而不是传统意义的考试题。从我收集到的信息看,整套卷子大致分三块:数据仓库理论基础、维度建模实战设计、SQL与数据开发能力。

基础理论部分会考察数仓分层架构(ODS、DWD、DWS、ADS)、维度建模理论(星型模型、雪花模型、事实表类型)、数据质量保障手段等。这部分不会让你背定义,而是给一个业务场景,让你说明如何分层、如何建模、如何保证口径统一。

建模实战部分通常会给一个具体的业务场景,比如“用户订单分析”,要求设计核心维度表和事实表,说明粒度、主键、字段含义。这是整套卷子的重头戏,后面我用一整章来拆。

SQL与数据开发部分则是典型的笔试编程题,覆盖窗口函数、留存计算、连续登录、行列转换、SQL调优,偶尔还会考察Hive/Spark的底层原理。

1.2 为什么校招笔试卷要这样出题

站在招聘方的角度看,这个考察逻辑非常清晰:数仓岗位的校招生入职后,前半年大概率是在做三类事——写ETL、建表建模、口径对齐。这就决定了笔试卷必须能筛出三类能力:

第一类是“能不能写对SQL”。校招生可以没写过生产级数仓,但只要SQL能力强,上手ETL的速度就很快。第二类是“懂不懂建模逻辑”。事实表和维度表怎么设计、粒度怎么定、缓慢变化维怎么处理,这些是数仓工程师和普通后端开发的核心区别。第三类是“有没有业务 sense”。同样的订单数据,让不同人设计表结构,得到的方案差异极大,这背后反映的是对指标口径、查询模式的理解。

所以这份卷子不考偏题怪题,考的全是数仓工程师日常天天打交道的活。

1.3 校招笔试和社招面试的差异

我经常和准备校招的同学说,不要用社招的题库来准备笔试。社招面试更侧重项目深挖,比如你做过什么数仓项目、遇到过什么数据倾斜、怎么优化的。而校招笔试卷是标准化的,答案相对客观,评卷也是按点给分。

这意味着校招笔试准备的核心是“基础扎实、术语准确、思路完整”。比如让你设计订单事实表,你不需要写出B站真实的埋点字段(也不可能知道),但你只要把维度建模的规范写清楚:粒度声明、维度外键、度量字段、退化维度、分区策略,就能拿到大部分分数。这套卷子的核心价值也在这里——它把数仓工程师的“基础功”量化了。

2. 从用户订单分析看数仓建模方法论

2.1 为什么笔试总爱考“订单分析”

几乎每一家互联网公司的数仓笔试题,都会出现“订单分析”或类似的交易类场景。原因很简单:订单分析链路长,从用户下单、支付、发货、收货到退款,是一条完整的业务链路,能够全面考察建模能力。

更关键的是,订单分析的核心“用户订单分析数据仓库设计核心维度表和事实表”正是数仓建模的经典场景——它涉及多个维度(用户、商品、商家、时间、地区),有多种事实(订单、支付、退款),还有业务口径的复杂度(订单金额是下单口径还是支付口径、退款是否冲减GMV等)。一道订单设计题,基本能把一个人对数据仓库的理解摸透。

2.2 维度建模的核心概念,用买菜理解

很多同学对维度建模的恐惧来自术语。但本质上,维度建模就是回答两个问题:我要分析什么(事实),我从哪些角度分析(维度)。

用买菜来类比:你每天记录“花了多少钱买什么东西”,这是事实;“今天”“在哪个菜市场”“买了蔬菜还是肉类”,这是维度。事实表存的是可加的数值度量(金额、数量、次数),维度表存的是描述性属性(名字、分类、地区、时间特征)。

在订单场景里,订单事实表的核心度量就是:订单金额、商品数量、优惠金额、运费;维度则是:哪一个用户(用户维度)、哪一件商品(商品维度)、哪一家店铺(商家维度)、什么时候下单(时间维度)、从哪个渠道来(渠道维度)。

2.3 事实表的三种类型,必须在笔试中写出来

这是我在批改模拟答案时最常看到的扣分点。很多同学设计订单事实表时,只写了一张“大宽表”,把所有度量都塞进去。但专业的建模会区分三种事实表:

事务事实表:每一笔业务事件产生一行,比如订单明细表,一行就是一个订单的一个商品子项。它最常用于统计订单数、GMV、销量这类可累计的指标。

周期快照事实表:按固定周期(比如每天)记录某个对象的累计状态,比如“用户每日累计订单金额表”。常见于分析DAU、留存、复购等需要切片状态的场景。

累积快照事实表:记录一个流程从开始到结束的关键节点时间,比如订单从创建、支付、发货到完成,一行代表一个订单的完整生命周期。常用于分析流程耗时、转化漏斗。

笔试时如果你能根据“用户订单分析”场景同时给出事务事实表和累积快照事实表的方案,并说明各自适用的指标,评分会明显上一个档次。

2.4 缓慢变化维:订单维度的“历史还原”能力

订单分析里经常遇到一个坑——商品价格变了、分类调整了、用户等级升级了。如果直接关联最新的维度表,所有历史订单都会按“现在的分类”统计,历史数据就失真了。

这就是缓慢变化维要解决的问题。完整写法是三种策略:SCD1直接覆盖、SCD2保留历史版本(增加生效时间和失效时间)、SCD3保留上一次值。在订单事实表设计中,通常采用“退化维度”策略,把订单下单时刻的关键属性直接冗余到事实表,比如商品的一级分类、二级分类、价格、商家名称,这样即使后续维度变化,历史订单分析依然能还原当时的业务状态。

3. 订单分析主题的维度表与事实表设计实操

3.1 第一步:把分析需求整理成指标清单

先做需求梳理,再动手建表,这是笔试答题的核心逻辑。拿到“用户订单分析”这个题目时,先不要着急写字段,而是先明确:这个数仓主题要支撑哪些分析?

我一般会按三条线拆解:

第一条线是交易核心指标:订单数、下单用户数、支付订单数、支付金额(GMV)、客单价、件单价。第二条线是商品分析:商品销量TopN、类目销售占比、品牌销售趋势、退货率。第三条线是用户分析:新老用户订单贡献、复购率、人均订单金额、用户生命周期价值。

有了这个指标清单,后边的表结构设计就有了明确的目标——每一张表都是为了支撑这些指标的计算。

3.2 维度表设计:四个必须覆盖的核心维度

在笔试中,维度表设计至少要覆盖四个核心维度:用户维度、商品维度、商家维度和日期维度。我给出一个经过实际项目验证的字段设计方案,可以直接作为答题模板。

用户维度表:

字段名 类型 说明
user_key bigint 代理主键,自增
user_id string 业务主键,用户ID
register_time timestamp 注册时间
user_level string 用户等级
is_new_user int 是否新客(当天注册)
channel string 注册渠道
region_id string 地区ID
age_group string 年龄分段

商品维度表:

字段名 类型 说明
goods_key bigint 代理主键
goods_id string 业务主键
goods_name string 商品名称
category_id string 类目ID
category_name string 类目名称
brand_id string 品牌ID
brand_name string 品牌名称
goods_price decimal 商品当前价格

商家维度表:

字段名 类型 说明
seller_key bigint 代理主键
seller_id string 业务主键
seller_name string 商家名称
seller_level string 商家等级
industry string 主营行业
join_time timestamp 入驻时间

日期维度表是数仓最容易被忽略但极其重要的一张维度表。核心字段包括:date_key(int类型,如20230724)、year、month、day、week_of_year、quarter、is_weekend、is_holiday、season_name等。它的昵称叫“时间维”,几乎每一张事实表都要和它关联,可以一次性生成5到10年的数据然后反复使用。

设计维度表时要特别注意字段类型的选择。日期不要用字符串存,用date_key整数;金额用decimal不要用double,否则精度丢失;状态字段用int或string都可以,但要保证字典值统一维护。

3.3 事实表设计:订单事实表的完整字段方案

订单事实表是整个“用户订单分析”的核心表,它会直接支撑订单数、GMV、客单价等核心指标。我在实际项目中沉淀了一套字段规范,笔试可以直接套用。

订单事实表(粒度:一个订单一行):

字段名 类型 说明
order_id string 订单ID,业务主键
order_sn string 订单编号
user_key bigint 用户维度外键
goods_key bigint 商品维度外键(单商品订单)
seller_key bigint 商家维度外键
date_key int 下单日期外键
order_amount decimal 订单应付金额
pay_amount decimal 实付金额
discount_amount decimal 优惠金额
freight_amount decimal 运费
order_status string 订单状态
pay_status string 支付状态
source_channel string 下单渠道,退化维度
category_name string 下单时类目,退化维度
is_paid int 是否支付
is_refund int 是否退款

但实际业务中,一个订单往往包含多个商品,因此还需要订单明细事实表(粒度:一个订单的一个商品一行):

字段名 类型 说明
detail_id string 明细ID
order_id string 订单ID
user_key bigint 用户维度外键
goods_key bigint 商品维度外键
seller_key bigint 商家维度外键
date_key int 下单日期外键
goods_count int 商品数量
goods_price decimal 下单时单价
total_amount decimal 商品小计
split_pay_amount decimal 分摊实付金额

为什么要拆成订单事实表和订单明细事实表?因为指标的口径不同。订单表统计订单数时是count(order_id),明细表统计销量时是sum(goods_count)。如果只做一张表,要么订单数算错,要么销量算错。这就是为什么笔试设计题一定要体现“粒度”意识——先声明粒度,再谈字段。

3.4 事实表和维度表的关联关系与数仓分层

设计完表,还需要把表放进数仓分层中说明各自的位置。这是笔试中体现工程素养的关键一步。

贴源层ODS:直接存放业务库同步过来的订单表、订单明细表、用户表、商品表、商家表,保持原始结构,不做过多的清洗,主要作用是备份和追踪。

明细层DWD:完成清洗、规范化、维度退化。比如订单表在这个层会从用户表关联出user_key,从商品表关联出goods_key,把冗余的分类名称关联进来。这一层的核心原则是“维度退化到事实表,构建明细宽表”。

汇总层DWS:基于DWD层,按主题进行汇总。比如用户订单汇总表(按用户+日期维度汇总订单数、支付金额、退款金额)、商品销售汇总表(按商品+日期维度汇总销量、销售额)。

应用层ADS:面向具体报表和应用,比如大屏GMV、运营看板、排行榜。

分层设计的核心好处是:避免业务方直接查询ODS原始表,保证指标口径统一,同时通过层层汇总减少重复计算。

3.5 笔试评分点:一份高分的建模答案长什么样

结合我这些年看过的笔试答案,一份能拿高分的建模设计题答案通常包含以下要素:

  • 明确声明每张表的粒度。比如“订单事实表粒度为一个订单一行”“订单明细事实表粒度为一个订单的一个商品一行”,这句话是建模的分水岭。
  • 区分代理主键和业务主键。user_key和user_id都写出来,说明你理解维度表的SCD处理。
  • 包含退化维度字段。很多考生只写维度外键,忽略了source_channel、category_name这类冗余字段,这说明缺乏实际ETL经验。
  • 说明分区策略。比如订单表按date_key分区,每天一个分区,离线任务每天凌晨调度,保证数据时效性。
  • 解释设计取舍。比如为什么把优惠金额单独拆出来而不是只写实付金额,因为运营需要分析折扣力度对转化的影响。

4. SQL实战与常见问题排查

4.1 笔试SQL高频题型:可以提前准备的六类题

B站笔试卷的SQL部分,题型基本固定。根据我收集到的信息和圈内讨论,高频题型集中在以下六类:

  • 窗口函数类:TopN、排名、同环比、累计值计算。
  • 留存计算类:次日留存、7日留存、漏斗转化。
  • 连续类问题:连续登录N天用户、连续消费N周。
  • 行列转换类:行转列、列转行、多行拼接。
  • 去重与空值处理类:UV去重、null值替换、无效数据过滤。
  • SQL优化类:如何减少shuffle、如何避免数据倾斜。

窗口函数是绝对重点。很多候选人能把join写得很熟,但一遇到row_number()、lag()、lead()、sum() over(partition by ... order by ...)就发怵,这是平时训练不足的表现。笔试卷里窗口函数通常不会单独出一道题,而是嵌在业务场景里考。

4.2 典型题目一:连续3天登录用户

这道题我在各个厂的笔试里见过至少五次,是数仓SQL的经典入门题。需求:给定用户登录记录表user_login(user_id, login_date),求连续登录3天及以上的用户。

解题思路的关键在于:连续日期的实质是“日期减去行号得到一个相同分组”。

with t1 as (
  select user_id,
         login_date,
         row_number() over(partition by user_id order by login_date) as rn
  from user_login
  group by user_id, login_date
)
select user_id
from (
  select user_id,
         date_sub(login_date, rn) as grp
  from t1
) t2
group by user_id, grp
having count(1) >= 3;

注意几个细节:第一步先group by去重,因为同一天可能有多条登录记录,不去重会导致row_number计算错误;date_sub(login_date, rn)得到的是分组标识,同一个分组内日期是连续的;最后having count(1) >= 3筛选连续3天。

4.3 典型题目二:计算用户次日留存率

留存率是数据分析的高频需求,笔试也爱考。需求:计算某日新增用户在次日还活跃的比例。

标准写法:

with new_user as (
  select user_id, date(register_time) as reg_date
  from user_dim
  where date(register_time) = '2024-01-01'
),
active_next as (
  select distinct user_id
  from user_active_log
  where dt = date_add('2024-01-01', 1)
)
select 
  count(n.user_id) as new_user_cnt,
  count(a.user_id) as retained_cnt,
  round(count(a.user_id) / count(n.user_id), 4) as retention_rate
from new_user n
left join active_next a
on n.user_id = a.user_id;

这道题的避坑要点在于:活跃用户表通常是每天一个分区的登记录,必须用distinct去重;留存率的分子是次日活跃的新用户数,分母是新用户总数,join时用left join防止新用户因次日未活跃而丢失。

4.4 典型题目三:订单指标计算,fact表和维表join的坑

这道题和前面的建模题互为照应。表结构就是第3章设计的订单事实表和维度表。需求:统计2024年1月每天的下单用户数、订单数、GMV,并按商品类目分组展示。

select
  date_key,
  a.category_name,
  count(distinct user_key) as buyer_cnt,
  count(order_id) as order_cnt,
  sum(pay_amount) as gmv
from dwd_order_fact a
where date_key between 20240101 and 20240131
  and pay_status = 'paid'
group by date_key, category_name
order by date_key, category_name;

这里有几个隐蔽的坑需要说明。第一,count(order_id)统计的是订单数,但如果事实表粒度是订单明细,count(order_id)会重复计数,此时要先count(distinct order_id)。第二,GMV是支付金额,必须过滤掉未支付订单,不然订单数和GMV口径对不上。第三,维度字段category_name已经冗余在事实表中,所以不需要join维度表,这也是第3章设计时做退化维度的意义所在。

4.5 数据倾斜、空值和精度问题:笔试中的隐藏分

除了基础语法,笔试卷还会在细节里埋坑,这些地方往往是区分“会写SQL”和“能写生产SQL”的分水岭。

数据倾斜是数仓面试的必问题。笔试里如果遇到join大表和小表,可以选择mapjoin提示优化;遇到group by热点key,可以考虑加盐拆key再合并。这些不是要求你在笔试现场优化框架,而是考察你有没有排查性能问题的意识。

空值处理是另一个高频坑。事实表中user_key为空,说明是匿名下单或数据缺失;金额字段为空,sum()会直接忽略,但count()不会。更隐蔽的是join时维表缺失,导致事实表数据丢失。生产环境通常会把维表缺失的key统一替换成-1,并冗余一个“未知”维度记录,而不是直接过滤掉。

精度问题也值得提一句。金额字段在MySQL中可能是decimal(10, 2),但在Hive/Spark中如果建表时误用了double,多次聚合运算后会出现精度漂移,导致对账不平。这也是为什么第3章设计表结构时,金额一律用decimal。

4.6 常用排查SQL和问题速查表

实际工作里,数据对不上是常态,笔试里偶尔也会出一个“数据异常排查”场景题。我整理了一份速查表,建议准备笔试的同学可以反复揣摩。

现象 可能原因 排查方法
订单数比业务后台多 明细表粒度重复 检查是否有同订单多商品行被重复count
GMV比业务后台少 过滤了未支付或退款订单 核对支付状态过滤条件
留存率超过100% 新增用户表用了全量数据 确认是否限定注册时间
维表join后数据变少 维表缺失或on条件不严谨 改left join并检查匹配率
金额对不上 精度丢失或时区影响 检查decimal类型和日期分区

5. 一些准备建议和踩坑记录

前段时间我在帮一个学弟模拟这套笔试卷时,发现一个很有意思的共性问题:大家能把SQL题写得非常漂亮,但一遇到“设计核心维度表和事实表”就露怯,字段名随手写,粒度不声明,甚至会出现把用户手机号、地址等敏感信息直接塞进维度表的低级错误。这说明大多数人平时刷题只刷了SQL,对建模训练太少。建议准备阶段,至少手写三遍订单主题的建模设计,从需求梳理、维度设计、事实表设计到分层方案,形成肌肉记忆。

另一个常见问题是字段命名太随意。笔试时书写的时间有限,很多人会写出order_date、user_name之类的字段。但在实际数仓开发中,字段命名有严格规范:维度外键用xxx_key,业务主键用xxx_id,时间统一用date_key关联日期维表,金额字段统一用decimal。好的命名规范能让代码可维护性大幅提升,也是面试官评分的隐性加分项。

还要提醒一点:B站的业务场景和纯电商略有差异,它有大量内容生态、视频UP主、直播带货相关的分析场景。但数仓建模的核心方法论是相通的,掌握好用户、内容/商品、作者/商家、时间这几个核心维度,以及订单/播放/互动这类核心事实,无论考哪家公司的数仓笔试题,都能很快迁移。

这套试卷其实是一面很好的镜子,把数仓工程师的日常能力要求照得清清楚楚——能写SQL,更要懂建模;能做表,更要懂业务。沉下心把这套题吃透,收获的远不止一张offer。

更多推荐