ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大数据开发SQL面试高频考点与优化实战解析

大数据开发SQL面试高频考点与优化实战解析

1. 为什么SQL是大数据开发面试的“生死线”

做了这么多年大数据开发,面试过不少人,也被面试过不少次,一个很真实的感受是:SQL这关过不了,其他都白搭。

你Spark写得再溜、Flink状态管理理解得再深,落到面试官面前,他大概率还是会先问你几道SQL题探探底。原因很简单:大数据开发日常工作中,SQL是使用频率最高的语言。离线数仓用HiveSQL,实时计算用FlinkSQL,即席查询用Presto/ClickHouse,底层的HBase、Kafka虽然不直接用SQL,但周边生态的工具多多少少都带SQL影子。可以说SQL就是大数据开发者的通用语言,面试官用SQL题来筛选候选人,成本低、区分度高、可考察维度广。

很多候选人有个误区,觉得SQL就是写个select、join、where,能跑出结果就行。但面试官真正想考察的,是你对SQL执行逻辑的底层理解、对数据处理的思维模式、以及对性能优化的敏感度。同一个需求,有人写出来的SQL跑半小时,有人改写后三分钟跑完;有人面对“取每个部门薪资最高的员工”这种题能想到三种写法并分析优劣,有人只会一种写法还说不清原理——这就是差距。

这篇文章围绕面试中出现频率最高的几类SQL考点展开:in/exists的语义区别与执行逻辑、having/where的过滤时机差异、排序函数的进阶用法、以及常考的实际场景题。内容是结合我这些年面试与被面试的经验整理的,题目都是高频题,解析部分既讲答案,也讲为什么是这个答案,争取让你看完之后不但会做题,还能在面试现场把思路讲清楚,给面试官留下“这人是真懂SQL”的印象。

2. 面试前必须吃透的四个SQL核心概念

2.1 in与exists:不只是“用哪个”的问题

in和exists是面试题里的常客,几乎每三场面试就会遇到一次。很多人背过结论:“小表驱动大表用in,大表驱动小表用exists”,但这个结论本身是片面的,而且很容易让你在面试官的追问下露馅。

先看语义层面的区别。in是匹配判断,它的逻辑是:外层查询的某个字段值是否出现在内层查询返回的集合中。比如:

select * from emp where dept_id in (select dept_id from dept where location = '北京');

exists是存在性判断,它的逻辑是:对于外层查询的每一行,去执行内层子查询,如果子查询有返回结果,则这一行保留。写法上有一个容易被忽略的细节,exists子查询的select列表理论上写什么都行,因为它根本不关心返回了什么列,只关心有没有返回行:

select * from emp e where exists (select 1 from dept d where d.dept_id = e.dept_id and d.location = '北京');

这里select 1是习惯写法,写select *也没错,但容易让人误解以为exists会取列值,实际上它只看是否存在满足条件的行。

面试官最爱追问的点是:这两者的执行效率哪个高?这就涉及执行计划的差别。经典的逻辑是,in会先执行子查询,把结果集物化成一个临时表,然后外层查询与这个临时表做连接;exists则是典型的“外层驱动内层”,每处理一行外层数据就去执行一次内层子查询。基于这个逻辑,才有了“外层结果集小用exists、内层结果集小用in”的说法。

但这里必须补充一个重要认知:现代的大数据引擎和关系型数据库优化器,早就不是按教科书逻辑执行了。Hive从0.8版本左右开始支持in和exists的语义转换,SparkSQL的优化器会在物理执行前对逻辑计划做大量改写,很多情况下in和exists会被改写成同一种Join策略。所以面试时如果只谈“exists永远比in快”或者反过来,反而暴露了你对优化器了解不够。

更稳妥的回答思路是:从语义出发选型——如果内层子查询的结果集可能包含NULL,in的行为会变得微妙(见后面的坑点);从性能角度表态——具体哪个快取决于优化器、数据规模和数据分布,不能一概而论,但我们可以通过执行计划(explain)来判断实际执行策略。这个回答既展现了理论功底,又体现了工程意识,面试官很难挑毛病。

再补充一个实际工作中常用的经验:当in和exists都能表达同一需求时,我更倾向于用in。原因一是可读性好,二是大多数引擎对in的优化支持比较成熟,三是exists的嵌套写法在复杂SQL中容易造成阅读负担。但要注意控制in后面的集合大小,几万个元素的in列表会让很多引擎的优化器直接放弃治疗,这时候就该换成join或者拆分成临时表。

2.2 having与where:一字之差,天壤之别

having和where的考点比较基础,但恰恰是基础才最能看出功底。很多候选人能说“where是过滤行,having是过滤分组”,但说不清楚深层的原因。面试官追问一句“为什么聚合函数不能用在where里”,就卡住了。

核心区别在于执行时机。where是在分组和聚合之前执行的,它作用于原始数据行;having是在分组和聚合完成之后执行的,它作用于分组后的结果集。这就解释了为什么where里不能写聚合函数:因为where执行的时候,分组还没发生,聚合值根本还没计算出来。你不可能在结果还没算出来之前就去过滤它。

举一个经典的例子,查询部门平均薪资大于8000的部门:

select dept_id, avg(salary) as avg_sal from emp group by dept_id having avg(salary) > 8000;

如果把having换成where,写成where avg(salary) > 8000,大多数引擎会直接报错,告诉你无效使用聚合函数。即使个别引擎对这种写法做了兼容,逻辑上也是错误的,因为过滤时机不对。

还有一个隐藏考点很多人没注意:where和having共存时的执行顺序。看这个SQL:

select dept_id, count(*) as cnt from emp where salary > 3000 group by dept_id having count(*) > 10;

它的执行逻辑是:先过滤掉薪资低于3000的员工,然后按部门分组计数,最后只保留人数大于10的部门。也就是说,where过滤后的数据参与了聚合,having过滤的是聚合结果。如果这个需求写成where salary > 3000且having count(*) > 10,中间被过滤掉的低薪资员工不参与任何聚合计算,这会影响count的结果——很多人没意识到这个连带效应。

再延伸一个点:能不能用where替代having?在特定场景下可以,比如过滤条件不涉及聚合函数且分组字段就是原始列时:

select dept_id, count(*) from emp group by dept_id where dept_id = 'A';

这个写法有语法问题,正确的做法是先用where过滤再分组:

select dept_id, count(*) from emp where dept_id = 'A' group by dept_id;

这里就体现出写SQL的思维方式:能用where提前过滤的,尽量不要拖到having。因为提前过滤能减少进入分组操作的数据量,对性能有明显的正面影响。这个点如果能在面试中主动讲出来,就是加分项——说明你不只是会写SQL,还关心SQL的执行效率。

2.3 排序函数三兄弟:row_number、rank、dense_rank

排序函数是面试题的高频考点,尤其是“取分组TopN”这类需求,几乎必考。很多候选人知道有三个排序函数,但区别说不清楚,或者动手写的时候用错函数,导致结果不对。

这三个函数的区别用一个例子就能说透。假设有四个人的薪资分别是10000、9000、9000、8000:

  • row_number():不管有没有并列,都按顺序编号,结果是1、2、3、4。
  • rank():有并列时占用名次,相同值的行并列同一个名次,但下一个名次会跳号,结果是1、2、2、4。
  • dense_rank():有并列时也并列名次,但下一个名次不跳号,结果是1、2、2、3。

记住一句话:rank跳号,dense_rank不跳号,row_number唯一编号。这个考点在面试中不光会以理论题形式出现,还会包装成“取每个部门薪资排名前3的员工”“按班级成绩排名并显示并列名次”等场景题。

这三个函数都属于窗口函数。窗口函数的执行顺序也经常被问到,要记住一个大原则:窗口函数是在where、group by、having之后执行的,也就是说窗口函数能使用的数据是“过滤完、聚合完”之后的数据。这带来一个很常见的坑:如果你想对分组聚合后的结果再排序取TopN,直接用where去过滤窗口函数的结果是行不通的,因为where的执行时机早于窗口函数。

比如这个错误写法:

select dept_id, salary, row_number() over (partition by dept_id order by salary desc) as rn from emp where rn <= 3;

这个SQL在绝大多数引擎里会直接报错,因为rn这个别名是在select阶段生成的,而where执行得更早,根本不知道rn是什么。正确做法是套一层子查询:

select dept_id, salary, rn from ( select dept_id, salary, row_number() over (partition by dept_id order by salary desc) as rn from emp ) t where rn <= 3;

这个“子查询包一层再过滤”的手法,是TopN类题目的标准解法,一定要熟练。面试时写出这种结构,并且解释清楚为什么要套子查询,就说明你真正理解窗口函数的执行顺序。

2.4 group by与聚合函数的执行逻辑

group by是SQL面试的另一个基础考点,单独拿出来说,是因为很多人在复杂场景题中栽在group by的理解上。

group by的核心逻辑是:按照指定的字段把多行数据合并成若干个分组,每个分组内的多行数据被压缩成一行输出。压缩的过程中,select列表中出现的字段只有两种:group by中的字段,或者被聚合函数包裹的字段。其他字段如果想出现在select里,必须也写在group by中,否则就是非法SQL。这个规则是SQL的语法规定,底层逻辑是:分组后每个组只剩一行,非分组字段有多行值,引擎不知道该取哪一行。

有一个容易踩坑的点是:有些引擎(比如MySQL的部分模式下)允许select非分组字段,返回的是组内的随机行数据。这在面试时如果被问到,要能明确指出这是不规范写法,生产环境中应该避免,因为结果不可控。

聚合函数与group by配合使用时,有一个顺序问题需要记住:先分组,后聚合。聚合函数作用于每个分组内的所有行。也就是说,count(*)统计的是每个分组内的行数,sum(salary)计算的是每个分组内的薪资总和。理解了这个,就能明白为什么having可以直接使用聚合函数——因为having执行时聚合结果已经计算出来了。

另外,很多人在写“分组后取组内某一行”这类需求时,会不自觉地用group by,但实际group by并不适合这个场景。比如取每个部门薪资最高的员工,目标是“每个部门返回一条记录,且这条记录是薪资最高的那个人的完整信息”。如果用group by,select列表里除了dept_id和max(salary),其他字段要么不能出现,要么出现但取值不确定。这种需求的标准解法是窗口函数或者自连接,而不是group by。这个区分非常关键,面试官出这种题,就是想看你能不能选对工具。

3. 面试必考的高频SQL场景题与手写解析

3.1 场景题一:取每个部门薪资最高的员工(TopN问题)

这是大数据开发面试中出现频率最高的一道题,没有之一。某公司、某大厂,无论大小厂都喜欢出这道题,因为解法多样、可深挖的点多。我在面试中让候选人写这道题,最少见过三种写法,每种写法的优劣能直接反映候选人的SQL水平。

先看标准解法,用窗口函数:

select dept_id, emp_name, salary from ( select dept_id, emp_name, salary, row_number() over (partition by dept_id order by salary desc) as rn from emp ) t where rn = 1;

这个解法的核心逻辑是:先按部门分组,在组内按薪资降序编号,然后取编号为1的那一行。这里用的是row_number而不是rank或dense_rank,因为需求是“薪资最高”,如果有两个人并列最高,row_number会随机取一个,rank会返回两行——具体用哪个取决于是否允许并列,这在需求分析时就要问清楚。面试时主动问“如果薪资并列怎么处理”是一个加分的提问。

第二种解法是用自连接,思路是找出“不存在薪资比我高的人”的那些记录:

select e1.dept_id, e1.emp_name, e1.salary from emp e1 left join emp e2 on e1.dept_id = e2.dept_id and e2.salary > e1.salary where e2.emp_id is null;

这个解法的逻辑很巧妙:把自己和同一部门薪资更高的人做连接,如果能连接上,说明存在比我薪资高的人,那我不可能是最高;连接不上(e2为空)的,就是部门里的最高薪资。不过这个解法在大表场景下性能比较差,因为自连接会产生大量的中间数据,而且如果两张表的数据量都很大,这个join的成本会很高。

第三种解法是通过group by加子查询:

select dept_id, emp_name, salary from emp where (dept_id, salary) in ( select dept_id, max(salary) from emp group by dept_id );

先按部门求最大薪资,再反查哪些员工的部门与薪资对上。这个解法的可读性不错,但有一个隐藏问题:如果同一部门有两个人薪资相同且都是最高,这两个人都会被查出来。在需要唯一记录的场景下,这个解法需要额外处理。

面试时推荐优先展示窗口函数解法,因为结构清晰、性能可控、可扩展性强。能从这种题延伸到“每个部门薪资前3名”只需要把where rn = 1改成where rn <= 3,说明你对这个考点有完整的理解。

3.2 场景题二:连续登录天数问题(经典连环追问)

连续N天登录问题是面试官非常喜欢的进阶题,难度不小,考察的是窗口函数lag/lead、日期处理、以及分组思想。这道题第一次接触可能会卡壳,因为直观上不太好确定“连续”怎么判断——直接按用户分组数日子的话,中断的日子没法处理。

标准解法是“行号差值法”。先看核心步骤:

select user_id, d, date_sub(d, rn) as grp from ( select user_id, log_date as d, row_number() over (partition by user_id order by log_date) as rn from login_log ) t;

这里的思路是:如果用户的登录日期是连续的,那么日期减去行号得到的值应该相同。比如用户A在1号、2号、3号登录,行号分别是1、2、3,日期减行号分别是0、0、0,同一个值;如果1号登录、3号登录,行号是1、2,日期减行号是0、1,值不同。这样就把“连续区间”的识别转化成了“同一分组”的问题。

然后在此基础上统计每组的天数:

select user_id, grp, count(*) as continuous_days from ( select user_id, log_date, date_sub(log_date, rn) as grp from ( select user_id, log_date, row_number() over (partition by user_id order by log_date) as rn from login_log ) t1 ) t2 group by user_id, grp;

再求每个用户的最大连续天数,再套一层:

select user_id, max(continuous_days) as max_days from ( select user_id, grp, count(*) as continuous_days from ( select user_id, log_date, date_sub(log_date, rn) as grp from ( select user_id, log_date, row_number() over (partition by user_id order by log_date) as rn from login_log ) t1 ) t2 group by user_id, grp ) t3 group by user_id;

面试中写这道题时,关键是讲清楚“日期减行号”这个核心思想。我在实际工作中第一次遇到这个需求时,也是想了半天——直觉上想用lag或lead判断前后日期是否相邻,但那样写出来的逻辑很复杂,而且如果连续区间多,代码会非常啰嗦。行号差值法把复杂问题转化成了分组统计问题,代码简洁而且逻辑清晰。

另外要注意日期函数的差异。上面示例用的是Hive风格的date_sub函数,MySQL里是date_sub(log_date, interval rn day),Oracle是log_date - rn。面试时可以说明自己最熟悉哪种引擎的语法,并指出迁移时需要注意日期函数的差异,这会让面试官觉得你有跨引擎开发经验。

3.3 场景题三:找出连续出现3次及以上的数字

这是一道在数据库面试领域流传很久的经典题,核心考察点是表自连接或者窗口函数lag。题目大概是:有一个表记录了某个数字序列,要求找出连续出现至少3次的值。

教科书解法是用三表自连接:

select distinct l1.num from log l1, log l2, log l3 where l1.id = l2.id - 1 and l2.id = l3.id - 1 and l1.num = l2.num and l2.num = l3.num;

这个解法的逻辑直白:找三行相邻的记录,且数字相同。但缺点也很明显——如果问题改成“连续出现5次”,就要写五表自连接,非常笨拙。

更好用的是lag窗口函数解法:

select distinct num from ( select num, lag(num, 1) over (order by id) as prev1, lag(num, 2) over (order by id) as prev2 from log ) t where num = prev1 and num = prev2;

lag函数可以往前取任意偏移量的值,把“往前看两次”的逻辑直接写出来。改成“连续出现5次”只需要增加lag(num, 3)、lag(num, 4)两个列,条件改成num等于四个prev即可。

这道题的难点其实不在语法,而在思维方式。很多人第一次看到这个题,会想按num分组,但按num分组后id的顺序信息就丢了,没法判断“连续”。正确的思路是:用id排序,然后往前看N行做比较。面试中如果你能指出“分组不行,必须保序”这个关键点,面试官会非常认可——说明你抓住了问题的本质。

3.4 场景题四:in与exists的实战对比

既然前面花了很大篇幅讲in和exists,这里给一道典型的实战题来检验你的理解。

题目背景:有两张表,orders是订单表(字段:order_id, cust_id, order_date, amount),customers是客户表(字段:cust_id, cust_name, level)。需求是查出所有在2024年下过订单且订单金额大于500元的客户信息。

exists写法:

select c.* from customers c where exists ( select 1 from orders o where o.cust_id = c.cust_id and o.order_date between '2024-01-01' and '2024-12-31' and o.amount > 500 );

in写法:

select c.* from customers c where c.cust_id in ( select cust_id from orders where order_date between '2024-01-01' and '2024-12-31' and amount > 500 );

两条SQL结果一样,执行逻辑有差异。exists是逐行驱动,in是先生成子查询结果集再匹配。前面说了现代优化器可能会改写执行计划,但有一个本质区别需要注意:exists对NULL的处理更安全,而in遇到子查询结果包含NULL时,匹配逻辑会变得不可预期。

实际场景中,我喜欢用in,因为可读性好,而且大多数引擎对in的转换优化已经非常成熟。如果数据量级很大,比如orders表日增量上亿,这类SQL在大数据引擎里跑,in和exists最终大概率都会被优化成同一种join方式。但如果面试官追问性能,要说清楚:具体看执行计划,建议用explain确认有没有走广播join、有没有数据倾斜风险,而不是背结论。

4. 大数据场景下的SQL优化分析

4.1 数据倾斜在SQL中的表现与应对

大数据开发面试里,SQL题写到后面,面试官一定会往数据倾斜这个方向引导。数据倾斜是分布式计算中数据分布不均导致某个任务执行时间远大于其他任务、拖慢整个作业的现象。它可以说是大数据开发岗位上最核心的调优话题之一。

在SQL层面,数据倾斜最常见于group by和join操作。以group by为例,如果某个key的值特别多,比如订单表里某个商品的订单量占了总量的90%,按这个key分组聚合时,处理这个key的任务就要处理90%的数据,直接拖垮整体。join场景更明显,大表关联小表时如果关联键分布不均,个别任务的数据量会远超平均水平。

面试中问到数据倾斜,很多候选人能说出“加盐”这个词,但说不清完整方案。加盐的思路是:给倾斜的key打散,让原本集中在一个任务的数据分散到多个任务处理。具体SQL写法分两步——第一步先找出可能倾斜的key,比如在Hive里可以用:

select key, count(*) as cnt from big_table group by key order by cnt desc limit 10;

第二步是对这些倾斜key做加盐处理,比如给key拼接一个随机数后缀:

select concat(key, '_', floor(rand() * 10)) as salted_key, count(*) from big_table where key in (select key from t_skew_keys) group by salted_key;

加盐后需要在结果上再做一次去盐聚合。这个方案的核心是:把大key拆成10个随机小key,每个小key的任务量降为原来的十分之一,但要注意加盐之后需要额外的步骤还原。

面试时如果能主动提到Hive对这个场景有一个更简单的参数开关,比如数据倾斜聚合开关(处理group by倾斜时,可以将部分数据在map端先做部分聚合),效果会更好——说明你不光知道原理,还了解实际生产中的工具手段。

4.2 join优化:广播join与map join

SQL性能优化还有一个高频考点,就是join方式的选择。面试官会问“大表关联小表有什么优化手段”“什么是广播变量”。

核心思想不难:如果一张大表和一张小表做join,把大表广播到所有节点、代价太大,反过来把小表广播到每个节点,每个节点都在本地完成join,可以避免shuffle。这个思想对应到技术实现,是Spark里的broadcast join、Hive里的map join。写法上,在Hive中可以通过强制提示(map join hint)把join变成map端join;在SparkSQL里,优化器会根据表的大小自动选择广播join,也可以通过参数调整广播阈值。

写SQL时的实践建议是:把需要广播的小表放在join的右侧。因为很多引擎对join顺序是敏感的,虽然新一代优化器大多能自动调整,但养成这个习惯没有坏处。更重要的是,要能识别什么样的表适合广播——维度表(部门表、地区表、产品表)通常是小表,事实表是大表,雪花模型和星型模型里的常见join场景,基本都是事实表关联维度表,这就是天然的广播join场景。

另外一个容易被忽略的优化点是:join之前先做过滤和字段裁剪。比如关联两张表时,与其把两张表的全部字段都select出来再过滤,不如在子查询里先做好过滤和裁剪,减少进入join的数据量。很多人写SQL不注重这一步,导致join的输入数据量比实际需要的大很多,作业跑得慢还找不到原因。

4.3 从执行计划反推SQL问题

在实际工作中,遇到SQL跑得慢,我的第一反应永远是先看执行计划,而不是凭感觉改SQL。这个习惯在面试中如果展现出来,会是一个很大的加分项。

在Hive里执行explain select ...可以看到执行计划,在SparkSQL里是explain or extended,在MySQL里是explain。通过执行计划能看出:join是BroadcastHashJoin还是SortMergeJoin,每个stage的输入数据量,有没有出现不必要的shuffle,过滤条件下推是否生效等。

举个例子,我排查过一个SQL慢的问题,业务方说“这个SQL之前跑几分钟,现在要跑一小时”。打开执行计划一看,发现关联键上有一个函数包裹:join条件变成了on substr(a.key, 1, 5) = b.key,相当于给关联键做了函数变换,导致优化器无法使用高效的join方式,只能每条数据都做一次函数计算,再回表匹配,性能断崖式下降。这是非常典型的隐藏问题,代码审查和普通日志都发现不了,只有看执行计划才能定位到。

面试中如果被问到“SQL优化的一般步骤”,比较完整的回答是:先explain看执行计划,确认有没有全表扫描、shuffle是否过多、join方式是否合理;再根据执行计划反推问题,可能是join键分布不均、可能是过滤条件下推失败、可能是join方式不优;最后针对问题选择方案。这个回答既展示了方法论,又体现了实际经验。

4.4 分区分桶与谓词下推

面试中另一个必然出现的优化考点是分区表的原理。分区的本质是物理存储上的目录划分,水平切分数据,查询时通过分区裁剪只读需要的目录,大幅减少扫描量。在Hive里建分区表的标准写法是:

create table partitioned_table ( id bigint, name string ) partitioned by (dt string) stored as parquet;

查询时指定分区条件,就可以让引擎只扫描对应分区目录下的数据。这个机制对应到大数据面试,核心考点是“谓词下推”(predicate pushdown)——优化器会把where条件尽可能下推到数据扫描阶段,让引擎在读取数据时就过滤掉大部分数据,减少shuffle阶段的数据量。

但有一个实际使用中的常见坑:如果在where条件里对分区字段做了函数处理,比如where substr(dt, 1, 6) = '202401',很多引擎就无法把这个条件下推成分区裁剪了,因为优化器识别不出你是想过滤某个分区,它只能认为你是在对所有分区做完函数计算后再过滤。结果就是全分区扫描,性能灾难。正确写法是直接写成where dt >= '202401' and dt < '202402',保留字段的原始形态,让优化器可以准确裁剪分区。

这类问题面试时考察的不是你背不背得住命令,而是你有没有在生产环境里踩过坑、有没有形成“保留字段原始形态以便下推”的肌肉记忆。

5. 现场面试的答题套路与准备策略

5.1 拿到SQL题的五个思考步骤

面试现场写SQL和平时工作写SQL有很大区别:有时间的压力、有面试官的目光注视、有被打断追问的风险。很多候选人一紧张,拿到题就开始写,结果写了一半发现思路错了,又划掉重来,给面试官的印象非常差。

经过我多次模拟面试和真实面试的经验,总结出一个拿到SQL题后的标准思考流程,照着做可以最大程度避免草率动笔:

第一步:确认需求。把题目用自己的话复述一遍,尤其关注边界条件——需要去重吗?并列怎么处理?时间范围是什么?如果题目描述有歧义,一定要当场问清楚。面试官不会因为提问扣分,反而会觉得你是一个负责任的人。

第二步:确定核心算子。这个需求本质上是在做什么?是过滤、聚合、排序,还是join?很多时候题目会把多个需求混合在一起,你需要拆解出主算子。

第三步:确定关键字段和分组粒度。按什么字段分组?输出哪些字段?这个决定了group by写什么、select放什么。

第四步:选择SQL结构。是单表查询、多表join、还是子查询嵌套?需要窗口函数还是自连接?在动笔之前,心中应该有一个完整的结构框架。

第五步:动笔写,边写边讲思路。面试现场最忌讳闷头写、写完才说。正确方式是边写边简单说明:“我先用窗口函数按部门排序,然后外面包一层过滤取第一条”。这样即便写到一半出错,面试官也知道你的整体思路是对的。

5.2 被追问时如何应对

面试官在你写完SQL后,几乎一定会追问。追问的水平一般有两种:一种是顺着你的答案深挖,比如你用row_number写了TopN,他会问“如果薪资并列怎么办”;另一种是抛出反例或极端场景,比如“如果同一用户每天登录多次怎么办”。

应对追问的底层策略是:不要急着回答,先理解追问背后的意图。比如问“如果薪资并列怎么办”,背后的意图是考察你是否知道rank和dense_rank的区别、是否能根据需求选择正确的函数。这时候不要只说改成rank就行了,而是说明并列情况下没有明确需求时的默认处理方式,以及如何通过增加次级排序字段来稳定排序结果:

row_number() over (partition by dept_id order by salary desc, emp_id asc) as rn

再比如“每天登录多次”的追问,意图是考察你处理数据时有没有考虑数据质量。正确回应是:先确认原始数据中的log_date是日期还是时间戳,如果是时间戳需要先格式化成日期或者用to_date函数做转换,然后在分组前先对用户和日期去重,因为连续登录天数关注的是“天数”,不是“次数”。

对于自己不确定的问题,诚实说不知道,但可以补一句“但如果在生产环境中,我一般会通过...来验证”或者“让我想一想,是否可以这样理解”。面试官讨厌的不是你不会,而是你乱编。

5.3 面试前SQL复习清单

最后整理一个面试前三天可以照着过一遍的复习清单:

第一,窗口函数要熟练。row_number、rank、dense_rank、lag、lead、sum() over()这六个必须能手写出来,并且清楚各自在什么场景下使用。取TopN用row_number,连续次数用lag,累计值用sum() over()。

第二,group by的语法规范和聚合函数的配合要吃透。知道select列表为什么只能放分组字段或聚合字段,知道having和where的使用时机。

第三,in/exists的语义区别,以及子查询中NULL的影响。能说出各自执行逻辑的不同,不要只说“谁快谁慢”。

第四,join的类型和区别——inner join、left join、right join、full join的结果集特征要一清二楚。尤其left join在on里写过滤条件和在where里写过滤条件的结果差异,这是一个高频陷阱题。

第五,掌握四到五道经典场景题的多种解法。连续登录天数、分组TopN、行列转换、累计求和、相邻记录差,这几个题型覆盖了大部分SQL面试题的变化。

第六,准备好一个“慢SQL优化实例”的讲述材料。面试官问“你有没有优化过慢SQL”,如果你能按“现象-排查-定位-解决-结果”的叙事结构讲出一个完整案例,这个印象分会非常高。

我一直跟身边准备面试的朋友说:SQL面试题不要靠背题,要理解每一种语法背后的数据操作逻辑。你真正理解了一个机制,题目怎么换都能应对;你只是背了答案,面试官换一个问法就暴露了。这篇文章里的内容,如果你能自己动手把每个例子跑一遍,再基于这些例子扩展出两三个自己的变体题目,面试前能做到这个程度,SQL关基本稳了。

最后再分享一个小习惯:日常工作中多留意自己写过的SQL,慢的、快的、写得别扭的,都复盘一下“为什么”和“怎么改”。积累到一定程度,面试题在你眼里就不再是考题,而是老朋友。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进