news 2026/10/15 12:59:58

SQL笔试题高分攻略:窗口函数与连续登录等高频考点解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL笔试题高分攻略:窗口函数与连续登录等高频考点解析

如果你在准备大厂面试,SQL笔试题大概率是笔试环节里那位“沉默杀手”。它不像算法题那样有标准模板,也不像八股文可以突击背诵,但恰恰是它筛掉了不少基本功看似扎实的候选人。我这两年帮学生、朋友复盘过好几批头部科技公司的SQL笔试题,也亲自参与过候选人答卷的评审,慢慢摸清了这类题目的出题逻辑。这篇文章就把我看到的、以及我自己踩过的坑,完整拆给你看。

无论你准备的是后端开发、数据开发还是数据分析岗,这套SQL笔试背后的考察思路都是相通的。我会用具体的题目形态,而不是泛泛而谈的概念,带你过一遍高频考点、解题模板、易错点和备考节奏。如果你是初学者,可以按章节顺序读;如果你马上要笔试,建议直接跳到第4部分,那里全是血泪教训。

1. 出题人视角:这类SQL笔试题真正考的是什么

很多人以为SQL笔试题考的是“会不会写SELECT”,其实远没那么简单。我翻过不少大厂的笔试题,发现它们几乎都是把真实业务场景压缩成一张或几张表,让你在规定时间内完成取数、统计、分析或者建模。表面上考语法,实际考的是三样东西:数据敏感度、逻辑拆解能力、工程思维。

先给一张考点地图,这几类几乎覆盖了90%的题目:

考点维度常见题型考察目的
基础查询与过滤WHERE、DISTINCT、ORDER BY是否具备最基础的取数能力
聚合与分组SUM、AVG、GROUP BY、HAVING是否理解数据汇总的粒度变化
多表关联INNER JOIN、LEFT JOIN、全连接是否理解业务表之间的关联关系
窗口函数ROW_NUMBER、RANK、LAG、SUM OVER能否处理排序、排名、前后值、累计等分析问题
复杂业务建模留存、会话、树形结构、最大连续能否把业务问题翻译成SQL模型
性能与优化EXISTS与IN、索引、避免笛卡尔积是否有基本的性能意识

注意看,前两行是基本功,后四行才是真正的分水岭。基本功大家都会背,但一到真实场景就犯迷糊。

1.1 大部分候选人挂在“业务建模”上

举一个很多候选人复盘时会惊叹的题目:统计每个部门薪资排名前3的员工。这题看起来简单,但有人用GROUP_CONCAT排序后截断,有人用自连接比较工资个数,还有人直接ORDER BY后LIMIT 3,完全忽略了“每个部门”。

出题人给这个题,并不是真想让你写一行ROW_NUMBER(),而是想看你在面对“每个部门”这种分组诉求时,能不能立刻想到分区窗口函数。如果只想到全局排序,说明你还不理解PARTITION BY的意义。如果想到自连接,说明你学过但不懂为什么要用窗口函数。只有基础扎实、有实战经验的人,才会在第一反应里写出:

SELECT dept_id, name, salary, emp_rank FROM ( SELECT dept_id, name, salary, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS emp_rank FROM employee ) t WHERE emp_rank <= 3;

这题还藏着一个细节:题目说的是“薪资排名前3的员工”,那如果第三名有并列两个人怎么办?如果用ROW_NUMBER(),并列的两人会随机选一个,这在业务上通常是不对的。所以我在辅导候选人时,总是反复强调:先搞清楚题目对并列的处理偏好。一般“前N名”用DENSE_RANK()或RANK()更稳妥,除非题目明确说“取前3名员工”且不关心并列。

1.2 语法细节越严苛,越能筛掉“只会用工具点”的人

现在很多企业的SQL笔试题都支持跑真实数据库,你写的SQL会被真实执行,这就意味着语法细节逃不过去。比如MySQL的ONLY_FULL_GROUP_BY模式下,SELECT的列必须出现在GROUP BY里,或者是聚合函数。很多人平时用Navicat之类工具写查询,表数据量小,不报错就完事,但一旦笔试环境默认开启严格模式,马上原形毕露。

再比如窗口函数不能直接放在WHERE子句中,需要包一层子查询;HAVING和WHERE的执行顺序不同;LEFT JOIN后如果对右表字段加WHERE条件,可能导致左表行被过滤掉。这些看起来都是“小坑”,但在笔试成绩上就是天壤之别——因为大厂笔试自动判题时,差一行结果都是零分。

2. 高频必练题型:TopN、连续N天、累计聚合

如果你时间有限,只够练三类题,那我的建议是:TopN、连续N天、累计聚合。这三类几乎每年都出现在各大厂笔试里,而且它们背后代表的正好是窗口函数最核心的三种能力:分组排序、行间偏移、框架聚合。

2.1 TopN问题:四种解法,为什么窗口函数是首选

上面已经给了TopN的标准写法。我再说说其它解法,理解它们,你才能真正领悟窗口函数的优势。

解法一(老MySQL):用用户变量按部门排序,再外层过滤。这种写法依赖于变量赋值顺序,很容易出错,可读性也很差。

解法二(自连接子查询):找那些“在部门内薪资比它高的人少于3个”的员工:

SELECT e1.dept_id, e1.name, e1.salary FROM employee e1 WHERE ( SELECT COUNT(DISTINCT e2.salary) FROM employee e2 WHERE e2.dept_id = e1.dept_id AND e2.salary > e1.salary ) < 3 ORDER BY e1.dept_id, e1.salary DESC;

这个写法能正确处理并列薪资,因为它统计的是“比当前薪资高的不同薪资个数”。如果并列薪资都是最高,那它们的排名都是1,都会入选。逻辑没问题,但问题在于子查询对每一行都要扫描整个部门,数据量一上去就非常慢。

解法三(窗口函数)则把排名计算放在一次扫描中完成,性能高、可读性好。笔试中选它,不仅答案正确,还显得你很专业。

不过在写窗口函数时,一定要搞清楚RANK()、DENSE_RANK()、ROW_NUMBER()的区别:

函数排名是否跳跃并列处理典型场景
ROW_NUMBER()不跳跃并列时随机/按规则给不同序号取物理前N条
RANK()跳跃并列占同一排名,下一个跳过体育比赛名次
DENSE_RANK()不跳跃并列占同一排名,下一个连续“前N名”业务需求

我通常建议:只说“取前N条记录”用ROW_NUMBER();说“排名前N”用DENSE_RANK();说“排序用RANK()”要小心,因为它会留坑。

2.2 连续N天登录问题:这题背后是“分组定界”

连续性问题是我见过刷掉人数最多的一类。题目形态通常是:有一张用户登录表login_log(user_id, login_date),找出连续登录至少3天的用户。很多人会尝试用LAG(login_date)判断前后日期差是否为1,但这思路在面对“连续5天”时会变得很啰嗦。

有一个非常优雅的通用解法:日期减去行号,连续的日期会落到同一个小组里。

先对每个用户去重日期、按日期排序、加上行号,然后用login_date减去ROW_NUMBER()对应的天数。如果日期是连续的比如1号、2号、3号,而行号是1、2、3,那么1号减1天得到0号,2号减2天得到0号,3号减3天得到0号,这样它们就归到了同一个分组grp。一旦中间断档,比如1号、3号,那1号减1天是0号,3号减2天是1号,分组不同,就识别出了断档。

完整SQL如下:

WITH dedup AS ( SELECT DISTINCT user_id, login_date FROM login_log ), 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 grp FROM dedup ) SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS consecutive_days FROM ranked GROUP BY user_id, grp HAVING COUNT(*) >= 3;

这里有个关键点:必须先对(login_date)去重,因为同一天登录多次会干扰行号。另外,如果数据量大,这个分组逻辑也能高效执行。

如果你接触的是Hive,日期减法可以用DATE_SUB。不同数据库的差异不大,核心就是那个“日期减行号”的思维。

2.3 累计求和、同比环比:窗口函数的框架也能解决

累计求和的典型场景是“求每个产品每个月截止到当前的累计销售额”。表结构大概是sales(month, product_id, amount)。

SELECT month, product_id, amount, SUM(amount) OVER (PARTITION BY product_id ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_amount FROM sales;

注意,窗口函数里的ORDER BY会改变SUM的默认计算范围。如果不写ROWS BETWEEN,很多数据库的默认框架是从第一行到当前行,所以累计求和如果不小心加了PARTITION BY但漏了ORDER BY,就会变成全部分区总和。这个细节在笔试中很容易被忽略,尤其是当你看到结果里所有行的累计值都一样时,八成就是框架问题。

同比环比也经常考。比如计算每个月的环比增长率:

SELECT month, product_id, amount, LAG(amount, 1) OVER (PARTITION BY product_id ORDER BY month) AS prev_amount, (amount - LAG(amount, 1) OVER (PARTITION BY product_id ORDER BY month)) / NULLIF(LAG(amount, 1) OVER (PARTITION BY product_id ORDER BY month), 0) AS mom_growth FROM sales;

这里用LAG取上个月的值,然后做差比。NULLIF可以避免除零报错,这也是一个容易被忽略的细节。

3. 复杂业务场景题:留存、会话、层级递归

如果TopN和连续题算“基础套路”,那留存、会话识别和树形递归就是加了业务难度的“升级版题目”。它们的特点是:代码量不大,但要把业务逻辑翻译成SQL,非常考验抽象能力。

3.1 次日留存率:数据分析面试的标配

题目一般是给用户活跃日志active_log(user_id, dt),统计每日新增用户中,次日仍然活跃的比例。注意,这里“新增用户”如何定义?如果业务有注册表,可以直接从注册表取;如果没有,通常把每个用户的最早活跃日期视为“新增日”。

先求出每个用户的最早活跃日期作为首日,再关联次日活跃:

WITH first_active AS ( SELECT user_id, MIN(dt) AS install_dt FROM active_log GROUP BY user_id ), active_retention AS ( SELECT fa.install_dt, COUNT(DISTINCT fa.user_id) AS install_cnt, COUNT(DISTINCT CASE WHEN al.dt = DATE_ADD(fa.install_dt, INTERVAL 1 DAY) THEN fa.user_id END) AS next_retain_cnt FROM first_active fa LEFT JOIN active_log al ON fa.user_id = al.user_id GROUP BY fa.install_dt ) SELECT install_dt, install_cnt, next_retain_cnt, ROUND(next_retain_cnt / install_cnt, 4) AS next_day_retention_rate FROM active_retention;

为什么要用LEFT JOIN而不是INNER JOIN?因为在算留存时,很多次日没有回来的用户也必须保留在结果里,否则留存人数会被错误放大。用INNER JOIN的话,你没活跃就是不产生关联行,那next_retain_cnt自然少了,看起来没问题,但install_cnt也会被过滤掉那些没有次日活跃的用户吗?不会,因为install_cnt来自first_active,如果INNER JOIN,那么首日活跃但次日未活跃的用户在关联时根本没有al记录,整行消失,install_cnt就被算小了。所以LEFT JOIN才是正确选择。

还有一个细节:活跃日志可能有同一用户同一天多次活跃,所以统计“人数”时一定要COUNT(DISTINCT user_id),不能直接COUNT(*)。

3.2 会话识别:用SQL给用户行为“分段”

会话识别在用户行为分析里非常常见。比如用户点击日志click_log(user_id, click_time),要求把连续30分钟内无操作视为不同会话,每次有操作时如果距离上次操作超过30分钟,就算一个新会话。

核心思路:先按用户和时间排序,用LAG取上一次点击时间,判断当前时间与上一次的间隔是否超过30分钟,打一个标记0/1。标记为1的行代表新会话起点,然后对这个标记做累计求和,得到会话ID。

WITH clicks AS ( SELECT user_id, click_time, CASE WHEN TIMESTAMPDIFF(MINUTE, LAG(click_time, 1) OVER (PARTITION BY user_id ORDER BY click_time), click_time) > 30 THEN 1 ELSE 0 END AS is_new_session FROM click_log ) SELECT user_id, click_time, SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY click_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session_id FROM clicks;

这个解法很妙,因为它把“间断点识别”和“分组编号”结合在了一起。以后你遇到任何“把连续行为切分成段”的需求,比如电量充满中断、乘车行程分段、异常时间段划分,都可以用这个套路。

注意,不同数据库计算两个时间差的函数不太一样:MySQL用TIMESTAMPDIFF,PostgreSQL用EXTRACT(EPOCH FROM ...),Hive用datediff或unix_timestamp做差。笔试时先确认环境再写,不要默认为MySQL函数。

3.3 树形结构递归:组织架构、商品类目

这类题目一般出现在难度较高的笔试题里,比如给员工表emp(emp_id, emp_name, mgr_id),要求输出每个员工的完整汇报链路。比如某员工A,其经理是B,B的经理是C,最后链为“C > B > A”。

用递归CTE是最清晰的做法。以MySQL 8.0为例:

WITH RECURSIVE emp_tree AS ( -- 锚点:找到顶级管理者(没有上级的人) SELECT emp_id, emp_name, mgr_id, CAST(emp_name AS CHAR(500)) AS path FROM emp WHERE mgr_id IS NULL UNION ALL -- 递归:将员工接到上级链上 SELECT e.emp_id, e.emp_name, e.mgr_id, CONCAT(et.path, ' > ', e.emp_name) FROM emp e JOIN emp_tree et ON e.mgr_id = et.emp_id ) SELECT * FROM emp_tree;

锚点成员先找出树根,递归成员负责把下一层节点拼接到已有链上。停止条件就是没有更多员工满足mgr_id等于当前emp_id。

但这里有一个坑:如果组织架构中存在循环汇报,比如A的上级是B、B的上级是A,递归没有停止条件就会死循环。实际笔试中数据一般不会这么恶心,但你还是要在写完后检查一下:是不是所有非顶级节点最终都能追溯到根?如果不能,可能需要临时加一个路径深度限制。这个问题在面试聊起来时很加分,说明你考虑过真实数据质量问题。

4. 笔试答题时我踩过的坑和自查清单

我在帮人复盘卷子时,发现很多错误并不是“不会写”,而是“写得太急”导致的低级失误。下面这些坑,我基本都亲眼见过,自己也掉进去过。

4.1 结果集多一行、少一行的隐蔽原因

先说最常见的:LEFT JOIN之后在WHERE里对右表字段做条件过滤,把左表行给过滤掉了。比如:

SELECT u.user_id, o.amount FROM user u LEFT JOIN order o ON u.user_id = o.user_id WHERE o.amount > 100;

如果用户没有订单,o.amount是NULL,NULL > 100的结果是NULL,不满足WHERE,所以整行被过滤,LEFT JOIN退化成INNER JOIN。正确做法是把过滤条件放进ON子句:

SELECT u.user_id, o.amount FROM user u LEFT JOIN order o ON u.user_id = o.user_id AND o.amount > 100;

这个差别在笔试里非常常见,而且很多人很难一眼发现。建议写完SQL后,用一个小数据集手推一遍,例如输入两个用户,其中一个无订单,看结果是否符合预期。

另一个产生多余行的坑是关联字段不唯一。比如两张表,一张是用户维度,一张是订单事实,如果直接JOIN user_id,而右侧该用户有多条订单,结果行数会翻倍。如果想保留左表一行,就必须先聚合右表,或者用DISTINCT ON(PostgreSQL)。笔试题目如果没说清楚,一般默认是多对一关系,但你最好在注释里说明你的假设。

4.2 窗口函数里的排序字段有重复值?小心结果“随机”

如果窗口函数ORDER BY的字段有大量重复值,比如按薪资排序时有100人薪资相同,ROW_NUMBER()到底给谁1、谁2,数据库没有明确保证。某些笔试平台会按物理顺序赋值,那你的排名结果可能不稳定。所以在要求“并列相同”时用DENSE_RANK或RANK;在确实需要唯一序号时,ORDER BY里可以补一个主键字段,如ORDER BY salary DESC, emp_id。

我还见过有候选人把窗口函数写在WHERE子句里,然后报语法错误。记住,窗口函数只能在SELECT列表和ORDER BY中使用,如果想基于窗口结果过滤,必须包一层子查询或CTE。

4.3 时间分配和读题顺序:笔试的隐形竞争

SQL笔试题一般给60到120分钟,2到3道题。我的建议是:拿到题先全部读一遍,标出每道题用到的表和字段,特别是确认日期字段的格式、是否存在重复数据。然后从自己最熟练的题型开始做,把有把握的分数先拿稳。

如果一道题卡了超过15分钟,立刻跳下一题。SQL笔试不像算法题,很多解法你一眼就知道,卡住往往是因为漏看条件或者表理解错了。跳题后回来重读一遍,很可能突然开窍。

做完全部题目后,一定要留出5到10分钟做自查。自查链:检查每张表的“每一行”是否会被过滤掉;检查GROUP BY后SELECT的字段是否合法;检查窗口函数的PARTITION BY和ORDER BY是否写反;检查日期函数前后单位是否一致。

5. 考前一个月我这样刷题

最后聊点备考策略。很多人喜欢在笔试前疯狂刷题,但市面上题目质量参差不齐。我自己摸索出一套分阶段练习法,效果还不错,分享给你。

5.1 分三阶段:基础-套路-限时模拟

第一个阶段(第1周): 把SQL基础语法过一遍,重点是JOIN和GROUP BY,因为这两个是几乎每道题都会用到的地基。不要只看博客,要在本地数据库里把left join、inner join、full join、cross join的区别都测一遍,观察行数变化。

第二个阶段(第2周): 按题型专项练。建议总结一个“题型模板表”,比如“看到每个部门→PARTITION BY”“看到最近N天→LAG/LEAD”“看到累计→SUM OVER”“看到留存→先求首日再关联”等等。我不反对背模板,但要背着模板去理解题目,而不是套模板。

第三个阶段(第3-4周): 找在线SQL平台或者自己造数据,严格按笔试要求限时做题。每次模拟120分钟,3道题,一道基础聚合、一道窗口函数、一道业务建模。模拟的目的不是刷题量,而是练时间感和考场心态。我通常模拟完不会马上看答案,而是先复盘自己哪一步卡住了,再回去补对应的知识点。

5.2 学会“读题拆题”:把业务句子翻译成SQL骨架

看到一个题目,不要急着写SQL。先用中文把题目拆成几步:要查的表是哪张?筛选条件是什么?分组的维度是什么?输出的每一个字段怎么算?如果需要关联,关联键是什么?关联方向是什么?

举个例子,题目说“统计每个部门平均薪资高于公司平均薪资的部门”。这句话拆开就是:先算公司平均薪资(一个标量),再把部门作为分组,算每个部门平均薪资,最后HAVING比较。于是骨架自然浮现:

SELECT dept_id, AVG(salary) AS dept_avg FROM employee GROUP BY dept_id HAVING AVG(salary) > (SELECT AVG(salary) FROM employee);

每一步都有据可依,就不容易漏条件。

5.3 本地造数据:千万别偷懒

我个人最喜欢的方法是:在本地装一个MySQL或PostgreSQL,亲手建一套面试常考的表,比如员工表、订单表、登录表、点击日志表,然后往里面插入50到100条模拟数据,再围绕它们自己给自己出题。

为什么要自己造数据?因为你在网上找的题库往往只有查询语句,没有完整的建表插入语句,你根本不知道那些表字段是什么类型、有哪些脏数据。自己造数据能逼着你思考数据的真实形态:日期会不会重复?金额能不能为负?外键是否缺失?这些问题在笔试中恰好是出题人爱埋的雷。

我自己的习惯是,在纸上画一个简单的ER图,模拟各个表之间的关系,然后写建表语句。写完再针对每张表写至少5条查询,包括离线数据探索时的常用查询。等把这一套下来,你去看大厂笔试题,通常会感觉“这些场景我都见过了”。

最后再分享一个小技巧:笔试前把窗口函数的几种框架写在一张纸上。比如SUM(amount) OVER (PARTITION BY ... ORDER BY ... ROWS BETWEEN ...)里,那个ROWS BETWEEN的几种边界写法你真的分清了吗?如果分不清,就把它背熟。很多时候,面试官并不指望你在5分钟内憋出一个神级解法,他更希望看到你写出的SQL是清晰、准确、可维护的。能做到这一点,你已经超过了大多数人。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:59:29

openrig实战:轻量级GPU资源池化与调度提升算力利用率

1. 从“机器堆着吃灰”到“算力随手取”&#xff1a;openrig到底解决了什么先说一个很多跑AI工作负载的团队都会遇到的状态&#xff1a;机器越来越多&#xff0c;利用率却一直上不去。我在过去两年里帮几个朋友看过他们内部的工作站和自建服务器&#xff0c;普遍情况是这样的—…

作者头像 李华
网站建设 2026/10/15 12:59:21

浏阳汽车贴膜哪家专业?浏阳服务好的汽车贴膜门店哪家好。最新名单!贴隐形车衣、改色膜门店选哪家

备注信息&#xff1a; 浏阳汽车后市场&#xff0c;近几年已经从简单 “贴一层膜”&#xff0c;升级为 “按车型做整套漆面与玻璃防护”。一方面&#xff0c;浏阳新能源保有量持续上涨&#xff0c;全景天幕、隐藏门把手、毫米波雷达、智能传感器越来越普及&#xff0c;贴膜早已不…

作者头像 李华
网站建设 2026/10/15 12:59:20

扫描线算法在碰撞检测中的高效应用与优化

1. 扫描线算法概述扫描线算法&#xff08;Sweep Line Algorithm&#xff09;是计算机图形学和计算几何中一种经典的空间划分技术。它通过引入一条虚拟的扫描线&#xff0c;按照特定顺序&#xff08;通常从左到右或从上到下&#xff09;扫描空间中的对象&#xff0c;从而将复杂的…

作者头像 李华
网站建设 2026/10/15 12:59:08

轻量级知识图谱构建:跨源对齐与低资源关系补全实战

1. 这不是一篇“竞赛复盘”&#xff0c;而是一份可落地的知识图谱工程实践手记2020年ICDM知识图谱竞赛——这个标题乍看像学术会议的边角料&#xff0c;实则藏着一套被工业界反复验证过的轻量级知识图谱构建范式。我第一次看到这个方案时正在帮某高校实验室处理一批非结构化医疗…

作者头像 李华
网站建设 2026/10/15 12:58:49

2026年北京GEO公司遇到独家条款?该怎么判断

摘要&#xff1a;本文回答"服务方提出独家合作这类排他条款时&#xff0c;该怎么判断利弊、要不要签"&#xff0c;说明独家条款要看哪几处边界、退出的口子在哪&#xff0c;并给出一张条款风险核对表&#xff1b;内容依据公开可查来源与行业观察整理&#xff0c;属分…

作者头像 李华