数据分析面试SQL题汇总:高频考点与易错点全解析
简介一份面向数据分析岗位的SQL面试题汇总文档适合求职者、转行者以及初级数据分析师按需复习。文档围绕建表、插入数据、排序、连接、分组、聚合函数、日期操作等高频考点展开通过两道典型面试题完整演示了从数据加载到计算活跃度、次日留存的SQL实现思路并整理了知识点清单与ONLY_FULL_GROUP_BY报错等实操中常见的解决方法。资源包含1个docx文件共564KB轻量便携可随时查阅。目前已有1606人学习内容按面试题、知识点总结等模块组织既能巩固SQL基础查询语法也能培养留存计算、行转列等业务分析思维。读者可从中掌握GROUP BY、CASE WHEN、LEFT JOIN等语句的配合使用并熟悉DATE_FORMAT、GROUP_CONCAT、MIN、COUNT等函数在真实分析场景中的具体用法对提升数据分析面试中的SQL实操能力很有帮助。1. 数据分析面试里的SQL题为什么背熟语法还是挂在“第二高”有位A同学面试前一天把窗口函数背得滚瓜烂熟结果上机一碰到“统计每个部门工资第二高的员工”就卡住了。他写得出来ROW_NUMBER却没想过工资并列时到底该算并列第一还是并列第二、要不要跳号。像“数据分析面试题-SQL面试题汇总”这类文档把数据分析岗位面试里反复出现的SQL题按考点收在一起附上思路和易错点目的就是补上这道“从语法到口径”的坎。这套东西适合两类人一类是准备数据分析、商业分析面试的求职者用来摸清自己的短板另一类是要出SQL笔试题的面试官用来对照自己出的题有没有覆盖高频考点。SQL题表面考语法本质考三件事能不能把业务问题拆成表和条件能不能选对度量口径以及写出来的代码经不经得起一句句追问。2. 面试考点地图窗口函数、聚合分组、多表关联先练哪个拿到任何一份SQL面试题汇总第一件事不是从头刷而是先看它覆盖了哪些考点。数据分析岗的SQL题看着花样多九成落在三个方向窗口函数、聚合分组、多表关联。这三个方向正好对应日常分析的三个基本动作看排名与对比、看汇总与占比、看跨表拼接。下面把每个方向的考法、最小可运行写法和容易被追问的点过一遍。2.1 窗口函数不会它面试题直接少一半分窗口函数在数据分析岗的面试里出现频率最高原因是它和“看整体里的个体”这件事强绑定。日常需求里最常见的表述是给每个部门排名、算每个用户最近一次下单、算同比环比、算累计值。这些需求用GROUP BY写会很别扭因为GROUP BY会把多行压成一行而窗口函数不改变行数每一行都能带着聚合结果一起保留下来。面试官看一眼你的第一反应是窗口函数还是GROUP BY基本就能判断你有没有做过真实分析。最小可运行写法SELECT department, employee_name, salary, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary DESC ) AS rn FROM employee;这段代码的逻辑是PARTITION BY department 把数据按部门切成若干份ORDER BY salary DESC 在每个部门内按薪资从高到低排序ROW_NUMBER() 在每份里生成从 1 开始的连续序号。这个写法是“取每个部门薪资前三名”“取每个部门工资最高的人”这类题的地基。窗口函数里最容易被追问的是三个排序函数的区别建议直接记成一张表函数相同工资时怎么排典型用途ROW_NUMBER()强制给不同序号谁先谁后不确定取唯一排名、分页RANK()并列同号后面的会跳号竞赛排位、取“第几名区间”DENSE_RANK()并列同号后面不跳号取“第N个不同档位”所以“工资第二高”这道题如果题面没说明并列怎么处理用 RANK 或 DENSE_RANK 比 ROW_NUMBER 稳妥两个人并列第一时ROW_NUMBER 会把其中一个人排成第 2这和你心里的口径不一样。我一般会在答案里先把话讲清楚“我按工资档位理解第二高是第二档工资”再落代码面试官想不认可都难。窗口函数还有一个高频考点是 LAG / LEAD。比如“计算每个用户相邻两次登录的时间间隔”就是先按用户分区、按时间排序取上一行的日期SELECT user_id, login_date, LAG(login_date, 1) OVER ( PARTITION BY user_id ORDER BY login_date ) AS prev_login_date FROM login_log;LAG 的参数是两个第一个是取哪一列第二个是往上数几行1 就是上一行。结果里 prev_login_date 为 NULL 的那一行说明这是该用户第一次登录。这道题的常见误用是漏掉 PARTITION BY user_id漏了之后整个表的上一行会被当成这个用户的上一行结果全是错的而且不容易被肉眼发现。这里还有一个高频追问窗口函数能不能直接放到 WHERE 里做过滤不能。WHERE 是在窗口计算之前执行的想按排名过滤得先把窗口计算结果包一层子查询再过滤。2.2 聚合与分组面试官最爱设的COUNT(DISTINCT)陷阱聚合分组是每个面试题汇总里题量最大的板块因为它最贴近“数据指标”这个概念。题目形态通常是这样按渠道统计活跃用户数、按月统计总销售额、按地区统计平均客单价。看上去都是 GROUP BY但坑都藏在统计口径里。最经典的一道题是“统计每个渠道的活跃用户数和收入”SELECT channel, COUNT(DISTINCT user_id) AS active_users, SUM(pay_amount) AS revenue FROM user_behavior WHERE dt 2025-06-01 AND dt 2025-07-01 GROUP BY channel;逻辑说明先用 WHERE 把数据限定在 6 月再按 channel 分组COUNT(DISTINCT user_id) 对组内的 user_id 去重计数SUM(pay_amount) 合计组内金额。这里 DISTINCT 是灵魂user_behavior 是行为明细表一个用户一天可能有多条行为记录不加 DISTINCT活跃用户数会被重复计算。关于 COUNT面试官喜欢追问三兄弟的区别COUNT(1)、COUNT()、COUNT(字段)。前两个对多数关系型数据库来说没有性能差异都会数行COUNT(字段) 会跳过 NULL。也就是说如果某个字段没填值COUNT(字段) 的结果会比 COUNT() 小。题目里“统计有手机号的用户数”就必须用 COUNT(phone)用 COUNT(*) 就是错的。另一个常被翻车的点是 WHERE 和 HAVING 的边界。WHERE 是分组前过滤行HAVING 是分组后过滤组。想筛掉“只有 1 个会话的用户”不能写 WHERE COUNT() 1因为 WHERE 执行时聚合还没算完正确做法是 GROUP BY user_id HAVING COUNT() 1。还有一点很多数据库不允许 WHERE 里直接引用 SELECT 里的聚合别名报错时先检查是不是把 HAVING 写成 WHERE 了。2.3 多表关联与空值扩散为什么LEFT JOIN后数据变多多表关联是面试里最考验基本功的部分因为它的反直觉结论特别多。最经典的一个LEFT JOIN 之后行数不是变少而是可能变多。很多人以为 LEFT JOIN 是“以左边为准少了往右补”结果发现 COUNT(*) 比左表还大当场懵掉。原因在于一对多关系。左表一条订单右表可能有多条商品明细JOIN 的结果是左表这行复制成多行每行对应一条明细。看一个安全写法SELECT o.order_id, o.user_id, d.item_count FROM orders o LEFT JOIN ( SELECT order_id, COUNT(*) AS item_count FROM order_items GROUP BY order_id ) d ON o.order_id d.order_id;逻辑说明先把 order_items 按 order_id 聚合成“一个订单有几件商品”的单行结果再和 orders 关联。这样左表每一行最多匹配到一行不会产生行数膨胀。这也是数据分析里“先聚合、后关联”的典型思路面试官会专门出题看你会不会这么处理。JOIN 还有一个隐藏坑是空值扩散。假设订单表里有些 order_id 本身是 NULL或者右表关联键为 NULLLEFT JOIN 仍然会保留左表的行但右表字段填 NULL。后面一旦对这个字段做 SUM 或 COUNT结果就会偏因为 SUM 会忽略 NULL。所以面试里写完 JOIN要习惯性问自己一句关联键有没有 NULLNULL 会不会影响指标。我一般建议新手把三个方向按顺序练先聚合分组再窗口函数最后多表关联。聚合是基础窗口是加分项多表关联是区分度最高的部分。练完这三个方向再去看SQL面试题汇总里那些“难题”会发现本质都是它们的组合。3. 把三道必练题跑一遍连续登录、行转列、留存率怎么答面试题汇总里有些题属于“必练但必错”的类型连续登录N天、行转列与列转行、留存率计算。这三道题覆盖了窗口函数、聚合、JOIN和日期处理的组合用法。建议你先自己写一遍再对照下面的写法重点看自己的口径和边界处理。3.1 连续登录N天先想清楚“同一天重复登录算不算”题目有一张登录表 login_log(user_id, login_date)统计至少连续 3 天登录的用户。先建表并插入测试数据CREATE TABLE login_log ( user_id INT, login_date DATE ); INSERT INTO login_log VALUES (101, 2025-06-01), (101, 2025-06-02), (101, 2025-06-03), (102, 2025-06-01), (102, 2025-06-03);标准解法是“日期减行号”法WITH t AS ( SELECT user_id, login_date, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) AS rn FROM ( SELECT DISTINCT user_id, login_date FROM login_log ) dedup ) SELECT DISTINCT user_id FROM t GROUP BY user_id, DATE_SUB(login_date, INTERVAL rn DAY) HAVING COUNT(*) 3;逻辑说明先对登录记录去重因为同一天多条记录会干扰连续性判断。然后按用户分区、按日期排序得到行号 rn。如果登录日期是连续的login_date 减去 rn 会得到同一个常数这个常数相当于“连续段的编号”一旦中断常数就会变化。最后按 user_id 和这个常数分组组内数量大于等于 3就是连续登录 3 天的用户。这个解法有一个容易忽略的点DATE_SUB(login_date, INTERVAL rn DAY) 的方言差异。不同数据库写法不同面试时先说清楚“我想表达登录日期减行号这个意图”再按你熟悉的数据库写面试官一般不会揪着函数名不放。3.2 行转列与列转行改表结构是面试官最爱追问的点题目有一张月度销售表 monthly_sales(year, month, amount)需要把每个月作为一列输出。行转列的写法SELECT year, SUM(CASE WHEN month 1 THEN amount ELSE 0 END) AS m1, SUM(CASE WHEN month 2 THEN amount ELSE 0 END) AS m2, SUM(CASE WHEN month 3 THEN amount ELSE 0 END) AS m3, SUM(CASE WHEN month 12 THEN amount ELSE 0 END) AS m12 FROM monthly_sales GROUP BY year;逻辑说明CASE WHEN 把符合月份的金额挑出来不符合的置 0再用 SUM 把同一年份的金额合计。这样 month 字段的取值被“平移”成了列名。如果这张表同一年同一个月只有一条记录把 SUM 换成 MAX 也可以如果有多条记录用 MAX 会丢数据必须用 SUM。这是面试官最常追问的边界条件。列转行是反方向把一年 12 列转回 12 行。用 UNION ALL 拼接SELECT year, 1 AS month, m1 AS amount FROM year_sales UNION ALL SELECT year, 2 AS month, m2 AS amount FROM year_sales UNION ALL SELECT year, 3 AS month, m3 AS amount FROM year_sales;这里特别注意要用 UNION ALL 而不是 UNION。UNION 会去重如果两张表里存在相同行会被合并掉数据就少了。面试题汇总里这道题的正确率很低错的几乎都是栽在 UNION 去重上。3.3 留存率计算LEFT JOIN的条件写在哪决定你过不过题目用户活跃表 user_active(user_id, dt)计算 6 月 1 日新增用户在 6 月 8 日的留存率。留存率的第一原则分母是新增用户数分子是这群新增用户里第 7 天还活跃的人数。先定义“新增用户”首次活跃日是 6 月 1 日的用户。WITH first_active AS ( SELECT user_id, MIN(dt) AS first_dt FROM user_active GROUP BY user_id HAVING MIN(dt) 2025-06-01 ) SELECT COUNT(DISTINCT a.user_id) AS new_users, COUNT(DISTINCT b.user_id) AS retained_users, COUNT(DISTINCT b.user_id) / COUNT(DISTINCT a.user_id) AS retention_rate FROM first_active a LEFT JOIN user_active b ON a.user_id b.user_id AND b.dt DATE_ADD(a.first_dt, INTERVAL 7 DAY);逻辑说明第一步用 MIN(dt) 找出每个用户最早活跃日期HAVING 限定为 6 月 1 日得到新增用户集合。第二步把新增用户和活跃表做 LEFT JOIN关联条件是同一个用户 且 活跃日期等于首活日期加 7 天。LEFT JOIN 保证没在第 7 天回来的用户也被保留只是 b 表字段为 NULL。这样 COUNT(DISTINCT b.user_id) 数出来的就是留存用户除以分母就是 7 日留存率。这里有一个面试翻车率极高的细节日期条件写在哪。如果写成 ON a.user_id b.user_id然后 WHERE b.dt DATE_ADD(...)结果就全错了。因为 WHERE 会把 LEFT JOIN 没匹配上的行过滤掉LEFT JOIN 实际变成了 INNER JOIN留存率永远等于 1。很多老手都会在这个地方走神写完一定要自查关联条件是不是都写在 ON 里。4. 面试答题的四步流程拆题、定思路、落盘、讲假设会写 SQL 不等于能过面试。数据分析岗的 SQL 面试通常在半小时内给两三道题要的不是“最终结果对”而是“思路清楚、边界明确、能讲出口”。我总结了一套固定流程能减少临场发挥的波动。4.1 先拆题把中文描述翻译成“表-键-条件-目标”拿到题目先别急着写代码在草稿纸上写四列涉及哪些表、表之间的关联键、过滤条件、目标字段。拿一道高频题举例“统计 6 月每个渠道的付费用户数和收入”。拆出来是要素内容表用户表 users、订单表 orders关联键user_id条件订单支付时间在 6 月目标按渠道分组去重用户数、金额合计拆完题代码基本是填空。大部分错误发生在“题目没拆清就动手”比如漏了“付费”这个状态条件把未支付订单也算进收入。4.2 定框架先写伪SQL再决定用窗口还是自连接第二件事是判断题型框架。需要“每个组内排序、每组前几名”用窗口函数需要“同一张表里行和行比较”用自连接需要“按维度汇总指标”用聚合分组。先写一段伪代码把骨架立起来再补细节SELECT 渠道, COUNT(DISTINCT 用户ID), SUM(金额) FROM 订单表 WHERE 支付时间在6月 GROUP BY 渠道;伪代码的价值在于先锁定逻辑主干后面填充字段时不容易跑偏。如果发现题目要的是“每个渠道里客单价前 10 的用户”伪代码的主干就换成窗口函数而不是 GROUP BY。这一步能直接筛掉一半的错误思路。4.3 落盘与自查空值、去重、时间边界、性能四件套代码写完后在提交前按这四个维度自查一遍。空值JOIN 键有没有 NULLCOUNT(字段) 会不会因为 NULL 少算去重事实表里同一用户多条记录时计数逻辑对不对时间边界条件里 “6月” 是写 dt 2025-06-01 AND dt 2025-07-01还是写 BETWEEN。BETWEEN 看似方便但边界包含关系在不同数据类型下容易出错我习惯用左闭右开。WHERE dt 2025-06-01 AND dt 2025-07-01性能问题上面试一般不会要求调优但如果你主动写出“先把明细聚合再 JOIN”“JOIN 字段有索引更好”会明显加分。大表 JOIN 小表时把小表去重或聚合后再关联是面试官最想看到的习惯。4.4 主动声明假设把并列、NULL、时间口径先说出口SQL 面试题最大的特点是语文题同一个描述可以有两种理解。比如“第二高”比如“6 月的收入”是算支付时间还是下单时间比如“留存用户”是活跃过就算还是当天有行为才算。面试官把题递给你时你主动问一句“这里我按 XX 口径理解可以吗”比闷头写完再被发现理解偏了要加分得多。如果题面确实没给并列规则我的习惯是直接回答“有两种常规理解如果按不跳号档位算我用 DENSE_RANK如果按不重复的物理顺序算我用 ROW_NUMBER。题目没限定的话我默认取每个部门的第二档工资。”这样既展示了知识边界又给出了可执行的默认值。5. SQL面试题里的高频坑现象、原因、修复一条条说清SQL 面试题汇总里最值钱的不是答案而是易错点。下面四条是踩坑频率最高的记录每条都按“现象、原因、解决”展开建议收藏后反复看。5.1 空值陷阱SUM变小、COUNT变少、排名漏人现象统计某渠道的收入SUM 结果比业务账单里少了一截统计有手机号的用户数人数明显小于预期按薪资排序取前 10有几个人怎么都不出现。原因SUM、AVG 会忽略 NULLCOUNT(字段) 也会跳过 NULLJOIN 时关联键为 NULL 的行匹配不上CASE WHEN 判断 NULL 时永远不成立因为 NULL 不等于也不不等于任何值。解决先查数据里有没有 NULL再决定要不要用 COALESCE 兜底。收入合计写成 SUM(COALESCE(pay_amount, 0))可以避免因为 NULL 少算。判断空值只能用 IS NULL 或 IS NOT NULL写成 NULL 等于没写。针对排名漏人先确认排序字段是不是有 NULLNULL 在多数默认排序里排最后不是“没排上”而是被排到末尾了。5.2 去重陷阱跨渠道汇总时用户被数了两次现象单个渠道的活跃用户数加起来比全站去重后的活跃用户数大很多。每个渠道分开看都对一汇总就重复。原因在渠道维度 GROUP BY 后再 COUNT(DISTINCT user_id)一个用户如果出现在两个渠道两个渠道里都被计数跨渠道相加自然重复。这个问题在埋点数据里特别常见因为一个用户一天可能访问多个渠道页面。解决先确定指标粒度。如果业务定义是“全站去重活跃用户数”那汇总时不能用各渠道直接相加要么在更高层级重新去重要么先按用户维度打标“首次来源渠道”再计数。面试时遇到汇总对不上的题先反问一句“这里按什么粒度去重”比埋头调 SQL 更有效。5.3 窗口函数边界漏了PARTITION BY排名算错了组现象题目要求“每个部门内部排名”结果整张表只有一个排名序列部门 A 的人排完紧接着排部门 B 的人。代码能跑结果也“有排名”就是和题意的分组对不上。原因写窗口函数时漏了 PARTITION BY。ORDER BY 只管排序不管分组没有 PARTITION BY整个结果集被视为一个组。解决每写一个窗口函数默念一遍“PARTITION BY 决定组ORDER BY 决定组内顺序”。回查时把结果里有几个 1 数一下有几个 1 就说明分了几组。如果全表只有一个排名序号是 1那基本可以断定 PARTITION BY 漏了。5.4 自连接翻车笛卡尔积爆炸现象一张几千行的用户表做自连接结果出来几十万行甚至把数据库跑卡。数据里明明没那么多关系结果却异常膨胀。原因自连接时只写了表关联没写“行和行之间怎么比较”的条件。比如要“找出所有互为好友的用户对”ON 条件只写 a.user_id ! b.user_id就会产生近似 N 的平方行的笛卡尔积。解决写自连接前先明确要比较的是哪两行把比较条件完整写进 ON。拿“互为好友”举例除了关联键还要限制 a.user_id b.user_id 去掉重复对。数据量大时先按条件过滤再自连接能避免无谓的中间结果膨胀。提示面试官问“你的查询慢怎么办”最常见的三种回答方向就是减少 JOIN 前的数据量、先聚合后关联、确认关联字段有索引。自连接翻车是最能体现这三点的场景。6. 学有余力时的关键技巧把错题归档成自己的SQL错题本刷 SQL 面试题汇总最容易踩的误区是“刷了一遍就觉得自己会了”。实际上第二遍做同一道题很多人照样错在同一个地方。我的习惯是准备一个错题本只记一种内容第一次没做对的题。每道题按四列归档题目类型、我的错因、正确写法要点、一句话口诀。维护成本很低收益却很直接。题目类型我的错因正确写法要点一句话口诀连续登录3天没去重先DISTINCT再编号连续段 日期减行号7日留存率LEFT JOIN的条件写进了WHERE日期条件放ON不放WHERE流失用户靠LEFT JOIN兜底每个部门第二高并列口径没定义用DENSE_RANK不跳号并列看档位不编号行转列用了MAX丢明细多条记录用SUM同一维度多条时聚合用SUM这个错题本不是用来抄答案的而是用来压缩面试前的复习时间。正确写法和代码不用全抄只抄“我当时错在哪”和一句话口诀。面试前一天翻一遍比重新刷 50 道题有效率得多。拿我自己的经历说第一次面试数据分析岗时我在“同一天重复登录”这种地方栽过跟头当时只是改对了答案没有记录错因下一场面试换了张表、同样的逻辑又错了一次。后来老老实实把每道错题的错因和口诀归档遇到类似题型基本十拿九稳。做一本自己的SQL面试错题本比收集十份“SQL面试题汇总”都管用。希望帮到你。本文还有配套的精品资源点击获取