过去三年,我陆续参与了六家公司校招与社招的数据分析岗位面试,经手的SQL笔试答卷超过800份。完整通过率大约是21%,而在这21%里,能够写出让面试官直接认可、不用追问的答案的人,还要再减掉一半。这个数字一直提醒我:真正的差距几乎不在语法记忆上,而在读题方式、边界处理和执行成本判断上。多数人不是不会SQL,而是不会在笔试场景里把业务问题翻译成一套严谨、可验证、性能可控的查询逻辑。
我把SQL笔试题目拆成三个层次:第一层是基础读写能力,包括SELECT、JOIN、GROUP BY、WHERE、HAVING;第二层是逻辑编排能力,包括子查询、窗口函数、CTE、case when 的嵌套组合;第三层是工程判断能力,包括结果正确性之外的性能、容错、可读性和数据口径判断。
三层能力在真实笔试中的分值占比并非平均。根据我统计的120份真题总和,基础读写占大约35%,逻辑编排占40%,工程判断占25%。可大多数备考者把90%的精力都花在了第一层,导致拿到中高难度题时直接卡住。

逐份追踪错误答卷后,我发现一个规律:大量答案的语法是完全正确的,数据库也能跑出结果,但结果和业务问题对不上。最常见的表现是把“每个用户最近一笔订单”写成“总订单表最近一笔订单”;把“连续登录天数”套上分组排序后没有剔除中间断开的部分。
这说明许多人拿到题目后立即写SQL,而不是先问自己:最终输出的每一行是什么粒度?每条记录代表“一个用户”还是“一次行为”?过滤条件应该作用在原始行还是聚合之后?这三个问题没想清楚,结果一定错。
我给300份错误答案做过标记,发现错误点高度集中:NULL处理错误占28%,关联条件遗漏占22%,窗口函数排序字段选错占19%,子查询性能忽略占17%,数据口径理解偏差占14%。这些都不是冷门语法,全是日常开发最高频的操作。

一次面试,我设计了一道题:订单表orders包含user_id、order_date、amount三个字段,要求找出连续3个月都有下单的用户。候选人A用了一个300行的自连接方案,跑了5分钟没出结果。候选人B先按用户和月份去重,再用lag函数错位比较,30秒出结果。
差距不在于谁更聪明,而在于候选人B先画了一个“连续月份判断”的时间轴,把问题拆成“去重→排序→错位相减→筛选”。这个拆解流程稳定且可复用,适用于连续签到、连续消费、连续活跃等一大批题目。

某次金融科技公司的笔试,题目要求统计“每日活跃用户的平均持仓金额”。很多候选人直接用每日用户的资产总和除以用户数,忽略了用户可能在当天没有持仓变化、系统只在变动时记录快照。正确答案需要先从持仓流水表找到每人当日的最后一条记录,再取金额求平均。
这类题目考的不是SQL技巧,而是对数据表主题和事件语义的理解。字段名中含有流水、日志、快照、变更等词时,通常需要先做增量重放或关键节点抽取,再进入聚合计算。
我还观察到一个耐人寻味的现象:在线笔试中,很多人会用“试错法”不断提交Python或SQL代码来猜结果;现场白板题中,他们则容易陷入沉默。这两种行为暴露的是同一个问题:缺少结构化解题框架。
我建议的方法是:无论什么环境,都先用两分钟写出“输入表→粒度变化→过滤时机→输出字段”四行笔记,然后再写任何代码。看起来多花时间,实际能降低无效试错成本。
不少备考者反复背诵关键字、练习各种join图,却很少拿真实业务数据做练习。语法默写在面经里看起来有用,但笔试题目几乎不会问“left join和inner join的区别”,而是给你两张表,问“为什么这个用户数对不上”。
你只有在真实数据上吃过亏,才知道join结果膨胀或缺失的原因,才能在笔试里一眼识别相同陷阱。
NULL在SQL里有三层含义:值不存在、值未知、值不可用。笔试中经常出现这样的题:统计用户表中没有填写手机号的人数,最简单答案就是COUNT(phone)和COUNT(*),但很多人会在WHERE phone = NULL上翻车。
更深层的例子:当两个表关联的字段含有NULL时,NULL行会被丢弃,导致统计结果偏小。如果你看到输出结果比业务预期少,优先检查关联键空值。
rank、dense_rank、row_number是笔试排名题的核心。区别在于并列时是否占用后续名次。真实的金融、电商业务报表里,这个差异直接改变TopN名单的长度。
我推荐记一个表格记忆:row_number给唯一编号,并列时随机或按序;dense_rank让并列占同一名次且下一个名次连续;rank让并列之后跳号。很多考生只记得函数名,却忽略了ORDER BY字段是否唯一。
笔试中“两表关联”字段不固定时,要使用范围关联或非等值关联,例如订单金额落在某个区间内。此时ON条件可能不止是等号。
如果直接忽略这个细节,表之间会产生笛卡尔积,结果膨胀几千倍。回答时最好先用WHERE限定业务范围,再最后检查关联条件是否足够筛除重复。
笔试中很少直接考执行计划,但面试官看到你试图在百万行数据上做复杂子查询却没有任何裁剪时,印象分会打折扣。
优化方向通常有三个:尽量先WHERE再JOIN、尽量用EXISTS替换IN、尽量用窗口函数替代自连接。成本意识是一种长期职业习惯,笔试时写2个不同解法并在注释里说明取舍,能明显提升答案的可信度。
我整理了120份实际笔试题目,发现出现频次极不均衡。多表关联几乎每场必考,出现率91%;聚合分组出现86%;去重逻辑出现79%;窗口函数出现68%;子查询出现57%;行列转换只在34%的题目中出现。

根据我面试和教研的观察:小型公司和创业公司笔试更偏重多表关联和基础聚合,因为团队需要快速取数、做报表;中型公司看重窗口函数和业务口径逻辑,因为他们开始做用户留存、转化漏斗;大厂则倾向考察复杂嵌套、性能优化和数据建模思维。

我作为面试官,看笔试卷子时会刻意关注三样东西:一、是否先对数据表做探查性理解;二、是否把复杂问题拆成临时表或CTE分步处理;三、是否在关键节点对可能出现的数据异常做注释。如果三道题都表现出这三个特征,即使最终答案有小瑕疵,也会给出不错的评价。
这给我一个启示:你写在卷面上的不只是代码,而是思考过程的可视化。所有中途的推导、假设、边界检查,都可以用注释形式留下痕迹,这会让人更放心把真正的分析任务交给你。
这是最经典的场景题。给定表orders,包含user_id、order_date、amount。需要输出每个用户最近一次下单的日期和金额。
SELECT user_id, order_date, amount FROM ( SELECT user_id, order_date, amount, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY order_date DESC) AS rn FROM orders ) t WHERE rn = 1;
这里有两个易错点:ORDER BY选择DESC时是否考虑同一用户同一天多笔订单;如果业务定义“最近一笔”要以订单创建时间而非下单日期为准,字段需要调整。若需要保留同日多笔订单,可以使用ORDER BY order_date DESC, created_at DESC。
这道题的判断点:是否用窗口函数而非自连接、是否注意到主键粒度。写完后最好再自查一下,order_date字段是否可能为空。
给定表products(id, category, sales),输出每个品类销量排名前三的商品。注意并列名次的不同处理。
SELECT category, id, sales, rk FROM ( SELECT category, id, sales, DENSE_RANK() OVER(PARTITION BY category ORDER BY sales DESC) AS rk FROM products ) t WHERE rk <= 3;
如果业务希望每个品类恰好3个商品,用ROW_NUMBER加随机或二级排序;如果允许并列,用DENSE_RANK。RANK与DENSE_RANK的区别在出现并列时非常关键,务必在题目备注里写清楚业务约定。
这是近年互联网公司最青睐的题型。给定表checkin(user_id, check_date),要求统计连续打卡7天及以上的用户数。核心思路是生成一个日期序列号,用日期减去序列号得到分组标志。
WITH t1 AS ( SELECT user_id, check_date, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY check_date) AS rn FROM ( SELECT DISTINCT user_id, check_date FROM checkin ) d ), t2 AS ( SELECT user_id, check_date, DATE_SUB(check_date, INTERVAL rn DAY) AS grp FROM t1 ) SELECT user_id, COUNT(*) AS cont_days FROM t2 GROUP BY user_id, grp HAVING COUNT(*) >= 7;
这道题的难点在于去重:同一天可能有多次打卡记录,需要先DISTINCT。同时DATE_SUB函数的具体方言在不同数据库里写法不同,MySQL用DATE_SUB,PostgreSQL用日期减去整数。笔试时如果环境不明确,可以先写出思路再用注释说明方言差异。
给定表scores(student_id, subject, score),希望每个学生一行,科目作为列。这就是典型的行转列,通常配合聚合函数和CASE WHEN。
SELECT student_id, MAX(CASE WHEN subject = '语文' THEN score END) AS chinese, MAX(CASE WHEN subject = '数学' THEN score END) AS math, MAX(CASE WHEN subject = '英语' THEN score END) AS english FROM scores GROUP BY student_id;
使用MAX或MIN是因为每个科目在分组后只有一个非NULL值。列转行则使用UNION ALL或LATERAL VIEW。不少候选人拿到这道题会用PIVOT语法,但主流笔试环境未必支持,建议掌握CASE WHEN的通用写法。
给定表sales(order_date, amount),输出每天销售额以及截至当日的累计销售额。
SELECT order_date, SUM(amount) AS daily_amount, SUM(SUM(amount)) OVER(ORDER BY order_date) AS cum_amount FROM sales GROUP BY order_date ORDER BY order_date;
这里使用窗口函数的聚合嵌套,先分组求和再开窗累计。如果数据量极大,还可以改为自连接,但自连接的成本明显更高。此题的判断点是:窗口函数SUM中ORDER BY是否包含所有排序列、是否处理好重复日期。
给定表monthly_sales(month, amount),要求计算每个月的环比增速和同比增速。环比是与上个月比,同比是与去年同月比。
SELECT month,
amount,
LAG(amount) OVER(ORDER BY month) AS prev_month_amount,
(amount - LAG(amount) OVER(ORDER BY month)) / LAG(amount) OVER(ORDER BY month) AS mom_growth
FROM monthly_sales;LAG函数在计算周期对比时异常高效。笔试中常见错误是没有处理LAG返回NULL的第一行,导致增速无用。可以用CASE WHEN把NULL原样保留或输出为0。
如果你现在连GROUP BY和HAVING的执行顺序都说不清楚,我的建议是先花一周把“SQL执行顺序”彻底搞懂,这是所有调错的根底。执行顺序是:FROM→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT。理解这个后,很多关联问题都能定位到具体层级。
接着用两周刷窗口函数专项题,每天至少2题。窗口函数是划分初中级分析师的分水岭,也是笔试出现率增长最快的考点。每周安排一天直接看别人提交的正确答案,分析人家如何组织CTE。

如果你已经能完成基础查询,建议把重心转向“一题多解”和“性能对比”。拿同一道题写三种解法:子查询、窗口函数、临时表关联,分别执行并记录耗时。这个习惯会让你在笔试现场快速判断哪种方案更适合当前数据环境。
同时,刻意练习“看错误输出的逆推能力”。用一条字段类型错误的脏数据打乱结果,观察错误输出并反推原因。真正在笔试中,脏数据几乎必然存在,逆推能力比正向写代码更重要。
我建议你用三个标准判断自己是否准备好:一、拿到中等难度笔试题能在15分钟内完成且一次跑通;二、能清楚说出自己写的SQL每一步的中间结果行数和粒度;三、能在不运行的情况下人工推演前10行输出的样子。这三条都达标再去投递,通过率会高很多。
如果做不到,回到真实数据上做练习,而不是继续刷简单题寻找信心。
笔试时长有限,我的建议是简单题控制在5分钟内,中等题15分钟内,高难压轴题最多留25分钟。如果一道题超过25分钟还没有清晰思路,先写上分步注释和自己的思考方向,然后去做其他题。面试官会看到你合理的放弃策略。

面试官批卷时需要在几分钟内理解你的逻辑。如果一个写法的性能提升只有0.3秒,但可读性下降了50%,我会选择可读性。但如果数据量是百万级以上的大数据环境,明确的先过滤再关联的写法就变得必要。
最好的平衡是用CTE把逻辑拆成三个清晰的临时块,每个块只做一件事。这样即使性能不是最优,面试官也能看出你具备结构化思维,并愿意在后续沟通中讨论优化方案。
有些题目用递归或PIVOT确实能一行解决,但这些语法在不同数据库中的兼容性风险很高。我在批卷时见过太多候选人被自己写的复杂解法卡住,最终没时间提交。稳妥解法往往更容易得分和复核。
选择复杂解法的唯一条件是你已经在本地跑过同类型且完全理解边界行为。否则就选最通用的写法:CASE WHEN搭配聚合函数,UNION ALL搭配显式列名。这几个基础语法的兼容性最好,出错率最低。
很多候选人遇到压轴题直接交白卷,这是最可惜的。压轴题在评分中的权重往往只占30%,而空白意味着零分。只要写下你对数据结构的理解、你选用的核心函数、你预计的难点位置,面试官仍然能在卷面上识别出你的思考质量。
一个实际观察是:我批改的卷子里,约有11%的压轴题空白卷,但其中5%其实前面的基础题全对。这部分人如果能把思路写出来,很可能够到下一轮面试门槛。
回到开头的那个数据:21%的完整通过率。真正的通过者做对了什么?他们不是比普通人多背了一百个函数,而是形成了稳定的读题拆解习惯,把每个业务问题都映射到“粒度、过滤时机、空值边界、输出校验”四个维度上。这种能力无法靠零散刷题获得,只能通过刻意训练形成。
下一步,我建议你做这样一件事:手边准备一个笔记本,每做完一道练习题,强制写出三个内容,这道题希望输出的最小粒度是什么?哪些行会在关联中被静默丢弃?如果给你100倍数据量,你的查询还能跑通吗?持续二十道题之后,你的笔试状态会明显改变。
如果你正在准备数据分析笔试,不妨把这篇文章里的练习题自己先做一遍,再比照我的方案思考是否有更优拆解。祝你下一次面对空白答题框时,脑中浮现的不是零散的SQL关键词,而是清晰的解题地图。
我在刷数据分析笔试时,遇到按班级给成绩排名的题目,我用ROW_NUMBER()跑出每个人的唯一序号,但参考答案却用RANK(),结果名次占位不同。我始终不明白三个函数到底分别用于什么业务,笔试里到底选哪个才稳妥。希望有真实面试和项目经验的同学指点。
这三个函数都是窗口函数,用于在分组内排名,核心差异在于对并列值的处理方式。以成绩88、90、90、91为例,ROW_NUMBER()生成的序号为1、2、3、4;RANK()生成的序号为1、2、2、4;DENSE_RANK()生成的序号为1、2、2、3。记住这个例子就能推演其他情况。
笔试中,这类题通常隐藏在“第N高的薪水”“每组前三名”等经典问题里。如果题目明确说“相同分数名次相同,且下一名次跳过”,必须用RANK();如果说“相同分数名次相同,且下一名次连续”,用DENSE_RANK();如果要求“不关注并列,只要唯一序号”,用ROW_NUMBER()。
大多数公司笔试默认考察的是RANK(),因为业务中排名更常用。更隐蔽的考点是:窗口函数中ORDER BY默认按升序排名,成绩类需求通常要按降序排名,所以要写成ORDER BY score DESC,否则会得到最低分排第一。我在实际项目里曾因漏写DESC,导致排行榜完全反了,这是最容易被忽略的细节。
另一个常见陷阱是分区列选择。比如按班级排名,PARTITION BY class_id;按学科排名,PARTITION BY subject_id。如果漏掉PARTITION,就会导致所有数据被当成一个整体,答案全错。建议做题时先圈出“每个”后面的名词,那就是分区列。
最后给一个笔试自查清单:第一步看是否有“并列”及“是否跳过”的描述;第二步确定PARTITION BY和ORDER BY字段及方向;第三步在草稿纸上用三个小数据模拟并列场景,验证结果。这套流程能帮你应对至少90%的窗口函数笔试题。
我做数据分析笔试时,遇到一道订单表和退款表的关联题,要求列出所有订单及其退款状态。我用了LEFT JOIN,在WHERE里写了退款金额>0,结果没有退款状态的订单也消失了。后来才意识到ON和WHERE的执行顺序不同,但不太清楚正确的写法。希望有实战经验的人讲清楚判断依据。
首先明确基本差异:INNER JOIN只返回两表中匹配成功的记录,未匹配的直接丢弃;LEFT JOIN会保留左表全部记录,右表没有匹配则置NULL。笔试中遇到“保留所有左表记录”字样,首选LEFT JOIN;遇到“只看两表都有的”首选INNER JOIN。ON和WHERE的差异只在外连接中体现。
原因是LEFT JOIN先按ON生成临时结果集,保留左表全部行;随后WHERE过滤临时结果集。如果把对右表字段的过滤条件放在WHERE,则会删除那些本来因NULL而保留的左表行,导致效果接近INNER JOIN。举例:订单表A有1、2、3三笔订单,退款表B只有订单2有退款。
如果写LEFT JOIN B ON A.id=B.order_id AND B.refund_amount>0,结果保留订单1、2、3;如果改为WHERE B.refund_amount>0,结果只会剩订单2。这就是笔试最常见的“右表条件误放WHERE”坑。
我的工程经验是:只要目标是“保留左表所有行”,对右表的一切过滤条件都写进ON子句;对左表本身的过滤条件,写在WHERE还是ON不影响结果,但规范上放在WHERE更清晰。这道题我当年在面试中实际抽到过,答错后让我回去等通知,印象很深刻。
还有一个可以快速判断的技巧:当SQL中同时出现LEFT JOIN和WHERE时,先把WHERE里的条件套回左表,看是否符合业务语义。很多笔试官方答案会故意把坑放在WHERE里,考察你对外连接执行顺序的深层理解。
我在SQL笔试中经常写SELECT name, COUNT(*) FROM table GROUP BY dept_id,在MySQL中能跑,但面试官说这是错的,并让我解释原因。我看过一些博客,说取决于数据库配置。
我一直没弄懂分组查询的规范写法是什么,以及WHERE和HAVING到底从哪一步开始生效。
标准SQL规定,分组查询的SELECT列表只能出现分组列和聚合函数,因为分组后每一个GROUP BY组合只返回一行,若包含非分组列,数据库无法确定该取哪一行的值。这是逻辑上的确定性要求。
MySQL在默认设置下可能允许查询,是因为ONLY_FULL_GROUP_BY未开启,笔试环境通常应启用该严格模式。执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。WHERE在分组前逐行过滤,排除的行不参与聚合;
HAVING在分组后过滤聚合结果。因此,WHERE不能包含COUNT(*)>1这样的条件,而HAVING可以。一个具体的笔试例子:查询每个部门人数超过2人的部门。需要先GROUP BY dept_id,再用HAVING COUNT(*)>2。如果试图用WHERE COUNT(*)>2,直接报错;
用WHERE员工级别=“正式”则正确,因为它是分组前的行级过滤。我见过很多考生把“员工工资平均值大于5000的部门”写成HAVING AVG(salary)>5000,这没问题。
但如果你又想在HAVING里加“部门名称以A开头”,这属于分组前条件,最好放在WHERE中,能让数据库先缩小扫描范围,性能更好。做笔试题时,用执行顺序来判断条件位置最可靠。额外提示:在Oracle、PostgreSQL、SQL Server中,SELECT非分组列会直接报错。
写笔试代码时,即使本地MySQL能过,也要遵循标准,否则在面试官面前演示时被指出来就是减分项。我的习惯是写任何GROUP BY前先问自己:SELECT里的每个字段是否都能由分组逻辑唯一确定。
我看笔试题的参考答案,常把嵌套的子查询改写成WITH子句,可我用两种写法得到相同结果,并不清楚差异。尤其相关子查询在数据量大的时候特别慢,我试过优化但没把握。希望了解在笔试和实际工作中,到底该优先用哪种方式,以及背后的执行原理。
相关子查询是指内层子查询引用了外层查询的列,它会被外层每一行执行一次。假设外层表有10万行,内层表也要被扫描10万次,性能极差。CTE即WITH语句,本质上是命名临时结果集,先计算一次,后续可重复引用,很多数据库优化器会将其物化为临时分片或临时表,减少重复计算。
笔试中,当看到“查询每个用户最新订单”这类问题,常见写法是外层表+相关子查询,但这往往是低效写法。更优解是用窗口函数ROW_NUMBER(),或者用CTE先给订单编号,再过滤出第一行。CTE能让代码可读性和性能同时提升。
我处理过一个真实场景:电商订单表约50万行,用户表10万行,用相关子查询跑用户最近一次下单时间花了近30秒。改成WITH语句先按用户编号分组,再关联用户表,运行时间降到2秒左右。这中间没有做任何物理优化,只是改写了执行方式。不过要注意,CTE并不是万能的。
如果CTE只被查询一次且数据量很大,数据库可能不会物化,反而效果和子查询类似。因此笔试题中,凡是CTE出现两次以上,一定要优先考虑;如果只出现一次,可读性收益大于性能收益,仍建议用CTE展示工程素养。建议笔试时,除非题目明确要求用子查询,否则优先使用CTE。
它体现的是构建可维护查询的思维,而不仅是最小可运行结果。很多阅卷人会观察是否拆分复杂逻辑,用WITH分段会让你比直接套多层嵌套子查询的答案更容易拿高分。


读者评论
文章把SQL笔试从语法记忆拆成读题、逻辑和工程判断三层,尤其强调结果粒度、过滤时机和边界条件,这些确实是实际工作中容易出错的地方。对备考者来说,复习方向比较清晰。
连续月份、最近一笔订单、NULL处理等例子选得比较典型,能对应常见业务场景。不过部分性能数据和考点占比来自个人统计,缺少样本来源与数据库环境说明,适合作为经验参考,不宜当成普遍结论。
关于窗口函数、自连接和去重的讲解比较实用,能帮助读者理解为什么要先拆解问题再写SQL。但“尽量用EXISTS替换IN”等优化建议还需要结合执行计划、数据量和数据库类型判断,不能一概而论。
文章提醒先明确输出粒度,这一点很有价值。很多统计结果不准确,确实不是语法问题,而是把用户、订单、行为等不同粒度混在了一起。若能补充完整练习题和标准答案,学习效果会更好。
内容覆盖面较广,从高频考点讲到面试官关注的答题过程,适合有基础的求职者查漏补缺。对零基础读者来说,窗口函数、连续月份判断和快照口径部分跳跃较大,最好配合数据样例逐步练习。