做数据分析这几年我去面试过别人也被别人面试过SQL面试题永远是绕不开的一关。十个候选人里至少六个人会在“连续登录天数”“分组TopN”“留存率”这类题目上翻车而且翻车的原因不是没学过而是没搞清楚面试官到底想看什么。这篇文章我就把数据分析岗必会的SQL面试题整理一遍连同出题逻辑、经典解法、还有自己实际踩过的坑一次性讲清楚。1. 面试官到底想考什么SQL题背后的能力拆解1.1 三类高频SQL题从查询到分析先给SQL面试题分个类你就能发现它们不是乱出的。第一类是基础查询与聚合。主要考察WHERE过滤、GROUP BY分组、HAVING筛选、JOIN关联偶尔加个CASE WHEN做条件判断。这类题占了大概三成本质上是在确认你“能不能从表里捞出正确的数据”。很多人觉得简单但其实年龄、时间范围、状态过滤这些条件稍一多写错的情况非常普遍。第二类是窗口函数与复杂业务分析。比如“每个部门薪资最高的前三名”“每个用户最近一笔订单”“连续登录三天以上的用户”。这类题直接考察你对ROW_NUMBER()、RANK()、LAG()这些分析函数的掌握程度占比最高也最容易拉开差距。第三类是数据质量与优化思路。比如“表里有很多重复数据怎么洗掉”“一张千万级的大表关联很慢你会怎么处理”。这种题目往往不会要求你完整写代码而是考察你有没有工程意识对数据体积、索引、执行计划有没有概念。三类题目对应三种能力基础查询看你的SQL熟悉度窗口函数看你的业务拆解能力优化题看你的真实项目经验。面试官嘴上说的是“写个SQL”心里想的其实是“你能不能直接上手干活”。1.2 为什么面试官偏爱这些题型数据分析岗位的日常至少有百分之六十的时间在写SQL做取数、报表和专项分析。就算你用Tableau、Power BI或者Python做可视化底层数据还是得靠SQL拉出来。面试官需要快速判断一个人能不能处理真实业务数据而SQL题正是验证“业务技术结合度”最直接的途径。比如留存率这个经典指标业务口径不同SQL写法就完全不一样。你用“次日留存当天新增用户中第二天还有活跃的人数÷当天新增总人数”这个定义和用“第二天有任意行为的人数”去算结果可能差好几个百分点。面试官出这类题可能只是拿着一个模糊需求问你“算一下留存”实际上想听的是你怎么定义、怎么拆解、怎么和业务口径对齐。再比如TopN问题如果用ORDER BY ... LIMIT只能取出全表最大的几个但“每个部门的前三名”就必须用窗口函数。这里考察的不仅是语法更是你是否懂得“分区排序”这个分析思维。面试官要把这些能力都测出来光问概念是没用的必须给一张表、给一个业务场景让你亲手写。1.3 一道“简单题”暴露出的问题我见过很多候选人做题时一上来就埋头写代码根本不问需求。拿“统计2024年每个月的下单用户数”这道题举例看似简单里面至少埋着三个坑。第一个坑是“下单用户数”要不要去重。一个用户一天下三单算一个人还是三个人正确答案是按用户去重但有的候选人直接写了COUNT(order_id)把订单数当成了用户数。第二个坑是订单状态要不要过滤。下单了但未支付、已取消的算不算业务上通常只看有效订单但需求里没说你就得主动确认。第三个坑是日期时间字段怎么处理。如果下单时间是2024-01-31 23:59:58这种时间戳用MONTH(order_time)来分组没问题但如果你用BETWEEN 2024-01-01 AND 2024-01-31就会漏掉当天最后一小时的数据。这道题我至少面过十次能一次写对的人不到一半。问题不在SQL语法而在于候选人心里只有一个表结构没有真实的业务场景。面试官通过这道题能快速判断你是背过题的“伪熟练”还是真正理解数据口径的“真熟练”。2. 必考题型拆解窗口函数与分组TopN2.1 窗口函数三兄弟Row_Number、Rank、Dense_Rank的区别窗口函数是整个SQL面试题的核心而其中最基础也最容易搞混的就是排序三兄弟。我建议你先把它们的区别刻在脑子里函数排名特点典型场景ROW_NUMBER()按顺序生成唯一序号1, 2, 3, 4取每组前N条、去重保留最新记录RANK()并列相同但会跳号1, 1, 3, 4并列排名且允许后续名次跳过DENSE_RANK()并列相同不跳号1, 1, 2, 3富豪榜、榜单排名名次紧凑看一段具体SQLSELECT user_id, sale_amount, ROW_NUMBER() OVER (ORDER BY sale_amount DESC) AS rn, RANK() OVER (ORDER BY sale_amount DESC) AS rk, DENSE_RANK() OVER (ORDER BY sale_amount DESC) AS dr FROM sales;如果user_id有两位用户消费金额相同且都是最高那么rn会分别生成1和2rk会都排1但下一位是3dr会都排1但下一位是2。这个细节面试特别喜欢拿来追问因为很多业务并列需求到底要跳号还是不跳号必须由数据分析师自己确认清楚。实际项目里如果只是“取前三名”通常用DENSE_RANK()或者ROW_NUMBER()都行但如果你要“前三名且名次紧凑”就得用DENSE_RANK()。如果业务要求“每个部门只需要唯一的前三顺序不关心并列”那就用ROW_NUMBER()。选错函数的后果是榜单人数可能超过预期或者数据对不上这个坑我踩过不止一次。2.2 分组TopN一个窗口函数解决“每组前几”“每个部门的薪资Top3”“每个品类的销量Top3”“每个城市注册用户最多的前5个渠道”这类分组TopN基本就是数据分析面试的必考题。标准解法长这样SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales DESC) AS rn FROM product_sales ) t WHERE rn 3;在这里PARTITION BY把数据按品类切开ORDER BY在每个分区内按销量排序ROW_NUMBER()给每个分区生成从1开始的序号最后外层WHERE rn 3把前三名留下来。这道题有两个高频易错点。第一WHERE子句中不能直接使用窗口函数所以必须先包一层子查询再在外层过滤序号。第二如果忘了写PARTITION BY窗口函数会忽略分组在全局排序返回的结果就会变成“全表销量最高的三条”而不是“每个品类的前三条”这两者的业务含义天差地别。顺便提醒一句如果你面试时遇到的是MySQL 5.7这种老版本环境窗口函数用不了那就只能用GROUP_CONCAT或者用户变量去模拟非常麻烦。所以如果条件允许建议面试前了解清楚目标公司用的数据库版本至少在简历上写清楚自己熟悉MySQL 8.0或更高版本。2.3 窗口函数易错点分区、排序和去重窗口函数除了TopN还会在“去重”场景里高频出现。面试官经常给出一个包含重复记录的表问你怎么把同一个用户的重复数据清洗掉、保留最新一条。推荐写法SELECT user_id, order_id, create_time FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM user_orders ) t WHERE rn 1;这个逻辑就是把同一个user_id按创建时间倒序排好然后只取每组的第一条。很多候选人一说到去重就只会写SELECT DISTINCT但DISTINCT只能过滤完全一样的整行无法做到“按某个字段去重并保留最新一条”这是两者的本质区别。还有一个细节我在实际业务里吃过亏PARTITION BY的范围别写死。假如一张表里有“用户ID”和“订单类型”两个字段你想保留的是每个用户每种订单类型的最新一条分区就必须同时包含user_id, order_type这两个字段。只按用户分区会把不同订单类型里的数据误删掉这在业务上会造成严重错误。3. 进阶必考题连续问题、留存率与行列转换3.1 连续N天登录的三种经典解法连续登录问题在数据分析面试里的地位相当于“动态规划”在算法面试里的地位我认为怎么强调都不过分。题目通常是“统计连续登录3天以上的用户”“找出连续签到7天的用户”。最推荐掌握的是用ROW_NUMBER()做差值的解法。核心思路先把每个用户的登录日期排序然后用登录日期减去序号如果日期是连续的减出来的“分组日期”一定相同。以MySQL为例SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS continuous_days FROM ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp_date FROM user_login ) t GROUP BY user_id, grp_date HAVING COUNT(*) 3;解释一下为什么能这么算假设用户A在1月1日、1月2日、1月3日登录ROW_NUMBER()生成的序号是1、2、3那么1月1日减去1天得到12月31日1月2日减去2天也是12月31日1月3日减去3天还是12月31日。三天的差值相同说明它们是连续的三天。如果中间断了一天比如1月1日、1月3日那么1月1日减1天得到12月31日1月3日减2天得到1月1日两个组就分开了。第二种解法是用LAG()函数判断前一天是否登录。比如你只需要“找出连续两天登录的用户”不需要计算最长连续天数直接用SELECT DISTINCT user_id FROM ( SELECT user_id, login_date, LAG(login_date, 1) OVER (PARTITION BY user_id ORDER BY login_date) AS prev_date FROM user_login ) t WHERE DATEDIFF(login_date, prev_date) 1;这里LAG(login_date, 1)是取当前行往前数一行的日期如果日期差正好是1天说明当前用户昨天也登录了。这种方式在生产环境里非常常用尤其是在做“最近连续活跃判断”的时候。第三种解法是自关联比较效率偏低但思路直观适合数据量小时写一写。我个人建议面试时优先讲差值法思路清晰、代码短而且能顺手展示窗口函数的熟练度。3.2 留存率的完整计算逻辑留存率是数据分析业务指标里的重中之重面试题出现的概率和连续登录不相上下。但真正能把留存算明白的候选人不多因为它的难点不在SQL语法而在“新增用户”和“活跃用户”这两个口径定义。我的复现思路是这样的第一步先找到每个用户第一次登录的日期这部分就是“每日新增用户”第二步准备一张每日活跃用户明细去重防止一个用户一天被计多次第三步把新增用户和活跃用户按用户ID关联统计第N天仍然活跃的人数再除以当日新增总数得到留存率。WITH new_users AS ( SELECT user_id, MIN(login_date) AS first_date FROM user_login GROUP BY user_id ), active_daily AS ( SELECT DISTINCT user_id, login_date FROM user_login ) SELECT n.first_date, COUNT(DISTINCT n.user_id) AS new_cnt, COUNT(DISTINCT CASE WHEN a.login_date DATE_ADD(n.first_date, INTERVAL 1 DAY) THEN a.user_id END) AS retain_1day, COUNT(DISTINCT CASE WHEN a.login_date DATE_ADD(n.first_date, INTERVAL 3 DAY) THEN a.user_id END) AS retain_3day, COUNT(DISTINCT CASE WHEN a.login_date DATE_ADD(n.first_date, INTERVAL 7 DAY) THEN a.user_id END) AS retain_7day FROM new_users n LEFT JOIN active_daily a ON n.user_id a.user_id GROUP BY n.first_date;这里有几个坑我提醒所有面试者注意。第一累计留存率也可以做成动态的但你必须明确“第N日留存”等于“新增后第N天登录”不是“自然月的第N天”。第二如果你不加DISTINCT用户在一天内登录多次会导致活跃表膨胀关联后会重复计数留存率虚高。第三在面试中如果能顺嘴说出“我要先压缩活跃表、避免数据膨胀”面试官会非常认可因为这说明你不是第一次写这类查询。如果数据库是SQL ServerDATE_ADD要换成DATEADD(day, 1, date)如果是PostgreSQL可以直接写first_date INTERVAL 1 day。不同数据库函数名不一样面试时一定要说自己熟悉的是哪种方言这也是一个沟通技巧。3.3 行列转换用CASE WHEN聚合还是PIVOT行列转换在数据分析岗位的日常报表里太常见了比如“每个品类每个月的销售额希望横向展示成月份列表”。这个需求就是把长表变宽表。先说最通用的CASE WHEN聚合写法SELECT category, SUM(CASE WHEN MONTH(order_date) 1 THEN order_amount END) AS jan_amount, SUM(CASE WHEN MONTH(order_date) 2 THEN order_amount END) AS feb_amount, SUM(CASE WHEN MONTH(order_date) 3 THEN order_amount END) AS mar_amount FROM orders GROUP BY category;这里有个细节CASE WHEN不成立时返回NULL而SUM(NULL)不会报错会直接忽略所以不用特意写ELSE 0。当然如果你希望结果里没有NULL而是显示0就必须加ELSE 0。有些数据库支持PIVOT比如SQL Server写起来更简洁SELECT category, [1] AS jan_sales, [2] AS feb_sales, [3] AS mar_sales FROM sales_data PIVOT (SUM(sales_amount) FOR month_num IN ([1], [2], [3])) AS pvt;但MySQL 5.7及以下版本不支持PIVOT所以CASE WHEN聚合是通用方案也是面试时最稳妥的答案。反过来宽表变长表也是常考题思路是用UNION ALL把多列拆成多行或者用CROSS JOIN配合CASE生成多行。这种行列思维转换本质上考察的是对数据组织方式灵活处理的能力数据分析师做报表固化时天天都要用到。4. 面试里最容易翻车的细节陷阱4.1 NULL值count(*) 与 count(列名) 不是一回事我面试过一位候选人让他统计“每个部门有邮箱的人数”他写的是SELECT department, COUNT(*) FROM employees GROUP BY department;这道题的考点就是他完全没看懂“有邮箱”这三个字。正确写法应该是COUNT(email)COUNT(列名)会自动忽略该列为NULL的行而COUNT(*)会把整行都算进去哪怕邮箱是NULL。两者在数据有空缺时结果差异很大。另一个相关的坑是空字符串。很多业务系统里“没有邮箱”并不是存成NULL而是存成空串。如果只写COUNT(email)空串也会被算进去因为空串不是NULL。要严谨过滤空字符串应该写成COUNT(CASE WHEN email IS NOT NULL AND email ! THEN user_id END)在面试时主动问一句“邮箱字段是NULL还是空字符串”能直接体现你的经验值。这个细节看起来小但真实数分工作里至少会把报表结果搞错一次我印象特别深。4.2 “去重”不是简单的select distinct很多人一听到“去重”就写SELECT DISTINCT但真实业务里“去重”至少有三种含义。第一种是整行完全重复可以用DISTINCT。第二种是按某几个字段去重保留任意一条比如用户维度表里同一个用户出现多行只需要保留一行通常用DISTINCT ONPostgreSQL支持或者分组取第一条。第三种是按某一字段去重并且保留最新一条比如用户订单表里同一个用户有多笔订单你想看每个用户最近一次的消费这时必须用窗口函数先排序再取第一条。第三种要求在上面已经写过代码这里再强调一次用ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC)生成序号外层过滤rn 1。如果面试官追问“要是最新的两条数据你想都留下怎么办”那就要改用RANK()或DENSE_RANK()。这些追问的答法会直接影响你的评价。另外COUNT(DISTINCT)在数据量大的时候性能并不好而且如果某个字段大量为NULLCOUNT(DISTINCT field)不会把NULL计进去这也是一个容易被忽略的问题。4.3 日期区间的边界问题日期处理是SQL面试最容易出错的地方因为它牵扯到“时间戳”和“日期”两个不同的字段类型习惯。如果时间字段是datetime类型想统计1月份的数据用BETWEEN 2024-01-01 AND 2024-01-31是会漏数据的。因为1月31日的记录里如果时间戳是2024-01-31 23:59:59还能被包含进去但如果业务时间戳精确到毫秒比如2024-01-31 23:59:59.999仍然可能处于边界之外。稳妥的做法是WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-02-01 00:00:00也就是“含头不含尾”的半开区间写法这样2月1日零点整的数据不会被误算进1月同时1月31日最后一毫秒的数据也能被覆盖到。还有一个场景是时区问题。有些公司数据库存的是UTC时间而业务统计用的是东八区时间如果直接按数据库日期分组每天的统计口径都会偏移8小时。正确做法是先转换时区再截取日期SELECT DATE(CONVERT_TZ(create_time, 00:00, 08:00)) AS biz_date这个问题在小公司里不常见但在很多数据量大的互联网公司几乎是必考题。能主动提出“这个日期字段是UTC还是东八区”在你面试时是非常加分的加分项。4.4 性能意识面试官的隐藏考点我知道有些候选人不理解为什么数据分析面试会问SQL优化。其实原因很简单真实生产环境里的表往往不是面试题里那种几百行的练习表而是几千万甚至上亿行的超大表。最简单的性能点是“先过滤再聚合”比如SELECT user_id, COUNT(*) FROM user_behavior WHERE create_date 2024-01-01 GROUP BY user_id;如果你在GROUP BY之前先用WHERE把数据范围缩小处理的数据量会大幅减少。很多人会把过滤条件漏写或者把WHERE写成HAVING这既语法不对又性能差因为HAVING是在分组之后过滤数据已经膨胀过一遍了。另外尽量不要在索引字段上套函数比如WHERE MONTH(create_date) 1这个写法会让索引失效因为数据库必须先对每一行计算MONTH(create_date)才能比较。正确做法是直接给日期范围条件。面试时如果能顺口说出“要看执行计划用EXPLAIN看看有没有走索引”基本可以证明你是有真实项目经验的这一条就能拉开和纯背题候选人的差距。5. 刷题路径建议与面试答题技巧5.1 从零到面试的刷题顺序想短期内把SQL面试题准备扎实我建议按下面四步走。第一步把基础语法彻底过一遍。SELECT、WHERE、JOIN、GROUP BY、HAVING、ORDER BY、LIMIT、CASE WHEN这些用练习网站刷一遍做到闭着眼都能写对。第二步专门练窗口函数。ROW_NUMBER()、RANK()、DENSE_RANK()、SUM() OVER()、LAG()、LEAD()不用追求太多先把最常用的这六七个练熟。第三步刷高频业务题型。连续N天、留存率、分组TopN、按用户去重最新记录、累计求和、环比同比这些题型有固定的套路理解一套就能解决一大片。第四步做综合场景题。比如“给你订单表和用户表统计每个城市每月的新客消费额”这种题目会把前面所有知识点揉在一起最接近真实面试。我不太建议一上来就在各种刷题平台里从第一题刷到最后一题那样效率太低。面试题的范围其实很固定用专题击破的方式准备一周左右就能有明显的效果。5.2 拿到SQL题先别急着写很多人面试时容易紧张看到题目就像考试一样立刻动手写这其实是大忌。SQL题目往往描述不完整隐含的业务口径需要你主动确认所以拿到题之后应该先做三件事。第一件事快速说清楚你的理解。比如“我理解这张订单表以order_id为主键一个用户可能有多笔订单我们要统计的是2024年每个月活跃下单的去重用户数对吗”这句话会让面试官觉得你沟通能力在线也会顺便把题目的歧义消除。第二件事说出实现思路。我经常跟身边人说哪怕你不写完整代码只要能把“第一步先过滤状态第二步按用户分组第三步用COUNT(DISTINCT user_id)”讲清楚面试官已经能知道你会不会。第三件事再落笔写SQL。写的时候边写边解释如果卡住了也可以在思路正确的前提下得到部分分数。如果你完全不会窗口函数也不是完全没救。分组TopN可以写成子查询加关联SELECT a.user_id, a.amount FROM sales a WHERE ( SELECT COUNT(*) FROM sales b WHERE b.category_id a.category_id AND b.sales a.sales ) 3;这种解法的性能不好但思路是正确的至少能证明你知道“每组前3”是什么意思。面试官想要的不一定是唯一解而是你在遇到问题时能不能稳住用已有知识找到可行方案。5.3 高频SQL面试题速查表把多年面试和实际工作里高频出现的SQL题目整理成一张表方便你考前翻一翻。面试题核心考点推荐解法连续登录3天以上的用户窗口函数、日期差值ROW_NUMBER() OVER 日期减序号分组每个部门薪资Top3分组TopN、窗口函数ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 外层过滤计算次日/3日/7日留存率新增用户口径、关联去重新增用户集合 活跃用户集合 LEFT JOIN每个月销售金额按月份列展示行列转换SUM(CASE WHEN MONTH(...) n THEN ... END)同一个用户保留最新一条记录按字段去重ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC)rn1每日累计销售额累计求和SUM(amount) OVER (ORDER BY date)当月和上月的销售额对比环比计算LAG(amount, 1) OVER (ORDER BY month)存在重复数据统计真实用户数NULL和去重COUNT(DISTINCT user_id)注意空字符串这八类题目基本覆盖了数据分析面试里百分之八十的SQL场景。把每一类都练到能默写出来并且能讲清楚每一步在做什么应对绝大多数面试是够用的。最后再分享一个我个人的经验面试时写SQL可以故意在关键地方放慢语速边写边说出“这里要加DISTINCT因为一个用户一天可能下多单”这个举动会让面试官直观感受到你的业务敏感度。写完不要马上说“写完了”自己先拿一组极简数据在心里跑一遍确认边界情况都覆盖了再告诉面试官“我检查过两个边界场景”。这种习惯在真实工作中特别值钱因为上线之后再发现口径问题代价远比面试时多花两分钟大得多。