1. 面试到底考什么:数据分析SQL面试的底层逻辑
做数据分析或者商业分析岗位面试,SQL基本是绕不开的一道硬门槛。我面过不少候选人,也帮朋友做过模拟面试,一个很直观的感受是:很多人不是不会写SQL,而是不知道面试官到底在考察什么,导致准备方向偏了。SQL题目看着是考语法,实际上考的是三样东西:逻辑拆解能力、业务理解深度、代码习惯。
先说逻辑拆解。数据分析场景里的SQL题,从来不会直接告诉你“用LEFT JOIN连接两张表”,而是给你一个业务问题,比如“统计每个品类下销量排名前3的商品”。这时候你需要自己把问题拆解成:先算每个商品在品类内的销量,再按品类分组做排名,最后过滤出前3名。这个拆解过程,就是面试官最想看到的。
再说业务理解。同样是“求留存率”,不同公司的口径可能完全不同。是按注册日期算还是按首次活跃日期算?留存是次日留存还是7日留存?活跃的定义是“有过任意行为”还是“有过核心行为”?这些问题在题目描述里往往不会写全,面试官就是想看你会不会主动追问澄清。能在写SQL之前把业务口径问清楚的人,在我这里一定加分。
最后是代码习惯。一个完整的SQL面试题,通常要求你现场写代码,面试官一边看一边问。这时候代码的可读性、健壮性就很重要了。是习惯用SELECT *还是显式列出字段?空值处理有没有考虑到?排序的升降序有没有弄反?这些细节比“能不能写出正确答案”更能反映一个人平时的工作习惯。
基于我接触到的实际面试题,SQL考察的深度大致可以分成三个层次:基础语法层(JOIN、聚合、筛选、去重)、逻辑进阶层(窗口函数、子查询、CTE)、业务应用层(留存、复购、TopN、连续登录等经典场景)。下面我按这个分层,把高频考点一个个拆开讲,每一道题都会结合真实业务场景来说明,而不是干巴巴地背语法。
2. 基础语法题:别在JOIN和去重上翻车
别小看基础题,很多工作了三五年的人,照样在JOIN和去重的细节上栽跟头。这一部分我建议你当成“送分题”来对待,但前提是每个细节你都真正吃透了。
2.1 五种JOIN的本质与最容易踩的坑
表连接是SQL面试必考,几乎每轮面试都会出现。核心就五种:INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN、CROSS JOIN。面试最常见的考点是LEFT JOIN和INNER JOIN的区别,以及ON和WHERE放在LEFT JOIN里为什么会结果不同。
我直接说一个经典陷阱。面试官给你两张表,一张用户表(user_id, user_name),一张订单表(order_id, user_id, amount),让你统计每个用户的订单总金额,没有订单的用户也要列出来。大部分人能写出:
SELECT u.user_id, u.user_name, SUM(o.amount) AS total_amount FROM user u LEFT JOIN orders o ON u.user_id = o.user_id GROUP BY u.user_id, u.user_name;这个答案本身没问题。但如果面试官追加一句“请注意过滤掉无效订单(status = ‘invalid’)”,很多人会顺手在WHERE里写:
SELECT u.user_id, u.user_name, SUM(o.amount) AS total_amount FROM user u LEFT JOIN orders o ON u.user_id = o.user_id WHERE o.status != 'invalid' GROUP BY u.user_id, u.user_name;结果就出问题了。LEFT JOIN之后,没有订单的用户,右表字段都是NULL,NULL != 'invalid'这个条件在SQL里不成立,这些用户整行被过滤掉了。你违背了“没有订单的用户也要列出来”这个前提。
正确做法是把过滤条件放到ON子句里:
SELECT u.user_id, u.user_name, SUM(o.amount) AS total_amount FROM user u LEFT JOIN orders o ON u.user_id = o.user_id AND o.status != 'invalid' GROUP BY u.user_id, u.user_name;这个细节我几乎在每次模拟面试都会讲,因为它太典型了。ON子句负责“连接时决定哪些行匹配”,WHERE负责“连接完成后决定哪些行保留”。LEFT JOIN的场景里,右表过滤条件写错位置,LEFT就变成了INNER。
提示:见到LEFT JOIN题目,先问自己一个问题——如果右表没有匹配行,这个用户还要不要出现在结果里?要,那右表的所有过滤条件都写在ON里面。
2.2 聚合与去重:GROUP BY、DISTINCT、COUNT的细节
去重是另一个高频考点。大家可以看看热词里“sql语句去重查询”“清洗---sql语句去重”出现频率多高,就知道工作中这是逃不掉的。面试一般考察两种去重:简单去重和分组去重。
简单去重用DISTINCT,比如统计有多少个不同的用户下了单:
SELECT COUNT(DISTINCT user_id) FROM orders;有一点要注意,COUNT(DISTINCT user_id)不会计入NULL值。如果user_id允许为空,那么COUNT(DISTINCT user_id)和COUNT(DISTINCT user_id) + COUNT(DISTINCT CASE WHEN user_id IS NULL THEN 1 END)结果可能不一样。实务中用户ID不会为NULL,但面试时有人问起来,你能答出这个细节会很加分。
分组去重通常用ROW_NUMBER(),这个后面细说。这里先提醒一个更隐蔽的坑:COUNT(*)和COUNT(字段)的区别。COUNT()统计的是行数,COUNT(字段)统计的是该字段非NULL值的数量。比如你要统计每个用户的订单数,用COUNT(order_id)没问题,但如果某张表的order_id允许为NULL,那结果就会少于实际行数。习惯上用COUNT()或COUNT(1)更稳妥。
再补充一个工作中能救命的小技巧。数据清洗场景里“去重”往往不是简单DISTINCT,而是“同一用户保留最新一条记录”。这个需求用DISTINCT做不了,需要结合窗口函数:
WITH ranked AS ( SELECT user_id, action_time, behavior, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY action_time DESC) AS rn FROM user_behavior ) SELECT user_id, action_time, behavior FROM ranked WHERE rn = 1;这道题在我面试别人的时候经常出现,因为它是“去重”场景下最贴近实战的一个问题,而且天然衔接窗口函数考点。
2.3 空值处理的正确姿势
热词里有“sql去除空值”,说明这也是很多人工作时的痛点。面试不太直接考“怎么去除空值”,但会在题目里埋空值陷阱。两种常见考法:一是用IS NULL做判断,二是用COALESCE或IFNULL做默认值替换。
先说IS NULL。SQL里判断空值不能写“= NULL”,任何与NULL做等值比较的结果都是未知(UNKNOWN),在WHERE里会被过滤掉。这个基础错误通常问的是概念,你答“NULL只能用IS NULL或者IS NOT NULL判断”就过关了。
再说COALESCE。比如统计每个用户的订单总额,没有订单的用户要给0而不是NULL:
SELECT u.user_id, COALESCE(SUM(o.amount), 0) AS total_amount FROM user u LEFT JOIN orders o ON u.user_id = o.user_id GROUP BY u.user_id;COALESCE可以传多个参数,返回第一个非NULL的值,MySQL里也常用IFNULL(expr1, expr2)。相比之下,COALESCE是标准SQL,跨数据库通用性更好,我面试时更推荐写COALESCE。
还有一个细节要留意:COUNT和SUM遇到NULL的处理方式不同。COUNT(字段)忽略NULL,SUM(字段)也忽略NULL——如果该字段全为NULL,SUM返回NULL而不是0。很多人死在这上面,所以业务上“求总和”时,最好用COALESCE(SUM(amount), 0)包一层,既能防空值也能统一返回值语义。
3. 窗口函数专题:数据分析师的分水岭
基础语法大家都会,真正拉开差距的是窗口函数。我面试过的候选人里,能熟练使用窗口函数的大概只有一半左右,而能把窗口函数在业务场景中用对的人更少。如果说基础题是“看你会不会”,那窗口函数就是“看你的思维深度”。
3.1 窗口函数和GROUP BY到底差在哪
窗口函数的核心特征是:不改变行数,每一行都能看到聚合结果。GROUP BY会把多行压成一行,而窗口函数是“在保留每一行原始信息的同时,额外计算出一个聚合值”。
举个例子。表里有每个用户每天的消费记录,你想知道“每个用户的总消费金额”以及“每一笔消费占总消费金额的比例”。用GROUP BY只能得到每个用户的总金额,看不到单笔明细;用窗口函数则可以:
SELECT user_id, order_id, amount, SUM(amount) OVER (PARTITION BY user_id) AS user_total_amount, amount / SUM(amount) OVER (PARTITION BY user_id) AS amount_ratio FROM orders;为了理解这个区别,可以打个比方:GROUP BY像一个漏斗,把很多行倒进去,出来的是一小撮精华;窗口函数像一面放大镜,不改变原图,只是在每行旁边贴了一个计算标签。数据分析里经常遇到“既要明细又要汇总”的需求,比如算占比、算累计、算同组排名,这就是窗口函数的主场。
窗口函数的基本语法是:
函数名() OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 [ROWS BETWEEN 边界规则] )注意两点:PARTITION BY的“分组维度”和ORDER BY的“排序维度”决定了窗口的形状;ROWS BETWEEN则用来控制计算范围。绝大多数面试题只要用到PARTITION BY和ORDER BY就够,但你能讲清楚窗口边界规则,面试官对你的印象会明显不一样。
3.2 排序三兄弟:ROW_NUMBER、RANK、DENSE_RANK
这是窗口函数里最经典的一组题。同样是给用户按消费金额排序,这三者的区别是什么?用一个简单例子说明:
| 用户 | 消费金额 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|---|
| A | 100 | 1 | 1 | 1 |
| B | 100 | 2 | 1 | 1 |
| C | 80 | 3 | 3 | 2 |
| D | 60 | 4 | 4 | 3 |
ROW_NUMBER依次编号,不管有没有并列;RANK遇到并列会跳过编号(100并列第一,下一个人排第三);DENSE_RANK遇到并列不跳过(100并列第一,下一个人排第二)。
面试题里最常出现的“取TOP N”需求,默认用哪个?我的经验是:看需求强调不强调并列。如果题目说“取每个品类销量前3的商品”,一般意味着最多取3条;如果题目说“找出销量最高的所有商品”,那就需要包含所有并列的数据。前者用ROW_NUMBER或RANK都行,但要注意ROW_NUMBER在并列时会随机选一个,RANK会把并列全部选出来却可能超过3条;后者更保险的是用RANK或DENSE_RANK再加过滤条件。
实际工作里,我用得最多的还是ROW_NUMBER,因为很多业务场景只需要一条“代表记录”,比如“每个用户最近一次下单记录”。但面试时最好把三者区别以及适用场景都说清楚,这能体现你考虑问题的周全。
TopN问题的标准答法,以“每个部门薪资最高的员工”为例:
WITH ranked_employee AS ( SELECT employee_id, employee_name, department_id, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn FROM employee ) SELECT employee_id, employee_name, department_id, salary FROM ranked_employee WHERE rn = 1;这段代码面试中出现频率极高,建议你练到闭眼能写。注意我把排序结果单独放在CTE里再过滤,而不是直接在外面套WHERE rn = 1——因为WHERE的执行顺序在SELECT之前,窗口函数的结果在WHERE阶段还不可见,必须用子查询或CTE包一层。
3.3 聚合窗口与移动计算:累计值、LAG与LEAD
除了排序,窗口函数还经常用来算累计值,比如“求每个用户截至当前日的累计消费”。这里的核心是ORDER BY与SUM窗口函数的组合:只用PARTITION BY是组内汇总,加上ORDER BY就变成了“随着排序逐行滚动累加”。
SELECT user_id, order_date, amount, SUM(amount) OVER ( PARTITION BY user_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_amount FROM orders;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW的意思是,从分组的第一行到当前行参与计算,这也是累计值(running total)的标准写法。实际业务里,计算月度累计销售额、用户累计消费、库存滚动结余,用的都是这套逻辑。
LAG和LEAD是另外两个高频函数,用来取前一行或后一行的值。最常见的使用场景是算同比环比。比如算每个月的环比增长率:
WITH monthly_sales AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS total_amount FROM orders GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, total_amount, LAG(total_amount, 1) OVER (ORDER BY month) AS prev_month_amount, (total_amount - LAG(total_amount, 1) OVER (ORDER BY month)) / LAG(total_amount, 1) OVER (ORDER BY month) AS mom_rate FROM monthly_sales ORDER BY month;注意LAG的第二个参数表示向前几行,默认是1;第三个参数是取不到前一行时的默认值。如果第一个月没有前值,结果会是NULL,业务上可以用COALESCE处理。我面试的时候喜欢问LAG,因为它能自然衔接“窗口函数 + 业务指标”的能力。
4. 业务场景题:从SQL到商业分析思维
到了这个环节,SQL就不再是单纯的语法题了,而是真正考核数据分析师的业务抽象能力。面试官会给你一个业务问题,看你能不能把它翻译成SQL。这里挑四个出现频率最高的场景拆解。
4.1 留存率计算:口径决定一切
留存率是数据分析面试的保留题目,几乎每家互联网公司的面试题里都有它的变体。题目描述一般是:有一张用户活跃表,包含user_id和active_date,求某日新增用户的次日留存率、7日留存率等。
很多人一上来就写SQL,结果发现题目根本没给“新增用户”的定义。所以在动手前,一定要先和面试官确认口径:新增用户的定义是注册时间在某天,还是首次活跃时间在某天?我面试时会特意问一句,看候选人会不会主动澄清。这个动作本身就占了得分点。
假设口径是“某天首次活跃的用户视为该日新增用户”,计算次日留存率的SQL:
WITH first_active AS ( SELECT user_id, MIN(active_date) AS first_date FROM user_active_log GROUP BY user_id ), active_next_day AS ( SELECT DISTINCT a.user_id, a.active_date FROM user_active_log a ) SELECT f.first_date, COUNT(DISTINCT f.user_id) AS new_user_cnt, COUNT(DISTINCT CASE WHEN n.active_date = DATE_ADD(f.first_date, INTERVAL 1 DAY) THEN f.user_id END) AS retained_user_cnt, COUNT(DISTINCT CASE WHEN n.active_date = DATE_ADD(f.first_date, INTERVAL 1 DAY) THEN f.user_id END) / COUNT(DISTINCT f.user_id) AS retention_rate FROM first_active f LEFT JOIN active_next_day n ON f.user_id = n.user_id GROUP BY f.first_date;核心逻辑是:先找到每个用户的首次活跃日期,作为新增日期;再用LEFT JOIN关联该用户次日是否活跃;最后用COUNT DISTINCT CASE WHEN的写法来算留存人数和留存率。
这道题的难点在于:为什么用LEFT JOIN而不是INNER JOIN。用INNER JOIN会把次日没活跃的新增用户全部排除掉,留存率分母直接少一块。另外,为什么子查询里要加DISTINCT?因为用户一天可能有多条活跃记录,如果不先去重,同一个用户会JOIN出多行,COUNT DISTINCT虽然能兜底,但会带来额外的计算负担。
注意:写留存SQL的关键,并不是记住某个固定模板,而是理解“新增日期”和“活跃日期”这两组时间维度的对应关系。面试中如果遇到留存、活跃、转化这类指标,先画清楚时间线再写代码。
4.2 复购率:行为口径的拆解
复购率是电商、零售行业面试的常见题。题目可能是:有一张订单表,包含user_id、order_id、order_date,求2024年1月的复购率。
复购率的口径也有两种常见理解:一是“统计周期内购买2次及以上的用户数 / 总购买用户数”;二是“老用户中再次购买的比例”。两种口径,SQL差异不大,关键是你要说清楚你采用的是哪一种。
按第一种口径写:
WITH user_buy AS ( SELECT user_id, COUNT(DISTINCT order_id) AS buy_cnt FROM orders WHERE order_date >= '2024-01-01' AND order_date < '2024-02-01' GROUP BY user_id ) SELECT COUNT(CASE WHEN buy_cnt >= 2 THEN user_id END) AS repurchase_user_cnt, COUNT(user_id) AS total_user_cnt, COUNT(CASE WHEN buy_cnt >= 2 THEN user_id END) / COUNT(user_id) AS repurchase_rate FROM user_buy;是不是感觉和留存率的写法很像?对,数据分析面试里很多题目本质上都是“分组计数 + 条件计数 + 比例计算”的排列组合。把“满足条件的用户数”和“总用户数”相互比较,就出现了各种率指标。
这个SQL里我用了COUNT(CASE WHEN ... THEN user_id END),它等价于SUM(CASE WHEN ... THEN 1 ELSE 0 END)。两种写法都行,看个人习惯。如果user_id没有NULL值,两种写法结果一样;如果有NULL值,SUM(CASE WHEN ... THEN 1 ELSE 0 END)更稳妥。
4.3 连续登录天数:经典中的经典
连续登录天数这道题,几乎每轮面试必考。题目通常这样:给定用户登录表(user_id, login_date),求每个用户的历史最大连续登录天数。
解题思路很巧妙,核心是一句话:用登录日期减去按登录日期排序得到的序号,如果连续,这个差值会是同一个值。
举个例子。用户A在1月1日、1月2日、1月5日登录。按日期排序后,它们的ROW_NUMBER分别是1、2、3。用登录日期减去编号:1月1日减1等于12月31日,1月2日减2等于12月31日,1月5日减3等于1月2日。前两个差值相同,说明这两天连续;第三个差值不同,说明断档了。
WITH login_ranked AS ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) DAY) AS diff_date FROM ( SELECT DISTINCT user_id, login_date FROM user_login_log ) t ) SELECT user_id, MAX(consecutive_days) AS max_consecutive_days FROM ( SELECT user_id, COUNT(*) AS consecutive_days FROM login_ranked GROUP BY user_id, diff_date ) a GROUP BY user_id;注意两点。第一,内层子查询先做了一轮DISTINCT去重,因为同一天可能有多次登录,不去重的话序号会偏大,导致连续日期计算错误。第二,既然同一用户同一天只有一条记录,那么按dif_date分组后,组内行数就等于这个连续区间的天数。
这道题考察的“用日期减序号识别连续区间”的思路,在用户行为分析里应用非常广。比如识别连续消费用户、连续签到用户、连续活跃用户,本质都是这个套路。我面试时把这个思路理解了,比背答案值钱得多。
4.4 同比环比:LAG的实战价值
同比环比在商业分析面试里也很常见,尤其面业务方向的数据分析师。题目可能给你一张日/月销售额表,让你计算每个月的环比增长率和同比增长率。
环比就是和上个月比,同比增长可以和去年同月比。用LAG可以轻松实现:
WITH monthly AS ( SELECT DATE_FORMAT(month, '%Y-%m') AS month, SUM(sales_amount) AS sales_amount FROM sales_table GROUP BY DATE_FORMAT(month, '%Y-%m') ) SELECT month, sales_amount, LAG(sales_amount, 1) OVER (ORDER BY month) AS prev_month_sales, LAG(sales_amount, 12) OVER (ORDER BY month) AS prev_year_sales, ROUND((sales_amount - LAG(sales_amount, 1) OVER (ORDER BY month)) / LAG(sales_amount, 1) OVER (ORDER BY month) * 100, 2) AS mom_growth_rate, ROUND((sales_amount - LAG(sales_amount, 12) OVER (ORDER BY month)) / LAG(sales_amount, 12) OVER (ORDER BY month) * 100, 2) AS yoy_growth_rate FROM monthly ORDER BY month;LAG(sales_amount, 12)表示往前推12个月,适用于月度数据的同比。如果数据是日粒度,同比就是LAG(..., 365),但要注意闰年问题,实际工作中常按“去年同期同一天”特殊处理。能意识到这一点,在面试中会很加分。
5. 性能和规范:让面试官刮目相看的细节
很多候选人以为SQL题目只要结果对就可以了。其实不然。我面过不少人,答案写对了,但满屏SELECT *,临时表建了好几个,过滤条件乱放。虽然不算错,但给人的印象分大打折扣。面试官心里会嘀咕:这个人如果上线写接口,会不会也这么草率?
5.1 慢SQL优化:基础但不冷门
热词里有“慢sql优化”,这确实是数据分析师日常工作中绕不开的问题。你的SQL跑半小时,业务还在等数据,这体验谁受得了。面试中关于性能优化常被问到的点有:
- 避免SELECT *,只查需要的字段。数据传输少,解析快,也方便后续维护。
- WHERE子句里的函数包裹会导致索引失效。比如WHERE DATE(create_time) = '2024-01-01',可以改成WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。
- 隐式类型转换同样会让索引失效。比如数字字段被写成字符串来比较,或者反过来。
- 分组前先过滤,而不是分组后再过滤。WHERE过滤的行数越少,后续GROUP BY和JOIN的计算量就越小。
- 大表JOIN尽量用小表驱动大表。很多数据库优化器会自动处理,但手工SQL里注意一下总没有坏处。
- 子查询能改JOIN就改JOIN。老版本MySQL里,子查询性能弱于JOIN;现在优化器改进了不少,但可读性上JOIN仍然更清晰。
这些点你不需要背得多深入,但能在写完SQL之后主动说一句“我注意到这个查询里create_time上用了函数,可能索引会失效,建议改成范围查询”,面试官就会觉得你是真做过优化,而不是只会背题。
5.2 写出让面试官舒服的SQL
代码风格在面试里是隐性加分项。我总结几点经验:
显式写明字段名,不写SELECT *。这一点前面提过,这里再强调一次。面试官会从你的每个字段里看出你有没有需求分析的意识。
大小写和缩进保持统一。关键字大写、字段小写是我的习惯。缩进用到缩进,JOIN条件对齐写清楚。代码不是写给自己看的,是写给同事看的。
复杂的SQL优先用CTE拆解。比起层层嵌套的子查询,CTE让每一层逻辑都有名字、都能独立理解。比如前面留存率那个例子,我先写first_active、再写active_next_day,每一步都能看懂。面试时向面试官解释起来也顺畅很多。
关键指标计算加上注释。数据分析SQL里经常出现复杂的口径计算,比如“留存率 = 次日活跃的新增用户数 / 当日新增用户数”。在SQL开头写清楚这个口径,后面再看代码的人就不会一脸懵。
5.3 排查SQL错误的方法论
面试现场写SQL,难免写出跑不通的代码。我不怕候选人报错,就怕候选人对错误毫无头绪。这里分享一个排查思路:
先看报错的类型。语法错误多半是括号不匹配、逗号多了少了;字段名报错,可能是表别名没对上;分组聚合报错,一般是SELECT的字段没有出现在GROUP BY里。
再看结果与预期不符的情况。如果拿到结果但数字不对,我建议按以下顺序排查:先确认JOIN类型对不对,是不是应该用LEFT JOIN却用了INNER JOIN;再看过滤条件写在了ON还是WHERE;然后检查空值数据,尤其是NULL是否被意外过滤或计入;最后检查去重逻辑,JOIN是否导致数据膨胀。
有一个我在工作中反复用的小技巧:先跑一层子查询,验证中间结果,再跑完整SQL。面试时如果你觉得结果不对,也别慌,把中间的CTE单独拿来看一眼,往往问题就暴露了。这个习惯在面试现场很能体现你的调试能力。
6. 面试现场实录与避坑指南
最后这部分,我整理了一些面试现场的真实观察和技巧。这些都是常规准备帖里看不到的,但往往是决定结果的关键细节。
6.1 拿到题之后,先别急着写
很多人拿到题就开始敲键盘,结果写了半天发现理解错了方向,浪费时间也影响心态。我的建议是,花30秒到1分钟确认三件事:
- 时间范围:题目的统计周期是哪个时间段?是日、周、月还是累计?
- 用户口径:这里的“用户”是注册用户、活跃用户,还是付费用户?
- 结果形式:输出是按天分组,还是按用户分组?指标是数量、金额还是比例?
面试官设计题目时,不可能把所有细节都写得清清楚楚。主动问清口径,不仅不扣分,还会让面试官觉得你有扎实的业务意识。我面试时,候选人如果能把口径问到点子上,我心里基本已经给这个人的业务理解打了高分。
6.2 七种SQL面试高频错误
| 错误类型 | 典型场景 | 正确做法 |
|---|---|---|
| JOIN后过滤条件写错位置 | LEFT JOIN时把右表条件写WHERE | 放到ON里 |
| 只求结果不考虑重复 | JOIN导致多对多数据膨胀 | 先看关联字段是否唯一 |
| COUNT(*)与COUNT(字段)混用 | 字段为NULL导致计数偏少 | 明确统计口径 |
| NULL处理不当 | 用“= NULL”判断空值 | 用IS NULL / IS NOT NULL |
| 窗口函数过滤直接放WHERE | WHERE rn = 1 报错 | 必须包一层子查询/CTE |
| 日期函数格式不统一 | DATE_ADD、DATE_SUB混用 | 统一日期处理函数 |
| GROUP BY忘记包含非聚合字段 | SELECT的普通字段不在GROUP BY里 | 要么加进GROUP BY,要么用ANY_VALUE/MAX等聚合 |
6.3 面试时如何展示沟通能力
SQL面试不只是写代码,同时也是沟通测试。我建议你在写SQL的过程中,自言自语式地向面试官同步你的思路。比如:“我先建一个CTE去重,拿到每个用户的首次登录日期,然后再关联后续活跃记录来计算留存。”这种表达有几个好处:面试官能跟上你的思路,即使最后结果有小问题,他也知道你的方向是对的;而且遇到卡壳时,面试官更容易给你提示。
写完之后,不要只说“写完了”,可以花20秒顺一下你的逻辑:“这里我用ROW_NUMBER是因为需要每组取最新一条,并列情况没有影响,因为只要一单最新记录;空值我用COALESCE做了兜底。”这种小结既展示了代码能力,也展示了沟通总结能力。
7. 一份按优先级排列的备战建议
如果你正在准备数据分析或商业分析方向的面试,我按重要性从高到低列一个清单,你可以照着查缺补漏:
第一优先,把窗口函数练熟。排序三兄弟的区别、SUM窗口的累计用法、LAG/LEAD的环比同比,这三件事几乎覆盖了八成窗口函数面试题。
第二优先,把经典业务场景题做一遍。留存率、复购率、连续登录天数、TopN,每道题至少手写两遍,做到不看参考答案也能写出来,并且能说出每一步在干什么。
第三优先,把JOIN和去重的细节吃透。ON和WHERE的位置区别、LEFT JOIN右表条件过滤、COUNT DISTINCT的空值陷阱,这几个点基础但容易翻车。
第四优先,练一两道完整的SQL笔试题,控制时间。面试现场题量通常不小,既要准确又要速度。平时可以给自己掐表20分钟,模拟真实面试环境。
第五优先,面试前花10分钟回顾一下自己的实战SQL经验。面试官大概率会问:“你最近写过最复杂的一条SQL是什么?”这个问题没有标准答案,但你要能用自己的实际工作案例讲清楚任务背景、SQL逻辑和最终效果。
我个人在实际带人过程中的体会是,SQL面试准备最怕的是“眼高手低”。看懂了不等于会写了,会写了不等于能在限时和压力下写对。数据分析和商业分析岗位的发展路径越往上走,对逻辑思维和业务理解的要求越高于纯语法技巧。所以建议你刷题之外,多想想每道题背后的业务意义——面试官想要的从来不是一个“打字员”,而是一个能通过SQL洞察业务问题的分析师。