前两天整理硬盘,翻出一个压箱底的文件夹,里面存着2018年参加美丽联合集团校招时做的那份基础平台-数据仓库开发工程师笔试试卷。当时为了这场笔试啃了一个多月的数仓理论,刷遍了能搜到的面试题,最后虽然顺利拿到了offer,但回头看,真正让我在笔试和后续面试中站稳脚的,不是死记硬背的那些概念,而是把数仓分层、维度建模、Hive调优这套东西真正想明白了。今天干脆把这个“古董”试卷翻出来,结合当年做题的思路和后来实际建仓踩坑的经验,做一次完整的复盘拆解。这个话题对正在准备数仓岗位校招、或者刚入行想系统梳理数仓知识体系的朋友应该挺有价值——你会发现,2018年的笔试题,放在今天依然不过时,因为数仓的底层逻辑从来没变过。
1. 一份笔试试卷背后的岗位画像:电商数仓工程师到底考什么
1.1 基础平台部的定位与命题逻辑
先聊个很多人会忽略的点:为什么岗位名称里带“基础平台”四个字。在电商公司里,基础平台部通常负责的是整个公司的数据基础设施,包括数据仓库的搭建、ETL调度、数据质量保障、数据服务接口这些底层能力。这也意味着,这个岗位的笔试不会只考你会不会写SQL,它更在乎你有没有搭过一套完整的数据体系,知不知道数据从业务库到报表端要经过哪些环节,每一环节的职责和边界是什么。
当时试卷的命题逻辑其实很清晰,就三大块:理论基础题考察你对数仓核心概念的理解深度,SQL实操题考察你的基本功和优化意识,方案设计题则模拟一个真实业务场景,看你能不能把理论落到工程实践上。这种组合今天依然是数仓校招笔试的主流形态,因为数仓工程师的日常工作就是这三件事——理解业务、处理数据、设计方案。
1.2 试卷三大模块:理论基础、SQL实战、方案设计
我印象里那份试卷的题型分布大概是这样的:
| 模块 | 题型 | 考察重点 | 题量 |
|---|---|---|---|
| 理论基础 | 简答/选择 | 数仓分层、维度建模、范式理论 | 约40% |
| SQL实战 | 手写SQL或HiveSQL | 窗口函数、多表关联、数据去重、行列转换 | 约30% |
| 方案设计 | 开放设计题 | 订单分析主题域建模、ETL流程设计 | 约30% |
这个配比值得细品。理论占四成,说明公司要的不是只会调包的“SQL boy”,而是真正理解数仓为什么这么设计的人。SQL占三成,是因为动手能力是底线,写不对SQL,理论再漂亮也没用。方案设计占三成,则是为了筛掉那些只会背概念、遇到实际问题就懵的应试型选手——数仓工程师本质上是在做业务和数据的翻译官,不懂业务建模的人,写出来的表就是一堆没人用的垃圾。
2. 数据仓库分层架构:从ODS到ADS,每一层到底在干什么
2.1 四层架构各层职责与边界
那份试卷的第一道简答题,我记得很清楚:“请简述数据仓库的分层架构,并说明每一层的主要职责。”这道题几乎是我后来每一次面试都会被问到的,可见它有多基础、多重要。标准的数仓分层是四层:ODS(操作数据存储层)、DWD(明细数据层)、DWS(汇总数据层)、ADS(应用数据层)。
ODS层是数据仓库的“入口”,负责把业务库、日志、第三方数据等原始数据原封不动地同步进来,一般只做增量或全量抽取,不做任何加工。DWD层是清洗和标准化的地方,对ODS层的数据做去重、字段规范、维度退化、格式统一,形成干净的明细数据。DWS层以分析主题为驱动,把DWD层的明细数据按照维度进行汇总,生成宽表或汇总表,比如按天、按用户、按商品的聚合结果。ADS层则是面向具体应用的数据,直接为报表、BI看板、数据产品接口服务,表结构和查询需求高度耦合。
2.2 为什么一定要分层:清晰、复用、解耦
我当时在试卷上是这么写的:分层的核心目的是三个——清晰、复用、解耦。清晰是指每一层都有明确的职责边界,数据流向一目了然,新人拿到一张表能迅速知道它属于哪一层、经过哪些加工,不用像看天书一样翻几个月的旧脚本。复用是指把公共的计算逻辑沉淀在DWD和DWS层,下游各业务线不用各自重复计算,省时省力还保证口径统一。解耦则是指当上游业务系统或者底层数据源发生变化时,只需要调整对应层级的任务,不会牵一发而动全身。
后面自己真正参与搭仓之后,我更深刻地体会到分层的另一个价值:它是一道天然的数据质量防火墙。上游业务库的数据往往是混乱的,字段可能为空、枚举值可能变化、时间格式可能不统一,如果这些脏数据直接流到报表里,业务方对数据的信任度会瞬间崩塌。而在DWD层统一清洗和标准化的过程中,我们可以把异常数据拦截、告警、记录,确保流入上层的数据是可靠的。这也是为什么很多公司在数仓规范里会写死一条红线:业务方不允许直接查ODS表,一切数据消费必须通过DWD及以上的层级。
2.3 一个订单明细从ODS到ADS的完整旅程
为了把分层这件事讲透,我拿当时试卷里的一道SQL题展开:统计每天每个类目下的成交金额和成交订单数。这个需求看起来简单,但它能完整串起数仓的分层链路。
ODS层首先同步订单表、订单明细表、商品表、类目表这些原始表,一张表对应一个同步任务,字段结构和业务库保持一致。DWD层做清洗,比如过滤掉测试订单、退款订单,把订单状态字段从业务库的0/1/2映射成可读的待支付/已支付/已关闭,然后把商品表里的类目ID通过维度退化方式冗余到订单明细里,这样下游就不用每次都去关联商品表拿类目了。DWS层按天、按类目做汇总,生成一张dws_trade_category_daily的汇总表,字段包括日期、类目ID、类目名称、成交金额、成交订单数、成交用户数等。ADS层直接基于DWS表查询,一个简单的SELECT就能满足报表需求。
这条链路看起来不复杂,但每一步都有讲究。比如DWD层为什么要把类目冗余进订单明细,而不让下游自己去关联?因为订单表是事实表,商品表是维度表,事实表的数据量是巨大的,如果每个下游任务都要拿着订单明细去关联商品表,会白白消耗大量的计算资源,还会因为关联操作引入数据倾斜的风险。维度退化这个操作就是在DWD层把常用的维度属性直接写进事实表,用空间换时间,这是数仓建模里非常经典也非常实用的一招。
3. 维度建模实战:用户订单分析主题域的核心维度和事实表
3.1 选业务过程与声明粒度
方案设计题是整份试卷里最硬核的部分,也是最能拉开差距的地方。我印象里那题的场景大概是:“假设公司有一个电商平台,需要你设计一个用户订单分析主题域的数据仓库模型,要求包括核心维度表和事实表的设计,并且支持按用户、商品、类目、时间等维度分析订单数据。”
这道题表面上是考察维度建模,实际上考察的是你有没有真正做过需求调研。拿到这个题的第一反应不应该是提笔就画表结构,而是要问自己三个问题:这个分析主题要覆盖哪些业务过程?每个业务过程的分析粒度是什么?度量指标具体指什么?我当时在试卷上先界定清楚了业务过程,把用户订单分析拆成四个核心业务过程:下单、支付、发货、完成。每个业务过程都有自己独立的生命周期和事实表,不能混在一张大宽表里。因为下单和支付,一个管订单生成,一个管资金流动,两者的度量指标、更新频率、数据量级完全不同,混在一起会让模型的扩展性大打折扣。
粒度就更关键了。订单事实表的粒度应该是一张订单一行,而订单明细事实表的粒度应该是一张订单下的一个商品一行。前者用于分析客单价、订单数这种订单级指标,后者用于分析商品销售、类目分布这种商品级指标。粒度声明不清楚,后面的事实表设计一定是一团乱麻。
3.2 核心事实表设计:订单事实表、订单明细事实表、支付事实表
理清楚业务过程和粒度之后,就可以动手设计事实表了。我当时在试卷里画了三张核心事实表:订单事实表、订单明细事实表、支付事实表。
订单事实表粒度是订单级别,每行代表一笔订单,核心度量字段包括订单金额、运费、优惠金额、实付金额。订单明细事实表粒度是订单+商品级别,每行代表一个订单中的一种商品,核心度量字段包括商品数量、商品成交金额、商品优惠分摊金额。支付事实表粒度是支付流水级别,每行代表一笔支付记录,核心度量字段包括支付金额、支付方式、支付时间。
这里有个设计细节值得展开:订单事实表和订单明细事实表的关系。在电商场景中,一笔订单可能包含多个商品,所以订单金额和明细金额之间存在一对多的关系。如果只有一张订单明细事实表,想要分析客单价(订单金额/订单数)就很麻烦,因为同一订单的商品明细会出现多行,直接对订单金额求均值会导致重复计算。所以设计上要把订单级和明细级分成两张事实表,订单级只存订单维度的金额和状态,明细级只存商品维度的数量和金额。这样不管是看订单维度指标还是商品维度指标,都能找到合适的表,而且不会算错。
3.3 核心维度表设计:用户维、商品维、商家维、日期维
事实表定好,维度表就好办了。我设计了几张核心维度表:用户维度表、商品维度表、商家维度表、日期维度表。
用户维度表的粒度是一个用户一行,核心字段包括用户ID、注册时间、性别、年龄、会员等级、城市、省份。商品维度表的粒度是一个商品一行,核心字段包括商品ID、商品名称、类目ID、类目名称、品牌、价格、上架时间。商家维度表的粒度是一个商家一行,核心字段包括商家ID、商家名称、店铺类型、主营类目、开店时间。日期维度表的粒度是一天一行,核心字段包括日期、年、月、周、季度、是否节假日。
维度表设计里有几个容易被忽略但很重要的点。日期维度表不要想着临时用DATE_FORMAT函数在查询时转换,业务上大量的同比、环比、周分析都需要标准化的日期属性,提前把维度表做好,查询时直接关联即可。商品维度表里的类目字段要冗余ID和名称两个字段,虽然会造成一点存储冗余,但下游查询时不需要再关联一张类目表,链路更短、性能更好。另外,像“用户首单时间”“用户最近下单时间”这种累计型指标,看起来像维度属性,但它们其实是依赖于事实表的计算,不应该放在维度表里,否则每天都要回刷维度表,维护成本会很高。
3.4 维度建模笔试答题模板
分享一个我自己总结的维度建模题答题套路,至少能保证你在笔试和面试时不跑偏。第一步写清楚业务过程和粒度,哪怕题目没让你写,这个行为本身就说明你是有建模思维的。第二步画事实表,列出每张事实表的粒度、度量字段、外键。第三步画维度表,列出每张维度表的粒度和属性字段。第四步说明表和表之间的关联关系。第五步,如果时间允许,补充一个典型查询案例,比如“统计上周各品类销售额Top10”,说明你的模型如何支撑这个查询。
这套答题框架的逻辑是:先界定业务范围和数据粒度,再设计核心表结构,最后用查询验证模型。它和实际建仓工作的思考顺序是完全一致的。我当时在笔试时用这套框架答题,不仅把整个设计过程写得清楚,还在最后加了一句:“以上设计采用了星型模型,事实表在中间,维度表在周围,查询时通过外键关联,保证了灵活性和性能的平衡。”这句话直接点明了建模风格,面试官在阅卷时一眼就能看出你对维度建模是真正理解的,而不是只背了几个名词。
4. Hive与大数仓工具链:笔试里最容易被问爆的优化考点
4.1 分区表、分桶表、存储格式怎么选
SQL实操题里有一道让我印象很深的题:“给定一个订单表,日增数据量是1000万行,请设计合理的表结构来存储和查询。”这就是典型的Hive表设计题,考察点集中在三件事上:分区策略、分桶策略、文件格式。
分区表的逻辑很简单,用年、月、日这种业务字段把数据切分到不同的目录下,查询时只要带上分区条件,Hive就不需要扫描全表,直接读对应分区即可。对于日增千万级的订单表,首选按天分区,这样每天一个分区,数据量在千万级,查询单日数据时扫描量就是全部数据的几百分之一。如果某些查询需要频繁按小时做分析,也可以考虑按天分区后再按小时做二级分区。但要小心,分区粒度太细有个副作用:HDFS上会产生大量小目录和小文件,NameNode压力会很大,而且任务的启动开销会被放大。
分桶表则是按某个字段的哈希值将数据均匀打散到固定数量的桶文件中。分桶的价值主要在两个场景:一是做SMB Join(Sort Merge Bucket Join)时,两张表都按关联键分桶,可以在桶级别直接做关联,大幅减少Shuffle的数据量;二是做随机抽样时,分桶表可以直接从某个桶里取数据,效率远高于全表扫描。
文件格式方面,当时主流的选项是TextFile、SequenceFile、ORC和Parquet。TextFile虽然可读性好,但存储空间和查询性能都比较差。ORC和Parquet都是列式存储,压缩比高、查询时只需要读取涉及的列,是生产环境的主流选择。ORC在Hive生态里表现更优,Parquet在Spark生态里兼容性更好,选型时主要看你所在公司的算力引擎是偏向哪个。我当时在试卷上写的是ORC + Snappy压缩,理由很实在:ORC的列式存储和谓词下推能力对典型数仓查询场景的优化效果非常明显,Snappy压缩则在压缩比和解压速度之间取得了较好的平衡,适合大多数分析型负载。
4.2 数据倾斜:七种场景和解决方案
数据倾斜是Hive面试的高频考点,也是笔试SQL题里经常埋的坑。我当时遇到的一道题是:统计每个商品的成交金额,按金额倒序输出Top10。看起来很简单,但如果某个爆款商品的成交记录特别多,这个Key的Reduce Task就会成为瓶颈,整个任务可能就因为这一个Key跑不动。
数据倾斜的本质是数据分布不均衡,导致某些Reduce Task处理的数据量远超其他Task。常见场景包括:JOIN时关联键大量相同、GROUP BY时分组键极度集中、COUNT(DISTINCT)去重值太多、ROW_NUMBER()窗口函数结合PARTITION BY后某个分区数据量过大。解决方案要分场景来说,不能一上来就加参数:
第一种,空值引发的倾斜。很多表的关联键里会有大量空值,JOIN时这些空值会全部落入同一个Reduce Task。解决办法是给空值加随机前缀,比如IF(user_id IS NULL, CONCAT('random_', RAND()), user_id),这样空值会被打散到不同的Task里,而不影响最终计算结果。
第二种,热点Key引发的倾斜。比如爆款商品、大V用户这种数据量特别集中的Key。可以先统计出TopN热点Key,把数据拆成“热点数据”和“非热点数据”两个集合,热点数据单独处理,非热点数据正常聚合,最后把两部分结果合并。
第三种,GROUP BY的聚合倾斜。如果只是做简单的COUNT、SUM,可以开启Map端预聚合,参数是hive.map.aggr=true,让每个Map Task先做一轮局部聚合,大幅减少进入Reduce的数据量。如果聚合的字段基数非常大,比如COUNT(DISTINCT user_id),则可以考虑用GROUP BY加子查询的方式替换,分两步去重。
第四种,动态分区导致的小文件倾斜。当动态分区时某些分区的数据量特别大,会产生大量小文件。解决办法是调整hive.exec.dynamic.partition.mode和hive.merge.smallfiles.avgsize等参数进行小文件合并。
4.3 拉链表与缓慢变化维:历史数据怎么存
方案设计题里还藏了一个进阶考点,问的是“用户维度表如何保存历史变化”。这是典型的缓慢变化维问题,数仓面试里常年出现。最常用的方案是拉链表,也叫SCD2(Type 2 SCD,缓慢变化维类型二)。
拉链表的核心思路是:每次数据变化时,不修改旧记录,而是新增一条记录,用start_date和end_date两个字段标识这条记录的有效期。比如一个用户把会员等级从普通会员升级成VIP会员,拉链表里会保留一条旧记录,end_date设置为升级前的一天,同时新增一条新记录,start_date为升级当天,end_date为9999-12-31表示当前有效。查询某个时间点的用户状态时,只要带条件'2024-06-01' BETWEEN start_date AND end_date,就能准确还原当时的用户画像。
拉链表的设计有几个细节容易踩坑。一是全量快照表每天全量保留会导致存储爆炸,而拉链表只在数据变化时新增记录,存储量相对可控。二是每日更新拉链表时需要做两表关联,用业务主键关联当日增量表和历史拉链表,新增的记录加进去,变化的记录做闭链和开链,逻辑要理清楚。三是在笔试或面试时,要主动提到对偶发的维度属性变化(比如用户性别填错了)可以单独做修正,不必走拉链逻辑,否则会产生大量无意义的历史版本,大大增加使用时的复杂度。
5. 校招笔试实战:时间分配、答题技巧与高频错题
5.1 时间分配和做题顺序
笔试时间一般是一个半小时到两个小时,题量不大但很烧脑,时间分配直接决定你的得分上限。我的个人策略是:拿到卷子先把所有题目扫一遍,判断每道题的分值和难度,然后按照“理论基础题 → 方案设计题 → SQL实操题”的顺序来做。
这看起来有点反直觉,因为很多人习惯先做SQL题,觉得趁手感热赶紧写代码。但我的经验是,理论基础题是最快拿到“保底分”的,简答题只要复习过就能写,花的时间少、得分率高。方案设计题需要深度思考,建议放在第二位做,因为此时大脑状态最好,能把模型边界想清楚。SQL题放到最后,一方面是因为它思路清晰、写起来快,另一方面是如果时间不够,至少前两部分的分数已经拿到手,不会因为SQL卡壳而丢了大部分分数。
5.2 高频错题实录
总结下来,数仓笔试题有四个高频错点,几乎每次考试都有人栽。第一个是订单金额的重复计算。统计订单数时,如果直接对订单明细表做COUNT(*),会把包含多个商品的订单当成多笔订单,导致指标虚高。正确做法是:统计订单数要COUNT(DISTINCT order_id)或直接查订单事实表,统计商品件数才用明细表。
第二个是高基数去重的性能优化。COUNT(DISTINCT user_id)在数据量大时性能极差,因为它会产生极大的数据倾斜和Shuffle压力。可以改写为先GROUP BY user_id做子查询,再对外层做COUNT(1),通过两步去重来降低单个Task的压力。如果数据量更大,还可以考虑用近似去重算法(如HyperLogLog),在精度和性能之间做取舍。
第三个是窗口函数的排序陷阱。ROW_NUMBER()、RANK()、DENSE_RANK()这三个函数的区别一定要搞清楚。ROW_NUMBER()对相同值的记录也强制分配不同序号,RANK()对相同值分配相同序号但会跳号,DENSE_RANK()对相同值分配相同序号且不跳号。笔试经常让你统计每类商品销售额前五,如果销售排序合理就用DENSE_RANK(),否则会出现并列名次但被截断的情况。
第四个是时间函数的边界条件。比如统计“最近30天”的数据,到底是用DATE_SUB(CURDATE(), 30)还是DATE_SUB(CURDATE(), 29),取决于需求里“最近30天”是否包含今天。笔试时一定要先明确时间口径再写SQL,这种边界问题往往是出题人故意埋的坑。
6. 回看这份试卷:数仓校招考察的核心能力清单
6.1 校招考察的核心能力清单
把整份试卷映射回能力模型,其实可以发现数仓校招真正在筛选的能力就三类。
第一类是结构化思维能力。数仓工程师日常面对的问题几乎都是非结构化的:一份模糊的需求、一堆杂乱的表、一个说不清的指标口径。能不能把复杂问题拆解成层次分明的子问题,能不能从业务过程出发梳理出清晰的数据链路,这是笔试方案设计题真正想考察的东西。第二类是数据敏感性。同一个指标,从不同表里查出来的结果为什么不一样?订单金额和实付金额到底差在哪里?数据同步延迟了,是补数还是等下一轮调度?这种对数字差异的敏感度,是做数仓的基本功。第三类是技术基本功。SQL是数仓工程师的通用语言,Hive、Spark、Flink这些引擎是生产力工具,这些硬技能没有捷径,只能靠多写多用慢慢积累。
6.2 给后来人的几点建议
最后分享几条可以复制的经验。如果你正在准备数仓校招,不要只刷题,更不要只背概念。建议自己动手搭一套完整的数仓demo,不需要多复杂,用MySQL或者PostgreSQL模拟业务库,用Hive做离线数仓,或者直接用Spark SQL也行。把一个电商订单的数据从ODS到ADS完整跑一遍,期间会遇到各种数据问题,这些问题才是你面试时最有价值的素材。“我遇到过什么坑、怎么排查的、最终怎么解决的”,比“我能背诵数仓分层的定义”更有说服力。
另外,平时多看真实的业务报表和数据分析报告,培养对指标口径的敏感度。很多应届生对“GMV”“DAU”“转化率”这些概念都停留在字面理解,但一旦被问到“为什么今天DAU涨了10%”“GMV里到底含不含未支付订单”,就答不上来。数仓工程师是业务的翻译官,你翻译的前提是真正理解业务在说什么。
那份2018年的试卷早已成为历史,但数据仓库这门手艺的价值一直在增长。希望这篇复盘能帮正在准备数仓岗位的朋友少走一些弯路,笔试只是起点,真正精彩的是你亲手建立起一套数据体系、让数据真正驱动业务决策的过程。按我个人的经验,把这份试卷吃透,你收获的不只是一个offer,更是一整套分析和解决问题的方法论。